android开发分享重学Android – 自定义View

View和ViewGroupView是Android所有控件的基类,同时ViewGroup也是继承自View。ViewGroup作为View或者ViewGroup这些组件的容器,派生了 多种布局控件子类,比如LinearLayout、RelativeLayout等Android坐标系Android视图坐标系View获取自身宽高getHeight():获取View自身高度getWidth():获取View自身宽度MotionEvent提供的方法我们看上图那个深蓝色的点,假设就是我们触

参考链接

View和ViewGroup

View是Android所有控件的基类,同时ViewGroup也是继承自View。ViewGroup作为View或者ViewGroup这些组件的容器,派生了 多种布局控件子类,比如LinearLayout、RelativeLayout等

重学Android - 自定义View

Android坐标系

重学Android - 自定义View

Android视图坐标系

重学Android - 自定义View

View获取自身宽高

  • getHeight():获取View自身高度
  • getWidth():获取View自身宽度

MotionEvent提供的方法

我们看上图那个深蓝色的点,假设就是我们触摸的点,我们知道无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理,MotionEvent也提供了各种获取焦点坐标的方法:

  • getX():获取点击事件距离控件左边的距离,即视图坐标
  • getY():获取点击事件距离控件顶边的距离,即视图坐标
  • getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标
  • getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标

View滑动的六种方法

layout(int l, int t, int r, int b)

通过修改View的left、top、right、bottom这四种属性来控制View的坐标。

传进来里面的四个参数分别是View的四个点的坐标,它的坐标不是相对屏幕的原点,而且相对于它的父布局来说的。

