惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
B
Blog RSS Feed
宝玉的分享
宝玉的分享
腾讯CDC
博客园_首页
T
Tailwind CSS Blog
月光博客
月光博客
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
A
About on SuperTechFans
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
MongoDB | Blog
MongoDB | Blog
博客园 - 聂微东
V
Visual Studio Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
SecWiki News
SecWiki News
美团技术团队
P
Privacy International News Feed
H
Help Net Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
Know Your Adversary
Know Your Adversary
Y
Y Combinator Blog
D
DataBreaches.Net
Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
S
Schneier on Security
G
GRAHAM CLULEY
博客园 - 三生石上(FineUI控件)
Cisco Talos Blog
Cisco Talos Blog
小众软件
小众软件
Forbes - Security
Forbes - Security
D
Docker
T
Tenable Blog
S
Secure Thoughts
雷峰网
雷峰网
S
Security @ Cisco Blogs
T
The Exploit Database - CXSecurity.com
The Cloudflare Blog
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
阮一峰的网络日志
阮一峰的网络日志

博客园 - y丶innocence

临时111 RecyclerView 数据多时无法滑动:ConstraintLayout 约束高度修复笔记 Android Kotlin OkHttp3 WebSocket 长连接与 Gson 数据解析系统笔记 Android + Kotlin + OkHttp WebSocket 相关概念与使用流程笔记(TLS/证书 + 鉴权/会话) AI对话导出markdown格式流程 代理转发 分享文件 面向全球的app的excel导出和kotlin IO原理 安卓导出笔记(未整理) java&kotlin listener 0.7 动画 0.3 view 滑动冲突 13. Jetpack 0. 安卓开发艺术探索参考资料 12. Material Design 7. 持久化技术 5. Fragment java 基础 4. UI 开发 3. Activity 2.2 Kotlin 面向对象 2.3 Kotlin高级 2.1 Kotlin基础 1. Android简介 [OpenJudge] 反正切函数的应用 (枚举)(数学) [OpenJudge] 摘花生 (模拟)
0.4 View 工作流程
y丶innocence · 2026-01-20 · via 博客园 - y丶innocence

自定义 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、 带有滑动嵌套时,需要处理滑动冲突。