一句话说透Android里面的View测量、布局以及绘制原理

111 阅读3分钟

Android View三剑客原理大白话解析

用装修房子类比理解:

测量(Measure)→ 量房间尺寸  
布局(Layout)→ 摆家具位置  
绘制(Draw)→ 刷墙贴装饰  

一、测量原理(卷尺测量阶段)

1. 父View的约束条件

// 测量规格 = 模式 + 尺寸  
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(   
    parentWidth,   
    MeasureSpec.EXACTLY // 模式:精确值/最大值/未限制  
);  

三种模式解释

  • EXACTLY:爹说了算(如match_parent或具体数值)
  • AT_MOST:最多这么大(如wrap_content)
  • UNSPECIFIED:随便你(ScrollView等可滚动布局)

2. 自定义View测量要点

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {  
    // 步骤1:计算自己想要的尺寸  
    val desiredWidth = calculateWidth()  
    val desiredHeight = calculateHeight()  
 
    // 步骤2:遵守父View的限制  
    val finalWidth = resolveSize(desiredWidth, widthMeasureSpec)  
    val finalHeight = resolveSize(desiredHeight, heightMeasureSpec)  
 
    // 步骤3:保存测量结果  
    setMeasuredDimension(finalWidth, finalHeight)  
}  

避坑指南

  • 忘记调用setMeasuredDimension会抛异常
  • wrap_content不处理会填满父容器

二、布局原理(家具摆放阶段)

1. 坐标确定流程

父View调用子View.layout(l,  t, r, b)  
    ↓  
子View的mLeft/mTop/mRight/mBottom被赋值  
    ↓  
触发onLayout()(如果是ViewGroup需遍历布局子View)  

2. 常用布局示例

LinearLayout垂直排列源码逻辑

void layoutVertical(int left, int top, int right, int bottom) {  
    int childTop = mPaddingTop;  
    for (View child : getChildren()) {  
        int childHeight = child.getMeasuredHeight();   
        // 设置子View位置  
        child.layout(left,  childTop,   
                    right, childTop + childHeight);  
        childTop += childHeight + dividerHeight;  
    }  
}  

三、绘制原理(装修粉刷阶段)

1. 绘制顺序层级图

1. 绘制背景 → drawBackground(canvas)  
2. 绘制自己 → onDraw(canvas)  
3. 绘制子View → dispatchDraw(canvas)  
4. 绘制装饰(滚动条等) → onDrawForeground(canvas)  

2. 优化绘制性能技巧

// 正确做法:避免在onDraw中创建对象  
val paint = Paint().apply {  
    color = Color.RED  
    isAntiAlias = true  
}  
 
override fun onDraw(canvas: Canvas) {  
    // ✅ 复用预定义对象  
    canvas.drawCircle(x,  y, radius, paint)  
 
    // ❌ 禁止在绘制时new对象!  
    // val tempPaint = Paint()  
}  

四、全流程协作机制

1. 从根View开始的遍历

ViewRootImpl.performTraversals()   
    ↓ 触发  
measure() → onMeasure()  
    ↓  
layout() → onLayout()  
    ↓  
draw() → onDraw()  

2. 触发更新的两种方式

方法作用范围性能消耗
invalidate()只重绘当前区域较低
requestLayout()重新测量+布局+绘制较高

五、高频面试题破解

Q1:为什么自定义View wrap_content失效?

原因分析

// 错误实现:直接使用MeasureSpec的尺寸  
setMeasuredDimension(  
    MeasureSpec.getSize(widthMeasureSpec),   
    MeasureSpec.getSize(heightMeasureSpec)   
)  

正确方案

// 当模式是AT_MOST时使用计算值  
val width = when(MeasureSpec.getMode(widthMeasureSpec))  {  
    MeasureSpec.AT_MOST -> min(desiredWidth, MeasureSpec.getSize(...))   
    else -> MeasureSpec.getSize(...)   
}  

Q2:View的绘制流程会多次执行吗?

绘制触发条件

  • 新View添加到视图树
  • View调用invalidate()
  • 动画执行期间
  • 屏幕区域失效(如被遮挡后重新显示)

六、性能优化黄金法则

  1. 减少层级嵌套 → 用ConstraintLayout替代多层LinearLayout
  2. 避免过度绘制 → 开启开发者选项中的"显示过度绘制区域"
  3. 善用include/merge → 复用布局文件
  4. ViewStub延迟加载 → 耗时布局按需加载

View三流程终极口诀:
测量就像量尺寸,父给限制子遵守
布局定位摆位置,上下左右算清楚
绘制如同刷油漆,先底后面再装饰
requestLayout全量走,invalidate只重绘
层级优化是王道,性能体验两手抓!