0.4 View 工作流程

自定义 View 核心要点总结与代码注释

一、核心知识点总结

1. View 的工作流程

  • measure:确定 View 的测量宽高(onMeasure
  • layout:确定 View 的最终位置(onLayout,对单一 View 无作用)
  • draw:将内容绘制到屏幕上(onDraw

2. MeasureSpec 说明

  • EXACTLY:对应 match_parent 或具体数值
  • AT_MOST:对应 wrap_content
  • UNSPECIFIED:父容器不对子 View 限制大小(多用于系统内部)

3. 自定义 View 注意事项

  • 必须处理 wrap_content:否则其行为等同于 match_parent
  • 必须手动处理 padding:在 onDraw 中考虑 padding 值
  • 背景影响最小尺寸getSuggestedMinimumWidth() 会结合 minWidth 与背景 drawable 的最小宽度
  • 推荐使用 resolveSize():统一处理 MeasureSpec 逻辑

4. 自定义 View 分类

  • 继承 View:需自行处理 wrap_contentpadding
  • 继承已有控件(如 TextView):无需处理测量和 padding
  • 继承 ViewGroup:需实现子 View 的测量与布局
  • 组合控件:继承 ViewGroup 封装多个子 View

二、代码文件(带完整注释)

CircleView.kt

package com.shakespace.artofandroid.chapter04

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import com.shakespace.artofandroid.R

/**
 * 自定义 CircleView,演示如何正确处理:
 * - 自定义属性(circle_color)
 * - wrap_content 的默认尺寸
 * - padding 的绘制偏移
 */
class CircleView @JvmOverloads constructor(
    context: Context?,
    private val attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    // 默认颜色为红色
    var color = Color.RED
    // 开启抗锯齿的画笔
    val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    init {
        initView()
    }

    /**
     * 初始化自定义属性
     */
    private fun initView() {
        // 从 XML 中读取自定义属性
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView)
        color = typedArray.getColor(R.styleable.CircleView_circle_color, Color.RED)
        // radius 属性被注释,实际使用 view 宽高决定
        paint.color = color
        typedArray.recycle() // 必须回收
    }

    /**
     * 重写 onMeasure 以支持 wrap_content
     * 若不重写,wrap_content 行为等同于 match_parent
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // 调用父类无实际作用,但保留良好习惯
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        // 使用 resolveSize 处理 AT_MOST (wrap_content) 情况
        // 默认期望大小为 200px
        val measuredWidth = Math.max(
            this.suggestedMinimumWidth,
            resolveSize(200, widthMeasureSpec)
        )
        val measuredHeight = Math.max(
            this.suggestedMinimumHeight,
            resolveSize(200, heightMeasureSpec)
        )

        // 设置最终测量尺寸
        this.setMeasuredDimension(measuredWidth, measuredHeight)
    }

    /**
     * 绘制圆形,正确处理 padding
     */
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        // 减去 padding 得到可绘制区域
        val width = width - paddingLeft - paddingRight
        val height = height - paddingTop - paddingBottom

        // 取可绘制区域的内切圆半径
        val radius = Math.min(width, height) / 2

        // 圆心位置需加上 paddingLeft / paddingTop
        val centerX = (paddingLeft + width / 2).toFloat()
        val centerY = (paddingTop + height / 2).toFloat()

        canvas?.drawCircle(centerX, centerY, radius.toFloat(), paint)
    }
}

三、 .txt 内容

z_memo01_view.txt

1. ViewRoot -- ViewRootImpl

       start with performTraversals
       performMeasure -- measure -- onMeasure ----- (View) measure

       performLayout -- layout --onLayout     -----(View ) layout

       performDraw -- draw -- onDraw          ----- (View ) draw


2. when measure finished , can use getMeasurdWidth/getMeasureHeight to get width/height (usually it is  equivalent to final width/height)

3. when layout finished , can use getTop/getBottom/getLeft/getRight to get top/bottom/left/right position

