跳至主要內容

Android 自定义 view 的一些知识点

JI,XIAOYONG...大约 4 分钟

View 的绘制

View 的绘制分为 3 部分:

  1. measure

    测量,决定了 View 的测量宽、高。几乎所有情况下都等同于 View 的最终宽、高(如果 View 需要多次 measure 才能确定大小,或者重写了layout()方法,并修改了传入的值的话则不会相等)。

  2. layout

    布局,决定 View 的四个顶点坐标和实际的宽、高。

  3. 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 的宽高:

  1. Activity/View#onWindowFocusChanged()

    当前的 Window 获取或失去焦点的时候调用,此时 View 已经初始化完毕,可以获取宽、高。

    Activity 窗口焦点变化 (onPause/onResume) 时会被调用多次。

  2. View#post(runnable)

    该 runnable 在 view 的消息队列尾部,被执行时 View 已经初始化好了,可以在这里获取宽高。

  3. ViewTreeObserver

    注册 onGlobalLayoutListener,当 View 树的状态变更,或者 View 树内部 View 可见性发生变化就会被回调。

    当 View 树的状态变更可能被调用多次。

  4. View#measure()

    手动调用measure()方法获取宽高。

draw 过程

绘制过程分为以下几步:

  1. 绘制背景 background.draw(canvas);
  2. 绘制自身 onDraw(canvas);
  3. 绘制 children dispatchDraw(canvas);
  4. 绘制装饰 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 - 博客园 open in new window

适配自定义 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 开发艺术探索》

文章标题:《Android 自定义 view 的一些知识点》
本文作者: JI,XIAOYONG
发布时间: 2018/02/19 21:59:28 UTC+8
更新时间: 2023/12/30 16:17:02 UTC+8
written by human, not by AI
本文地址: https://jixiaoyong.github.io/blog/posts/e41787ec.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8