UI核心 - 绘制流程(测量、布局、绘制)

本文大纲: 话接上文,ViewRootImpl 中的测量、布局、绘制继续分析...

1. 测量流程源码分析

1.1. performMeasure与measure方法

  • mView即顶层容器DecorView,直接调用measure方法。
  • View.measure为final类型,不可重写。
  • 首先尝试读取缓存,若命中则调用onMeasure;若未命中,则重新设置并优化。

1.2. onMeasure与尺寸保存

  • onMeasure内部调用setMeasuredDimension和setMeasuredDimensionRaw。
  • 核心操作是对成员变量赋值并设置标记位,保存measuredWidth和measuredHeight。
  • 测量的本质是确定控件的宽和高。

2. MeasureSpec原理解析

2.1. 定义与结构

  • MeasureSpec封装了View的测量模式(Spec Mode)和尺寸(Spec Size)。
  • 它是一个32位int值:高2位表示模式,低30位表示尺寸。
c 复制代码
View = 模式 + 尺寸->MeasureSpec  32位int值

00000000000000000000000000000000

MODE_MASK : 11000000000000000000000000000000

~MODE_MASK: 00111111111111111111111111111111

SpecMode(前2位)   +  SpecSize(后30)

2.2. 三种模式常量

  • UNSPECIFIED:父容器不对View做限制,大小随意,通常系统内部使用。二进制前两位为00。
  • EXACTLY:精确模式,父容器已检测出View所需精确大小。对应match_parent或固定大小。二进制前两位为01。
  • AT_MOST:最大模式,父容器指定可用大小,View不能超过该值。对应wrap_content。二进制前两位为11。

2.3. 打包与解包方法

  • makeMeasureSpec(size, mode):将尺寸和模式打包成MeasureSpec。逻辑:(size & ~MODE_MASK) | (mode & MODE_MASK)。
  • ~MODE_MASK保留size低30位,MODE_MASK保留mode高2位。
  • getMode(spec):提取高2位模式。
  • getSize(spec):提取低30位尺寸。

3. DecorView的MeasureSpec确定规则

3.1. 计算入口

  • 通过getRootMeasureSpec方法,传入窗口宽度(window size)和顶层View的布局参数(lp.width)。

3.2. 具体规则

  • 若layout_width为MATCH_PARENT:模式为EXACTLY,尺寸为窗口大小。
  • 若layout_width为WRAP_CONTENT:模式为AT_MOST,尺寸为窗口大小(最大不超过窗口)。
  • 若layout_width为固定值:模式为EXACTLY,尺寸为固定值。

3.3. 结论

  • DecorView的MeasureSpec由窗口大小和自身LayoutParams决定。

4. 普通View/ViewGroup的MeasureSpec确定规则

4.1. 递归测量过程

  • FrameLayout.onMeasure遍历子View,调用measureChildWithMargins。
  • 该方法先调用getChildMeasureSpec获取子View的MeasureSpec,再调用子View的measure方法。

4.2. getChildMeasureSpec逻辑

参数:父容器spec、父容器padding、子ViewchildDimension。

  • 当父容器模式为EXACTLY时 :
    • 子View固定大小:模式EXACTLY,尺寸为固定值。
    • 子ViewMATCH_PARENT:模式EXACTLY,尺寸为父容器剩余空间(specSize - padding)。
    • 子ViewWRAP_CONTENT:模式AT_MOST,尺寸为父容器剩余空间。
  • 当父容器模式为AT_MOST时 :
    • 子View固定大小:模式EXACTLY,尺寸为固定值。
    • 子ViewMATCH_PARENT或WRAP_CONTENT:模式均为AT_MOST,尺寸为父容器剩余空间。
  • 当父容器模式为UNSPECIFIED时 :
    • 子View固定大小:模式EXACTLY,尺寸为固定值。
    • 子ViewMATCH_PARENT或WRAP_CONTENT:模式均为UNSPECIFIED,尺寸为0。

4.3. 结论

  • 普通View的MeasureSpec由父容器的MeasureSpec和自身LayoutParams共同决定。

5. ViewGroup与View的测量实现差异

5.1. ViewGroup测量

  • 先递归测量所有子View。
  • 根据子View的最大宽高、Padding、最小宽高、背景等因素,计算自身宽高。
  • 调用setMeasuredDimension保存自身尺寸。

5.2. View测量

  • 直接调用measure -> onMeasure -> getDefaultSize。
  • getDefaultSize中,无论EXACTLY还是AT_MOST,默认都返回specSize(父容器剩余空间)。

5.3. 重要结论

  • 若自定义View不重写onMeasure,match_parent和wrap_content效果一致,均填满父容器剩余空间。
  • 自定义View必须重写onMeasure以正确处理wrap_content。
  • 自定义ViewGroup需重写onMeasure以测量子View并确定自身大小。

6. 布局流程(Layout)

6.1. 入口与定位

  • performLayout调用host.layout。
  • View.layout中调用setFrame(l, t, r, b)确定左上右下四个坐标位置。

6.2. onLayout回调

  • layout方法最后调用onLayout(空实现)。
  • ViewGroup:需在onLayout中遍历子View并调用其layout方法,完成子View摆放(递归)。
  • View:只需确定自身位置,无需处理子View。

7. 绘制流程(Draw)

7.1. 入口

  • performDraw -> draw -> drawSoftware -> mView.draw。

7.2. View.draw六个步骤

  1. 绘制背景(drawBackground)。
  2. 保存图层缓存(如有必要)。
  3. 绘制内容(onDraw),自定义View需重写此方法。
  4. 绘制子View(dispatchDraw),ViewGroup需实现此方法以递归绘制子View。
  5. 绘制装饰(如滚动条)。
  6. 绘制前景。

7.3. 递归绘制

  • ViewGroup.dispatchDraw遍历子View,调用drawChild,进而调用子View的draw方法。

8. 总结与自定义建议

8.1. 布局

  • ViewGroup:重写onLayout摆放子View。
  • View:仅需确定自身位置,onLayout可选。

8.2. 绘制

  • 顺序:背景 -> 自身(onDraw) -> 子View(dispatchDraw) -> 装饰。

8.3. 自定义View

  • 必须重写onMeasure处理尺寸,重写onDraw绘制内容。

8.4. 自定义ViewGroup

  • 必须重写onMeasure测量子View,重写onLayout摆放子View;若仅包含系统控件,onDraw可不重写。
相关推荐
冬奇Lab2 小时前
LLM 驱动的自动化测试系列(07):移动端自动化(三)——Mobile-Agent-v3 与自研 GUI-Owl 模型路线
android·人工智能·测试
leobertlan5 小时前
痛苦系列 | DSP-02 频域切片:DTFT与DFT的探索
android·后端
leobertlan6 小时前
痛苦系列 | DSP-01 从连续到离散:DSP基础与采样
android·后端
事圆则缓8 小时前
Android 视频压缩上传实战:从 content Uri、转码到断点续传
android
冉冉同学11 小时前
AI Agent 开始操作真实手机:移动端自动化的 3 个新考点
android·ai编程
帅得不敢出门12 小时前
Android Framework关闭软件低电关机功能测试PMIC低电保护
android
恋猫de小郭13 小时前
Flutter + EmbeddingGemma 2,谷歌发布完全端侧的 AI Edge Foresight
android·前端·flutter
墨天梦14 小时前
B16_Material主题与可访问交互
android·kotlin·交互
弯_弯14 小时前
企业知识库 RAG 落地:从 Demo 到生产,四条工程链路与参数级自查
android·java·javascript