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 开发艺术探索》