Android 自定义 view 的一些知识点
View 的绘制
View 的绘制分为 3 部分:
measure
测量,决定了 View 的测量宽、高。几乎所有情况下都等同于 View 的最终宽、高(如果 View 需要多次 measure 才能确定大小,或者重写了
layout()方法,并修改了传入的值的话则不会相等)。layout
布局,决定 View 的四个顶点坐标和实际的宽、高。
draw
绘制,决定了 View 的具体显示内容。
其中通过 ViewRootImpl 类的performTraversals()依次调用performXXX()方法。
MeasureSpec
MeasureSpec 是一个 32 位 int 值,高 2 位表示 SpecMode,低 30 位表示 SpecSize。
SpecMode 有 3 种可能值:
- UNSPECIFIED 父容器没有限定 View 大小,可以是任意需要的大小
 - EXACTLY 父类指定了 View 的具体大小,View 的最终大小就是这个值 (match_parent 或者具体数值)
 - AT_MOST View 可以是这个值以内的任意大小 (wrap_content)
 
我们指定的 View 的 LayoutParams 和父容器(DecorView 则是窗口的尺寸,普通 View 是父容器的 MeasureSpec)一起决定了 View 的 MeasureSpec,进而决定了 View 的宽高。
SpecSize 决定于父容器的尺寸、以及 View 的 margin 和 padding。
View 绘制流程
final 类型的measure()方法调用onMeasure()方法。
public final void measure(int widthMeasureSpec, int heightMeasureSpec){
   if (forceLayout || needsLayout) {
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
   }
}在onMeasure()调用了setMeasuredDimension()方法设置了 View 宽、高的测量值。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;//返回 getSuggestedMinimumWidth/Height 的大小
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;//返回测量大小
            break;
        }
        return result;
}getSuggestedMinimumXXX()的值:
如果 View 没有背景,则返回的是 View 的android:miniWidth指定的值;
如果 View 有背景,则返回的是背景的minimumWidth的值和android:miniWidth指定的值中最大的一个值。
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }由此,我们知道,如果直接继承自 View 的控件必须重写onMeasure()方法,设置 wrap_content 时候控件的大小。这是因为:
wrap_content 对应的 specMode 是 AT_MOST 模式,其宽高等于specSize。
根据 ViewGroup 的getChildMeasureSpec()方法,我们知道此时的specSize是父容器目前可以用的大小,即这种情况下 wrap_content 的效果和 match_parent 的效果是一样的。
要避免这种情况,就需要重写onMeasure()方法,在里面专门指定 wrap_content 时 View 对应的大小。
获取 View 的宽高
由于 View 的绘制和 Activity 的生命周期不同步,所以在onCreate()/onStart()/onResume()中都无法有效获取 View 的宽高。使用以下方式则可以正常获取 View 的宽高:
Activity/View#onWindowFocusChanged()
当前的 Window 获取或失去焦点的时候调用,此时 View 已经初始化完毕,可以获取宽、高。
Activity 窗口焦点变化 (onPause/onResume) 时会被调用多次。
View#post(runnable)
该 runnable 在 view 的消息队列尾部,被执行时 View 已经初始化好了,可以在这里获取宽高。
ViewTreeObserver
注册 onGlobalLayoutListener,当 View 树的状态变更,或者 View 树内部 View 可见性发生变化就会被回调。
当 View 树的状态变更可能被调用多次。
View#measure()
手动调用
measure()方法获取宽高。
draw 过程
绘制过程分为以下几步:
- 绘制背景 
background.draw(canvas); - 绘制自身 
onDraw(canvas); - 绘制 children 
dispatchDraw(canvas); - 绘制装饰 
onDrawForeground(canvas); 
setWillNotDraw()表示当前的 ViewGroup 不需要绘制任何内容,系统会对此进行优化(默认启用)。如果 ViewGroup 需要绘制内容时,则需要手动关闭这个标志。
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}绘制两个图形重叠部分
android 自定义 view 时两个图形重叠部分的绘制方式,一定要调用canvas.saveLayer() ,否则不生效。
        //这个步骤十分重要,将当前画布保存为新的一层
        int save = canvas.saveLayer(0,0,mWidth,mHeight,null,Canvas.ALL_SAVE_FLAG);
        Paint paint = new Paint();
        paint.setColor(mBackgroundColor);
        RectF backgroundRectF = new RectF(0, 0, mWidth, mHeight);
        canvas.drawRoundRect(backgroundRectF, mRadius, mRadius, paint);
        paint.setColor(mForwardColor);
        //设置二者重叠部分的绘制方式
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        RectF progressRectF = new RectF(0, 0, mWidth * mProgress, mHeight);
        canvas.drawRect(progressRectF,paint);
        //restore to canvas
        canvas.restoreToCount(save);paint.setXfermode()可以设置的值参考下图:

参考自【原】使用 Xfermode 正确的绘制出遮罩效果 - sky0014 - 博客园
适配自定义 view 宽高,设置默认值
以其宽度为例,在onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
    mWidth = widthSize;
} else {
    mWidth = 100;
    if (widthMode == MeasureSpec.AT_MOST) {
        mWidth = Math.min(mWidth, widthSize);
    }
}参考资料
《Android 开发艺术探索》
