自定义View的绘制流程基础分析

2,305 阅读5分钟

一个基本的自定义View应该做的事情:

  1. 绘制角度来说,一共有三点 测量摆放绘画他们本身以及子views(针对于ViewGroup而言)。
  2. 保存UI状态
  3. 处理触摸事件

今天先从绘制流程开始学习吧,绘制流程:constructor()->onMeasure()->onLayout()->onDraw()

在开始之前,我们先来看看Android 默认视图的层级:


1. 通过onMeasure()方法,根据父容器的尺寸大小和约束,能知道一个View要占多大的地方。这是一个自下而上执行的方法,也就是说,先从最下层的RootView开始执行测量,然后分发测量viewGroup中众多的子Views。

  • 要搞清楚onMeasure(),我们先来看一下方法原型吧。

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {....}
    

    刚刚读源码的童鞋可能并不知道widthMeasureSpecheightMeasureSpec这两个参数的作用是什么。这两个参数实际上可以通过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有三种,分别为:UNSPECIFIEDEXACTLYAT_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位置的(如:水平摆放或者垂直摆放,在这里同学们可以参考一下LinearLayoutonLayout()方法的实现),一般我们只继承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的onMeasureonLayout方法,去重新评估view的大小和所在位置。

  • Enjoy Android :) 如果有误,轻喷,欢迎指正。