4. MeasureSpec
    UNSPECIFIED     usually use in system interior
    EXACTLY         final width/height == SpecSize , match to match_parent or exact value in xml.
    AT_MOST         width/height <= SpecSize , match to wrap_content

5. ViewRootImpl.java
        private static int getRootMeasureSpec(int windowSize, int rootDimension) {
            int measureSpec;
            switch (rootDimension) {

            case ViewGroup.LayoutParams.MATCH_PARENT:
                // Window can't resize. Force root view to be windowSize.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                // Window can resize. Set max size for root view.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                break;
            default:
                // Window wants to be an exact size. Force root view to be that size.
                measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                break;
            }
            return measureSpec;
        }

6. for View , it's MeasureSpec depends on Parental MeasureSpec and child's LayoutParams

    exact value  -- > EXACTLY
    match_parent ---> parentSize  , if parent is UNSPECIFIED  child is 0
    wrap_content --->
            EXACTLY/AT_MOST --> AT_MOST and not more than parentSize
             UNSPECIFIED  ----> child is 0

7.
    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;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;getChildMeasureSpec
            break;
        }
        return result;
    }

    So , Both AT_MOST and EXACTLY , will return specSize in MeasureSpec.
    When UNSPECIFIED , size would be getSuggestedMinimumWidth() or getSuggestdMinimumHeight()


   protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

   public int getMinimumWidth() {
            final int intrinsicWidth = getIntrinsicWidth();
            return intrinsicWidth > 0 ? intrinsicWidth : 0;
   }

   如果view没有背景, 那么宽度是mMinWidth , 对应属性 android:minWidth 的值,如果没有设置,则为0 。
   如果设置了背景,宽度为max(mMinWidth,mBackground.getMinimumWidth())

    getMinimumWidth 返回的是drawable的原始宽度,如果没有,返回0。 (ShapeDrawable没有原始宽度,BitmapDrawable有原始宽度)

   note : 直接继承View, 需要重写onMeasure处理wrap_content时的自身大小(通常是一个默认size), 否则wrap_content和match_parent相同

    solution: set a width/height when wrap_content

     e.g:
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int size = this.dp2px(150.0F);
        this.setMeasuredDimension(
            Math.max(this.getSuggestedMinimumWidth(),
                resolveSize(size, widthMeasureSpec)),
            Math.max(this.getSuggestedMinimumHeight(),
                resolveSize(size, heightMeasureSpec)));
    }

8.


z_memo02_viewgroup.txt

ViewGroup

1. ViewGroup is a abstract class , it will call measureChildren to measure child view

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    create childWidthMeasureSpec by getLayoutParams and parentWidthMeasureSpec

    getChildMeasureSpec

2. LinearLayout
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            if (mOrientation == VERTICAL) {
                measureVertical(widthMeasureSpec, heightMeasureSpec);
            } else {
                measureHorizontal(widthMeasureSpec, heightMeasureSpec);
            }
        }

        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                                heightMeasureSpec, usedHeight);
            will call child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

        use mTotalLength to store the preliminary height

                // Add in our padding
                mTotalLength += mPaddingTop + mPaddingBottom;

                int heightSize = mTotalLength;

                // Check against our minimum height
                heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

                // Reconcile our calculated size with the heightMeasureSpec
                int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
                heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

        LinearLayout will measure itself once children's measurement finished
            在Vertical模式下, 如果是match_parent, 则高度为specSize,如果是wrap_conent,则高度是所有子元素高度总和,但不能超过父容器剩余空间

3. 通常在measure完成之后就可以通过getMeasuredWidth/Height来获取测量的宽高,但是某些情况下,系统可能需要多次measure,所以最好在onLayout中再去获取测量的宽高。