public boolean onTouchEvent(MotionEvent event) {         //获取到手指处的横坐标和纵坐标         int x = (int) event.getX();         int y = (int) event.getY();          switch (event.getAction()) {             case MotionEvent.ACTION_DOWN:                 lastX = x;                 lastY = y;                 break;              case MotionEvent.ACTION_MOVE:                 //计算移动的距离                 int offsetX = x - lastX;                 int offsetY = y - lastY;                 //调用layout方法来重新放置它的位置                 layout(getLeft()+offsetX, getTop()+offsetY,                         getRight()+offsetX , getBottom()+offsetY);                 break;         }          return true;     }  

offsetLeftAndRight()与offsetTopAndBottom()

将ACTION_MOVE中的代码替换成如下代码:

case MotionEvent.ACTION_MOVE:     //计算移动的距离     int offsetX = x - lastX;     int offsetY = y - lastY;     //对left和right进行偏移     offsetLeftAndRight(offsetX);     //对top和bottom进行偏移     offsetTopAndBottom(offsetY);     break; 

LayoutParams(改变布局参数)

LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局的参数从而达到了改变View的位置的效果。同样的我们将ACTION_MOVE中的代码替换成如下代码:

LinearLayout.LayoutParams layoutParams= (LinearLayout.LayoutParams) getLayoutParams();  layoutParams.leftMargin = getLeft() + offsetX;  layoutParams.topMargin = getTop() + offsetY;  setLayoutParams(layoutParams); 

因为父控件是LinearLayout,所以我们用了LinearLayout.LayoutParams,如果父控件是RelativeLayout则要使用RelativeLayout.LayoutParams。除了使用布局的LayoutParams外,我们还可以用ViewGroup.MarginLayoutParams来实现:

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();  layoutParams.leftMargin = getLeft() + offsetX;  layoutParams.topMargin = getTop() + offsetY;  setLayoutParams(layoutParams); 

scollTo与scollBy

scollTo(x,y)表示移动到一个具体的坐标点
scollBy(dx,dy)则表示移动的增量为dx、dy。
其中scollBy最终也是要调用scollTo的。scollTo、scollBy移动的是View的内容。

如果在ViewGroup中使用则是移动他所有的子View/子控件

Scroller
我们用scollTo/scollBy方法来进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可以使用Scroller来实现有过度效果的滑动。

Activity的构成

一个Activity包含一个window对象,这个对象是由PhoneWindow来实现的,PhoneWindow将DecorView做为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域一个是TitleView一个是ContentView,而我们平常做应用所写的布局正是展示在ContentView中的。
重学Android - 自定义View

MeasureSpec

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配。

MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一 起才能决定View的MeasureSpec,从而进一步决定View的宽/高。对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的 LayoutParams来共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽/高。

MeasureSpec和LayoutParams的对应关系
重学Android - 自定义View

前面已经提到,对于普通View,其MeasureSpec由父容器的 MeasureSpec和自身的LayoutParams来共同决定,那么针对不同的父容器和View本身不同的LayoutParams, View就可以有多种MeasureSpec。这里简单说一下,当View采用固定宽/高的时候,不管父容器的 MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小。当View的 宽/高是match_parent时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩 余空间;如果父容器是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。当 View的宽/高是wrap_content时,不管父容器的模式是精准还是最大化,View的模式总是最大化并且大小不 能超过父容器的剩余空间。可能读者会发现,在我们的分析中漏掉了UNSPECIFIED模式,那是因为这个 模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。

通过上图可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以快速地确定出 子元素的MeasureSpec了,有了MeasureSpec就可以进一步确定出子元素测量后的大小了。

自定义View的分类

  1. 继承View重写onDraw方法
    这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要 静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。 采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。

  2. 继承ViewGroup派生特殊的Layout
    这种方法主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统 的布局之外,我们重新定义一种新布局,当某种效果看起来很像几种View组合在一起的时候,可以采用这 种方法来实现。采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。

  3. 继承特定的View(比如TextView)
    这种方法比较常见,一般是用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实 现。这种方法不需要自己支持wrap_content和padding等。

  4. 继承特定的ViewGroup(比如LinearLayout)
    这种方法也比较常见,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实 现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和方法2的区 别,一般来说方法2能实现的效果方法4也都能实现,两者的主要差别在于方法2更接近View的底层。

自定义View须知

  1. 让View支持wrap_content
    这是因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果。

解决方案:在onMeasure中添加对wrap_content情况的判断,为wrap_content可以设置一个默认大小。

@Override protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) { super.onMeasure(widthMeasureSpec,heightMeasureSpec); 	int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 	int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);  	int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);  	int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);  	//wrap_content时设置默认大小为200 	if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { 		setMeasuredDimension(200,200); 	} else if (widthSpecMode == MeasureSpec.AT_MOST) { 		setMeasuredDimension(200,heightSpecSize); 	} else if (heightSpecMode == MeasureSpec.AT_MOST) {         setMeasuredDimension(widthSpecSize,200);     } } 
  1. 如果有必要,让你的View支持padding
    这是因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用 的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其 造成的影响,不然将导致padding和子元素的margin失效。

解决方案:针对padding的问题,也很简单,只要在绘制的时候考虑一下padding即可,因此我们需要对 onDraw稍微做一下修改,修改后的代码如下所示。

protected void onDraw(Canvas canvas) { super.onDraw(canvas); final int paddingLeft = getPaddingLeft(); final int paddingRight = getPaddingLeft(); final int paddingTop = getPaddingLeft(); final int paddingBottom = getPaddingLeft(); int width = getWidth() -paddingLeft -paddingRight; int height = getHeight() -paddingTop -paddingBottom; int radius = Math.min(width,height) / 2; canvas.drawCircle(paddingLeft + width / 2,paddingTop + height/2,radius, mPaint); } 
  1. 尽量不要在View中使用Handler,没必要
    这是因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你很明确地
    要使用Handler来发送消息。

  2. View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
    这一条也很好理解,如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时 机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调 用 , 和 此 方 法 对 应 的 是 onAttachedToWindow , 当 包 含 此 View 的 Activity 启 动 时 , View 的 onAttachedToWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,如果不及 时处理这种问题,有可能会造成内存泄漏。

  3. View带有滑动嵌套情形时,需要处理好滑动冲突

本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。

ctvol管理联系方式QQ:251552304

本文章地址:https://www.ctvol.com/addevelopment/891711.html

(0)
上一篇 2021年10月19日
下一篇 2021年10月19日

精彩推荐