一个基本的自定义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()的区别
- 实际上,后两者的作用是一样的,只不过
postInvalidate
内部会将重绘操作放入子线程中,而invalidate
则是在调用线程中重绘view。 requestLayout
在什么时候用呢?当view本身的测量属性改变了的时候,就可以调用该方法去让parent view去重新调用view的onMeasure
,onLayout
方法,去重新评估view的大小和所在位置。
- Enjoy Android :) 如果有误,轻喷,欢迎指正。