当前位置: 首页 > news >正文

Android中视图测量、布局、绘制过程

一、测量阶段(Measure)

1. MeasureSpec 机制详解
// MeasureSpec结构(32位int)
int spec = (mode << 30) | size;
  • 三种模式

    模式触发场景特点
    UNSPECIFIED0ScrollView/RecyclerView子View父容器不限制子View尺寸
    EXACTLY1match_parent/固定值(dp)子View必须使用指定尺寸
    AT_MOST2wrap_content子View尺寸不超过指定值
  • 生成规则(核心方法 ViewGroup.getChildMeasureSpec()):

    public static int getChildMeasureSpec(int parentSpec, int padding, int childDimension) {int parentMode = MeasureSpec.getMode(parentSpec);int parentSize = MeasureSpec.getSize(parentSpec);int size = Math.max(0, parentSize - padding);int resultSize = 0;int resultMode = 0;switch (parentMode) {case MeasureSpec.EXACTLY:  // 父容器有确定尺寸if (childDimension >= 0) {       // 子View固定尺寸resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {resultSize = size;            // 子View填满父容器resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.WRAP_CONTENT) {resultSize = size;            // 子View尺寸不超过父容器resultMode = MeasureSpec.AT_MOST;}break;case MeasureSpec.AT_MOST:  // 父容器尺寸不确定if (childDimension >= 0) {resultSize = childDimension;  // 子View固定尺寸优先resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {resultSize = size;            // 子View尺寸不超过父容器resultMode = MeasureSpec.AT_MOST;} else if (childDimension == LayoutParams.WRAP_CONTENT) {resultSize = size;             // 同上resultMode = MeasureSpec.AT_MOST;}break;case MeasureSpec.UNSPECIFIED:  // 父容器无限制if (childDimension >= 0) {resultSize = childDimension;  // 子View固定尺寸resultMode = MeasureSpec.EXACTLY;} else {resultSize = 0;               // 子View可任意尺寸resultMode = MeasureSpec.UNSPECIFIED;}break;}return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
2. 测量流程源码级分析

关键步骤:

  1. ViewRootImpl 触发 performTraversals()

  2. DecorView 开始测量:

    • 调用 measure() → onMeasure()

    • 遍历子View(ViewGroup)并调用其 measure()

  3. ViewGroup 测量逻辑:

    • 通过 measureChildWithMargins() 为子View生成MeasureSpec

    • 调用子View的 measure() 方法

  4. 子View 响应测量:

    • 在 onMeasure() 中计算自身尺寸

    • 必须调用 setMeasuredDimension() 保存结果

3. 高频问题:为何 onMeasure() 被多次调用?
调用场景触发原因调用次数源码定位
常规ActivitySurface创建流程2次ViewRootImpl#measureHierarchy() → performMeasure()
relayoutWindow()后再次performMeasure()
Dialog主题宽度自适应尝试最多6次measureHierarchy()中三次测量尝试:
1. 预设宽度(baseSize)
2. 折中宽度((baseSize+desiredWidth)/2)
3. 全屏宽度
权重布局LinearLayout权重计算+1次LinearLayout#measureVertical()中二次测量带权重子View
滑动容器RecyclerView预加载动态增加RecyclerView#onMeasure()中预测量屏幕外item

常见问题:

Q1:描述MeasureSpec的作用和组成?
A:
MeasureSpec是32位int值,包含:

  1. 模式(高2位)

    • UNSPECIFIED:父容器不限制子View尺寸(如ScrollView子View)

    • EXACTLY:子View必须使用精确尺寸(match_parent/固定值)

    • AT_MOST:子View尺寸不超过设定值(wrap_content)

  2. 尺寸(低30位):父容器提供的可用空间

  3. 核心作用:父容器通过MeasureSpec向子View传递布局约束条件


Q2:自定义View时onMeasure()要注意什么?
A: 必须处理三点:

  1. UNSPECIFIED模式

    protected void onMeasure(int widthSpec, int heightSpec) {int width = resolveSize(minWidth, widthSpec); // 处理无限制情况int height = resolveSize(minHeight, heightSpec);setMeasuredDimension(width, height);
    }
  2. wrap_content支持

    if (widthMode == MeasureSpec.AT_MOST) {width = Math.min(desiredWidth, widthSize); // 限制在AT_MOST范围内
    }
  3. 调用setMeasuredDimension()

源码中使用的是setMeasuredDimension(getDefaultSizexxx),而对于getDefaultSize,对于AT_MOST以及EXACTLY的情况,返回大小事一样的,如果自定义View,需要重写onMeasure方法,对Wrap_content情况进行处理


Q3:ScrollView子View的测量为何特殊?
A: 源码强制修改高度模式为UNSPECIFIED:

// frameworks/base/core/java/android/widget/ScrollView.java
protected void measureChild(View child, int parentWidthSpec, int parentHeightSpec) {int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);child.measure(childWidthMeasureSpec, childHeightSpec);
}

结果:无论子View设置wrap_contentmatch_parent,实际高度均为内容高度(可超过屏幕),通过滚动查看完整内容。


Q4:ViewGroup如何测量子View?
A: 关键三步:

  1. 计算可用空间

    int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
  2. 生成子View MeasureSpec

    int childWidthSpec = getChildMeasureSpec(parentWidthSpec, paddingLeft + paddingRight, lp.width);
  3. 触发子View测量

    child.measure(childWidthSpec, childHeightSpec);
    // 测量后通过child.getMeasuredWidth()获取结果

Q5:解释ViewRootImpl的测量触发链
A: 核心链路:

  • performTraversals() 是三大流程的总入口

  • measureHierarchy() 处理Dialog等自适应布局的多次测量

  • relayoutWindow() 在测量后申请Surface,触发二次测量


Q6:根视图MeasureSpec如何得到

A:

观察performTraversals()方法可以发现如下代码:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);

