自定义View的绘制流程基础分析
一个基本的自定义View应该做的事情:
从绘制角度来说,一共有三点 测量,摆放,绘画他们本身以及子views(针对于ViewGroup而言)。
保存UI状态。
处理触摸事件。
今天先从绘制流程开始学习吧,绘制流程:constructor()->onMeasure()->onLayout()->onDraw()
在开始之前,我们先来看看Android 默认视图的层级:
1. 通过onMeasure()方法,根据父容器的尺寸大小和约束,能知道一个View要占多大的地方。这是一个自下而上执行的方法,也就是说,先从最下层的RootView开始执行测量,然后分发测量viewGroup中众多的子Views。
要搞清楚onMeasure(),我们先来看一下方法原型吧。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {....}
刚刚读源码的童鞋可能并不知道widthMeasureSpec,heightMeasureSpec这两个参数的作用是什么。这两个参数实际上可以通过MeasureSpec.getSize(),MeasureSpec.getMode()这两个方法来判断该view被测量后的尺寸。那么,为什么还要通过MeasureSpec.getMode()呢?这就是接下来要说明的重点。
以下是MeasureSpec中剔除部分代码之后的getMode()实现方式。
private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; //我们通过位运算可以知道,MODE_MASK实际上等于11(二进制)左移30位的二进制,在Java中int占32位,所以这里就表示的是int中最高位为11,其余30位为0的二进制数。 public static final int UNSPECIFIED = 0 << MODE_SHIFT; public static final int EXACTLY = 1 << MODE_SHIFT; public static final int AT_MOST = 2 << MODE_SHIFT; .... public static int getMode(int measureSpec) { //measureSpec就是我们传进来的 widthMeasureSpec 或者 heightMeasureSpec //它与上measureSpec,低30位就全部都清零了。 //将返回值与UNSPECIFIED、EXACTLY、AT_MOST比较就能得到measureSpec的mode了。 return (measureSpec & MODE_MASK); } |
通过以上的注释style分析,我们知道了MeasureSpec是如何计算mode了,那么为什么要计算这些mode呢?与size又有什么关系呢? View的MeasureSpec Mode有三种,分别为:UNSPECIFIED,EXACTLY,AT_MOST,他们的规则如下(要注意一点的是,在子view中计算的mode,是parent view告诉它的):
UNSPECIFIED:parent view对子view没有任何的约束,子view可以任一尺寸,但必须要有。发生场景: 对于一个ScorllView来说,它通常不会去约束子view高度的,也就是说,子view的高度加起来有多高,那么ScrollView就有多高。在这种情况下,子view的mode就会为UNSPECIFIED。
EXACTLY:parent view已经有了确切的size,子view的大小不能够超过parent的大小。发生场景: 在parent view指定大小或者为match_parent的时候,子view会得到这个mode。
AT_MOST:在这种模式下,子view能够尽可能大的达到设定的尺寸或者原尺寸。发生场景: 在parent view指定大小或者match_parent的情况下,子view为wrap_content,那么mode将会为AT_MOST。
通过以上对mode的规则分析,我们就能够知道,在onMeasure中要搞到尺寸,是需要根据mode来搞的。
于是我们可以根据规则写出如下代码:
public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size;//如果不指定size,那么我们就使用默认的size break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize;//这两种mode都允许我们直接使用测量出来的size break; } return result; } |
2. 通过onLayout()方法知道这个控件应该放在哪个位置。*同样,是一个自下而上的方法。*一般我们只有在重写ViewGroup的时候需要自己处理onLayout()方法,因为该方法主要是ViewGroup用于摆放子view位置的(如:水平摆放或者垂直摆放,在这里同学们可以参考一下LinearLayout的onLayout()方法的实现),一般我们只继承View来定制我们的自定义View的时候,都不需要重写该方法。不过需要注意的一点是,子view的margin属性是否生效就要看parent是否在自身的onLayout方法进行处理,而view得padding属性是在onDraw方法中生效的。
以下是重头戏!!!
3. 通过onDraw(Canvas canvas)方法将这个控件绘画出来。主要是通过Paint和参数中的canvas,还有各种Animation以及invalidate()、postInvalidate()这两个方法去进行视图的重绘,实现动态效果。canvas中画各种图形的方法,比如说:rect(矩形),circle(圆形) 等等。当然你可以使用Path,PathMeasure去完成更加细腻的动画。下面结合一个很的效果实例源码,来说明动态自定义View。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //根据模式来赋值with 和 height int widthMode = MeasureSpec.getMode(widthMeasureSpec); if( widthMode == MeasureSpec.EXACTLY ) width = MeasureSpec.getSize(widthMeasureSpec); else width = ViewGroup.LayoutParams.MATCH_PARENT; int heightMode = MeasureSpec.getMode(heightMeasureSpec); if( heightMode == MeasureSpec.EXACTLY ) height = MeasureSpec.getSize(heightMeasureSpec); else height = ViewGroup.LayoutParams.MATCH_PARENT; //左端点的X leftX = margin; //右端点的X rightX = width - margin; y = height/2.0f; //线段的长度 distance = width - ( 2 * margin + 2 * radius ); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onDraw(Canvas canvas) { //绘制左端点 canvas.drawCircle( leftX, y , radius * factor , mPaint); //绘制右端点 canvas.drawCircle( rightX , y , radius * ( 1 - factor ), mPaint); mPaint.setStrokeWidth(5); //绘制线段 canvas.drawLine(margin , y , margin + radius+ ((radius + distance)*(1-factor)) , y, mPaint); } public void startAnimation(){ mAnimator = ValueAnimator.ofFloat(1, 0); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { factor = (float) animation.getAnimatedValue(); postInvalidate(); } }); mAnimator.setDuration(1500); // 重复次数 无限循环 mAnimator.setRepeatCount(ValueAnimator.INFINITE); // 重复模式, RESTART: 重新开始 REVERSE:恢复初始状态再开始 mAnimator.setRepeatMode(ValueAnimator.REVERSE); mAnimator.start(); } |
效果图
没错!就是这么皮!!╭(╯^╰)╮
requestLayout()、postInvalidate()、invalidate()的区别
1.实际上,后两者的作用是一样的,只不过postInvalidate内部会将重绘操作放入子线程中,而invalidate则是在调用线程中重绘view。
2.requestLayout在什么时候用呢?当view本身的测量属性改变了的时候,就可以调用该方法去让parent view去重新调用view的onMeasure,onLayout方法,去重新评估view的大小和所在位置。
Enjoy Android :) 如果有误,轻喷,欢迎指正。
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。