4. 在onCreate\onStart\onResume 都无法正确获得测量的宽高,可以使用以下四种方式
        Activity/View -- onWindowFocusChanged (会被调用多次)
        view.post(runnable)
        ViewTreeObserver . onGlobalLayoutListener   (调用多次)
        view.measure(int widthMeasureSpec,int heightMeasureSpec)
            需要根据LayoutParams判断,如果是match_parent 无法测量。
            如果是具体值:View.MeasureSpec.makeMeasureSpec(100,View.MeasureSpec.EXACTLY) 得到spec
            如果是wrap_content:通过 View.MeasureSpec.makeMeasureSpec((1 shl 30)-1,View.MeasureSpec.AT_MOST) 得到spec

            * 1 shl 30 是 1 << 30 的Kotlin写法

5. two incorrect way
            View.MeasureSpec.makeMeasureSpec(-1,View.MeasureSpec.UNSPECIFIED)
            view.measure(WindowManager.LayoutParams.WRAP_CONTENT,indowManager.LayoutParams.WRAP_CONTEN)

6. Layout
    通过setFrame 设定view的四个顶点,初始化mLeft、mRight、mTop、mBottom
    再通过onLayout确定子View的位置

    在LinearLayout中的VerticalLayout方法
        childTop += lp.topMargin;
                    setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                            childWidth, childHeight);
        childTop会不断增加。

        private void setChildFrame(View child, int left, int top, int width, int height) {
            child.layout(left, top, left + width, top + height);
        }
        其中的width和height ,就是子元素的测量宽高。

7. getMeasuredWidth 和 getWidth 区别
    1.在默认实现中, 两者是相同的
    2.两者的赋值实际不同。
    3. 如果重写layout方法
        public void layout(int l , int t , int r, int b ){
            super.layout(l,t,r+100,b+100)
        }
        没有实际意义,但是会导致最终宽高比测量宽高大100px
    4. 此外,通常测量会进行多次,前几次测量的结果可能不准确。

8. onDraw()
    1. draw background      background.draw(canvas)
    2. draw itself          onDraw()
    3. draw children        dispatchDraw
    4. draw decoration      onDrawScrollBars

        comments in ViewGroup.java
            /*
             * Draw traversal performs several drawing steps which must be executed
             * in the appropriate order:
             *
             *      1. Draw the background
             *      2. If necessary, save the canvas' layers to prepare for fading
             *      3. Draw view's content
             *      4. Draw children
             *      5. If necessary, draw the fading edges and restore layers
             *      6. Draw decorations (scrollbars for instance)
             */

9. View has a special  method "setWillNotDraw" , if a view not need draw anything , set true.
    in default , this flag in View is false , and is true in ViewGroup , so if need draw something in ViewGroup
    need set this in false .

        public void setWillNotDraw(boolean willNotDraw) {
           setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
       }

z_memo03_custom_view.txt

自定义view分类
1. 继承View重写onDraw()
    通常用于实现一些不规则的效果,通过绘制的方式实现。
    需要自己支持 wrap_content和padding
2. 继承ViewGroup 派生特殊的Layout
    需要处理ViewGroup的测量和布局过程,并同时处理子元素的测量和布局过程。
3. 继承特定的View
    用于扩展已有View的功能, 例如TextView, 不需要自己支持wrap_content和padding
4. 继承特定的ViewGroup
    当需要将某几种View组合在一起时常用,可以将多个view放在一个ViewGroup中。,不需要处理ViewGroup的测量和布局过程。

自定义View须知
    1、支持wrap_content
        需要在onMeasure时对wrap_content做处理,给一个最小宽高。
    2、如果有必要,需要支持padding
        如果不在onDraw时计算padding,那么padding将不起作用。
        ViewGroup的measure和layout中也需要考虑padding的影响。
    3、 尽量不要在View中使用handler,使用post完全可以替代handler
    4、 有动画和线程需要及时停止,
        可以在onDetachedFromWindow中操作
        此方法对应onAttachedToWindow
        当View不可见时也需要及时处理线程和动画。
    5、 带有滑动嵌套时,需要处理滑动冲突。
posted @ 2026-01-20 23:02  y丶innocence  阅读(0)  评论(0)    收藏  举报