Android随笔-View绘制流程

一、整体绘制流程

阶段 触发方式 作用
触发绘制 requestLayout() / invalidate() / postInvalidate() 标记 View 需要重绘,加入 Choreographer 队列
VSync 信号 Choreographer.doFrame() 16.6ms (60fps) 同步信号,避免画面撕裂
ViewRootImpl performTraversals() 调度 measure → layout → draw 三大流程
SurfaceFlinger 合成 Layer 送显到屏幕,用户看到画面

二、Measure 测量阶段

流程

ViewRootImpl.performMeasure() → decorView.measure() → 遍历子 View measure() → onMeasure()

MeasureSpec

32位 int 值,高 2 位是 mode,低 30 位是 size:

Mode 含义 场景
EXACTLY 0 精确值 match_parent / 具体数值
AT_MOST 1 最大值 wrap_content
UNSPECIFIED 2 无限制 ScrollView 内部

核心代码

java 复制代码
// View.measure()
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    // 1. 缓存优化
    if (cacheIndex >= 0 && ...) {
        // 使用缓存
        return;
    }
    
    // 2. 强制重新测量
    if (forceLayout || ...) {
        mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    }
    
    // 3. 执行 onMeasure
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    
    // 4. 存储测量结果
    mMeasuredWidth = ...;
    mMeasuredHeight = ...;
}

// ViewGroup.onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 默认调用 measureChildren
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    
    // 或自定义测量逻辑
    int width = 0;
    int height = 0;
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
        width += child.getMeasuredWidth();
        height = Math.max(height, child.getMeasuredHeight());
    }
    setMeasuredDimension(width, height);
}

注意点

✅ 正确做法 ❌ 常见错误
getMeasuredWidth() 获取测量值 onMeasuregetWidth()(此时 layout 未完成)
子 View measure() 后调用 忽略 MeasureSpec mode
setMeasuredDimension() 设置结果 测量后未调用 setMeasuredDimension
MeasureSpec 解析父约束 子 View 未 measure 就获取尺寸

三、Layout 布局阶段

流程

ViewRootImpl.performLayout() → decorView.layout() → 遍历子 View layout() → onLayout()

参数

layout(l, t, r, b):left, top, right, bottom 四个边界坐标

  • getWidth() = r - l
  • getHeight() = b - t

核心代码

java 复制代码
// View.layout()
public void layout(int l, int t, int r, int b) {
    // 1. 判断是否需要重新布局
    boolean changed = isLayoutModeOptical(...) 
        ? setOpticalFrame(l, t, r, b) 
        : setFrame(l, t, r, b);
    
    // 2. 如果位置变化或强制布局
    if (changed || ...) {
        // 3. 调用 onLayout
        onLayout(changed, l, t, r, b);
        
        // 4. 标记布局完成
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    }
}

// ViewGroup.onLayout() (抽象,子类必须实现)
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // 遍历子 View 设置位置
    int childLeft = getPaddingLeft();
    int childTop = getPaddingTop();
    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        int cw = child.getMeasuredWidth();
        int ch = child.getMeasuredHeight();
        child.layout(childLeft, childTop, childLeft + cw, childTop + ch);
        childLeft += cw + mHorizontalSpacing;
    }
}

注意点

✅ 正确做法 ❌ 常见错误
getWidth()/getHeight() 获取布局后尺寸 onMeasure 中获取 width/height
onLayout 中设置子 View 位置 layout 参数计算错误
使用 getMeasuredWidth() 作为参考 忘记处理 padding
考虑 padding/margin 子 View 未 measure 就 layout

四、Draw 绘制阶段

流程

ViewRootImpl.performDraw() → draw() → onDraw() → dispatchDraw() → 遍历子View draw()

Draw 六步曲

java 复制代码
// View.draw()
public void draw(Canvas canvas) {
    // Step 1: 绘制背景
    drawBackground(canvas);
    
    // Step 2: 保存画布层数
    final int saveCount = canvas.getSaveCount();
    
    // Step 3: 绘制内容 (子类实现)
    onDraw(canvas);
    
    // Step 4: 绘制子 View
    dispatchDraw(canvas);
    
    // Step 5: 绘制前景/装饰
    onDrawForeground(canvas);
    
    // Step 6: 恢复画布
    canvas.restoreToCount(saveCount);
}

ViewGroup.dispatchDraw()

java 复制代码
@Override
protected void dispatchDraw(Canvas canvas) {
    // 使用 for 循环或缓存
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < childrenCount; i++) {
        View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || ...) {
            drawChild(canvas, child, drawingTime);
        }
    }
}

// 子 View 绘制
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

注意点

✅ 优化建议 ❌ 常见错误
避免在 onDraw 创建对象(会触发 GC,掉帧) onDrawnew Paint()/new Path()
使用 Path/Rect 缓存 频繁调用 invalidate()
使用 Canvas.clipRect 裁剪 在子线程调用 invalidate
复杂绘制使用 SurfaceView/TextureView 过度绘制 (Overdraw)
开启硬件加速

五、关键要点总结

measure 确定尺寸 → layout 确定位置 → draw 绘制到 Canvas → SurfaceFlinger 合成送显

阶段 核心方法 输出结果 注意事项
Measure onMeasure() mMeasuredWidth/Height getMeasuredWidth(),必须 setMeasuredDimension()
Layout onLayout() mLeft/mTop/mRight/mBottom getWidth()/getHeight(),考虑 padding
Draw onDraw() 像素渲染到 Canvas 避免创建对象,使用缓存

六、自定义 View 完整示例

java 复制代码
public class CustomView extends View {
    private Paint paint;
    private Rect rect;

    public CustomView(Context context) {
        super(context);
        init();
    }

    private void init() {
        // 在构造函数初始化,不在 onDraw 中创建
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.RED);
        rect = new Rect();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 解析 MeasureSpec
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        
        int width = widthMode == MeasureSpec.EXACTLY ? widthSize : 200;
        int height = MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY 
            ? MeasureSpec.getSize(heightMeasureSpec) : 200;
        
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        // 设置绘制区域
        rect.set(0, 0, getWidth(), getHeight());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 使用缓存的 Paint 和 Rect
        canvas.drawRect(rect, paint);
    }
}
相关推荐
2501_916008891 小时前
iOS 证书管理最佳实践 从创建到续期的完整指南
android·ios·小程序·https·uni-app·iphone·webview
敖行客 Allthinker1 小时前
IM 融合专题:后端架构师的核心修炼
java·开发语言·数据库
进击的程序猿~2 小时前
Go Slice源码深度解析指南
开发语言·后端·golang
Hesionberger2 小时前
快速求解完全平方数的最少数量
开发语言·数据结构·python·算法·leetcode·c#
私人珍藏库10 小时前
[Android] PeakFinder AR v4.8.89 (山峰全景识别+增强现实山峰查看器)
android·人工智能·智能手机·ar·工具·软件
researcher-Jiang11 小时前
高性能计算之OpenMP——超算习堂学习1
android·java·学习
alexhilton12 小时前
Kotlin DSL深度解析:从Gradle脚本到构建你自己的DSL
android·kotlin·android jetpack
烽火聊员12 小时前
查看Android Studio错误日志
android·ide·android studio
西门吹-禅12 小时前
java springboot N+1问题
java·开发语言·spring boot