可以看到,这里调用了getRootMeasureSpec()方法去获取widthMeasureSpec和heightMeasureSpec的值,注意方法中传入的参数,其中lp.width和lp.height在创建ViewGroup实例的时候就被赋值了,它们都等于MATCH_PARENT。然后看下getRootMeasureSpec()方法中的代码,如下所示:

private int getRootMeasureSpec(int windowSize, int rootDimension) {int measureSpec;switch (rootDimension) {case ViewGroup.LayoutParams.MATCH_PARENT:measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);break;case ViewGroup.LayoutParams.WRAP_CONTENT:measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);break;default:measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);break;}return measureSpec;
}

可以看到,这里使用了MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec,当rootDimension参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当rootDimension等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。


二、布局阶段(Layout)

核心流程

关键知识点
  1. LayoutParams的核心作用

    • 存储View的布局参数(宽/高、margin等)

    • 自定义ViewGroup需重写generateLayoutParams()支持margin

  2. ViewGroup布局流程

    protected void onLayout(boolean changed, int l, int t, int r, int b) {for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);// 1. 计算子View位置(根据业务逻辑)int left = calculateChildLeft();int top = calculateChildTop();// 2. 调用子View.layoutchild.layout(left, top, left + width, top + height);}
    }
  3. 位置计算要点

    • 基于getMeasuredWidth/Height(测量阶段结果)

    • 需处理padding(父容器)和margin(子View)


三、绘制阶段(Draw)

核心流程

关键知识点
  1. 绘制顺序(软件绘制)

    public void draw(Canvas canvas) {drawBackground(canvas);    // 1. 绘制背景onDraw(canvas);            // 2. 绘制自身内容dispatchDraw(canvas);      // 3. 绘制子View(ViewGroup实现)onDrawForeground(canvas);  // 4. 绘制前景/滚动条
    }
  2. 绘制阶段:一是绘制流程执行的顺序,测量和布局阶段都是先执行子 View 再执行 ViewGroup 自身,而绘制是先执行 ViewGroup 绘制流程,再执行子 View 的绘制流程.View 的绘制流程和 ViewGroup 的绘制流程几乎一模一样,唯一的区别是 View 中的 dispatchDraw() 是空实现,因为它没有子视图.

  3. 硬件加速本质

    • DisplayListCanvas:在UI线程记录绘制指令

    • RenderThread:在渲染线程执行GPU绘图

    • 优势:避免重复录制指令(如View未失效时复用DisplayList)

  4. 优化点

    • 避免在onDraw中创建对象(引发GC)

    • 使用canvas.clipRect()减少过度绘制


总结

Q:简述自定义View的三大流程及核心方法?
A:

  1. Measure(测量)

    • 目标:确定View的宽高

    • 关键方法:onMeasure(int widthMeasureSpec, int heightMeasureSpec)

    • 核心机制:父容器通过MeasureSpec传递约束条件,子View调用setMeasuredDimension()保存结果

  2. Layout(布局)

    • 目标:确定View的位置(四个顶点坐标)

    • 关键方法:onLayout(boolean changed, int l, int t, int r, int b)

    • 核心机制:父容器遍历子View并调用其layout(),子View通过setFrame()保存坐标

  3. Draw(绘制)

    • 目标:将View绘制到屏幕

    • 关键方法:onDraw(Canvas canvas)

    • 执行顺序:背景 → 自身内容 → 子View → 前景

    • 硬件加速:通过DisplayListCanvas录制指令,由RenderThread异步执行

http://www.dtcms.com/a/319156.html

相关文章:

  • 嵌入式 - 数据结构:二叉树
  • GitHub 上 Star 数量前 20 的开源 AI 项目
  • X4000 私有 5G 实验室入门套件
  • 90-基于Flask的中国博物馆数据可视化分析系统
  • MySQL的变量、控制流程和游标:
  • 智能升级新纪元:基于Deepoc具身模型外拓开发板的除草机器人认知进化
  • git工程多个remote 拉取推送
  • 配置VScode内置Emmet自动补全代码
  • leetcode 415.字符串相加
  • 如何重塑企业服务体验?
  • 六边形架构模式深度解析
  • 深度学习(1):pytorch
  • SurgRIPE 挑战赛:手术机器人器械位姿估计基准测试|文献速递-医学影像算法文献分享
  • Next.js 样式:CSS 模块、Sass 等
  • 前端技术架构设计文档(Vue2+Antd+Sass)
  • 安全合规2--网络安全等级保护2.0介绍
  • A Logical Calculus of the Ideas Immanent in Nervous Activity(神经网络早期的M-P模型)
  • Spring Boot整合PyTorch Pruning工具链,模型瘦身手术
  • 记录一次Inspur服务器raid配置流程
  • 【数据库】如何从本地电脑连接服务器上的MySQL数据库?
  • 某梆企业壳frida检测绕过
  • 网页前端CSS实现表格3行平均分配高度,或者用div Flexbox布局
  • Springboot2+vue2+uniapp 实现搜索联想自动补全功能
  • vue2.如何给一个页面设置动态的name。不同路由使用一样的组件。页面不刷新怎么办?
  • 小米前端笔试和面试
  • Redis 分布式Session
  • 内存杀手机器:TensorFlow Lite + Spring Boot移动端模型服务深度优化方案
  • 前端三大核心要素以及前后端通讯
  • 机器学习之随机森林(Random Forest)实战案例
  • 数据结构第8问:什么是树?