Android View 绘制机制完整详解

17 阅读39分钟

Android View 绘制机制完整详解


View绘制机制概述

什么是View绘制机制?

View绘制机制是Android系统将View从XML布局文件或代码中创建,经过测量(Measure)、布局(Layout)、绘制(Draw)三个阶段,最终显示在屏幕上的完整流程。

为什么需要View绘制机制?

  1. 确定View的尺寸:通过测量阶段确定View需要多大的空间
  2. 确定View的位置:通过布局阶段确定View在父容器中的位置
  3. 绘制View的内容:通过绘制阶段将View的内容绘制到屏幕上
  4. 支持动态更新:当View内容变化时,可以重新绘制

View绘制的三个阶段

View绘制流程:
    ↓
【1. Measure(测量)】- 回答"多大?"
    ↓ 确定View的宽高
    ↓ 调用onMeasure()
    ↓ 必须调用setMeasuredDimension()
【2. Layout(布局)】- 回答"在哪?"
    ↓ 确定View的位置
    ↓ 调用onLayout()
    ↓ 设置left, top, right, bottom3. Draw(绘制)】- 回答"画什么?"
    ↓ 绘制View的内容
    ↓ 调用onDraw()
    ↓ 使用Canvas绘制
【显示在屏幕上】✅

记忆口诀: "多大在哪画什么" - Measure确定多大,Layout确定在哪,Draw画什么内容

View绘制的设计思想

  • 责任链模式:从根View开始,逐层向下传递测量和布局信息
  • 自顶向下测量,自底向上布局:先测量子View,再确定父View的尺寸
  • 缓存机制:避免重复测量和绘制,提高性能

View绘制的三个核心方法

方法列表

方法作用调用时机返回值
onMeasure()测量View的宽高measure阶段void(通过setMeasuredDimension设置)
onLayout()布局View的位置layout阶段void
onDraw()绘制View的内容draw阶段void

方法调用顺序

【完整调用链】
ViewRootImpl.performTraversals()
    ↓
┌─────────────────────────────────────┐
│ 【1. Measure阶段】- 测量尺寸        │
└─────────────────────────────────────┘
    ↓
View.measure(widthMeasureSpec, heightMeasureSpec)
    ↓
View.onMeasure(widthMeasureSpec, heightMeasureSpec)
    ↓ 必须调用 setMeasuredDimension(width, height)
    ↓
┌─────────────────────────────────────┐
│ 【2. Layout阶段】- 布局位置         │
└─────────────────────────────────────┘
    ↓
View.layout(left, top, right, bottom)
    ↓
View.onLayout(changed, l, t, r, b)
    ↓ 设置View的四个边界
    ↓
┌─────────────────────────────────────┐
│ 【3. Draw阶段】- 绘制内容          │
└─────────────────────────────────────┘
    ↓
View.draw(canvas)
    ↓
View.onDraw(canvas)
    ↓ 使用Canvas绘制内容
    ↓
【显示在屏幕上】✅

关键记忆点:

  • Measure:先测量,确定尺寸
  • Layout:再布局,确定位置
  • Draw:最后绘制,显示内容
  • 顺序固定:Measure → Layout → Draw,不能颠倒

三个方法的关系

  1. onMeasure():必须先执行,确定View的尺寸
  2. onLayout():在onMeasure()之后执行,确定View的位置
  3. onDraw():在onLayout()之后执行,绘制View的内容

View绘制流程详解

完整绘制流程

1. 触发绘制】
    ↓
ViewRootImpl.performTraversals()
    ↓
【2. Measure阶段】
    ↓
performMeasure() → measure() → onMeasure()
    ↓
【3. Layout阶段】
    ↓
performLayout() → layout() → onLayout()
    ↓
【4. Draw阶段】
    ↓
performDraw() → draw() → onDraw()
    ↓
【5. 显示在屏幕上】

Measure阶段详解

流程:

ViewRootImpl.performMeasure()
    ↓
DecorView.measure()
    ↓
ViewGroup.measure()
    ↓
ViewGroup.onMeasure()
    ↓
遍历子View,调用子View.measure()
    ↓
子View.onMeasure()
    ↓
setMeasuredDimension()设置测量尺寸

关键点:

  • 自顶向下测量:从根View开始,逐层向下
  • 父View先测量子View,再根据子View确定自己的尺寸
  • 通过MeasureSpec传递测量约束

Layout阶段详解

流程:

ViewRootImpl.performLayout()
    ↓
DecorView.layout()
    ↓
ViewGroup.layout()
    ↓
ViewGroup.onLayout()
    ↓
遍历子View,调用子View.layout()
    ↓
子View.onLayout()
    ↓
确定View的四个边界(left, top, right, bottom

关键点:

  • 自顶向下布局:从根View开始,逐层向下
  • 父View确定子View的位置
  • 通过layout()方法设置View的四个边界

Draw阶段详解

流程:

ViewRootImpl.performDraw()
    ↓
View.draw()
    ↓
【绘制背景】
drawBackground()
    ↓
【绘制内容】
onDraw()
    ↓
【绘制子View】
dispatchDraw()
    ↓
【绘制前景、滚动条等】
onDrawForeground()

关键点:

  • 自顶向下绘制:从根View开始,逐层向下
  • 先绘制背景,再绘制内容,最后绘制前景
  • ViewGroup需要绘制子View(dispatchDraw)

MeasureSpec详解

什么是MeasureSpec?

MeasureSpec是View测量时的约束条件,包含两个信息:

  1. 测量模式(Mode):如何测量
  2. 测量尺寸(Size):测量的大小

MeasureSpec的组成

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;  // 未指定
    public static final int EXACTLY     = 1 << MODE_SHIFT;  // 精确值
    public static final int AT_MOST     = 2 << MODE_SHIFT;   // 最大不超过
    
    // 高2位表示Mode,低30位表示Size
    public static int makeMeasureSpec(int size, int mode) {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
    
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

三种测量模式

1. EXACTLY(精确模式)✅

含义: View的尺寸已经确定,必须使用指定的尺寸

记忆口诀: "精确值,必须用"

使用场景:

  • 指定了具体数值:android:layout_width="100dp"
  • 指定了match_parent:父View已经确定了尺寸

代码示例:

int widthSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
// View的宽度必须是100,不能改变

处理方式:

if (widthMode == MeasureSpec.EXACTLY) {
    width = widthSize;  // 直接使用,不能改变
}
2. AT_MOST(最大模式)⚠️

含义: View的尺寸不能超过指定的尺寸,但可以小于

记忆口诀: "最大不超过,可以更小"

使用场景:

  • 指定了wrap_content:View可以根据内容确定尺寸,但不能超过父View

代码示例:

int widthSpec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.AT_MOST);
// View的宽度不能超过200,但可以小于200(如150、100等)

处理方式:

if (widthMode == MeasureSpec.AT_MOST) {
    width = Math.min(calculateWidth(), widthSize);  // 取较小值
}
3. UNSPECIFIED(未指定模式)🔓

含义: View的尺寸没有限制,可以任意大小

记忆口诀: "无限制,任意大"

使用场景:

  • ScrollView中的子View:可以超出屏幕
  • 自定义View的特殊情况

代码示例:

int widthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
// View的宽度没有限制,可以任意大小

处理方式:

if (widthMode == MeasureSpec.UNSPECIFIED) {
    width = calculateWidth();  // 使用计算值,没有限制
}

📊 三种模式对比表

模式含义使用场景处理方式记忆口诀
EXACTLY精确值,必须用具体数值、match_parentwidth = widthSize"精确值,必须用"
AT_MOST最大不超过,可以更小wrap_contentwidth = Math.min(计算值, widthSize)"最大不超过,可以更小"
UNSPECIFIED无限制,任意大ScrollView子Viewwidth = 计算值"无限制,任意大"

MeasureSpec的传递

View的MeasureSpec
    ↓
根据子View的layout_width/layout_height
    ↓
生成子View的MeasureSpec
    ↓
传递给子View.measure()

总结与记忆表格

子View的 LayoutParams父View的 MeasureSpec生成的子View MeasureSpec通俗解释
match_parentEXACTLY (父有精确尺寸)EXACTLY (子也有精确尺寸)“父子同心,一样大。”
match_parentAT_MOST (父有上限)AT_MOST (子也有上限)“爸爸自己也在受限,你跟着我受限。”
wrap_contentEXACTLY (父有精确尺寸)AT_MOST (子不能超过父)“你想多小都行,但别超过我。”
wrap-contentAT_MOST (父有上限)AT_MOST (子不能超过父)“咱爷俩都有上限,都别超。”
具体数值 (e.g., 100dp)任意模式EXACTLY (子有精确尺寸)“你就按我说的这个尺寸来,别管我。”

核心思想

  1. 约束是向下传递的。父View从它的父View那里获得约束,再根据自己的规则“翻译”后传递给子View。
  2. match_parent 的子View,其约束取决于父View的状态
  3. wrap_content 的子View,总是收到一个有上限(AT_MOST)的约束
  4. 具体数值 是最强的“要求”,会直接转换为精确约束。

onMeasure()详解

onMeasure()的作用

onMeasure()用于测量View的宽高,必须在此方法中调用setMeasuredDimension()设置测量结果。

onMeasure()的调用时机

  1. View首次添加到View树时
  2. 调用requestLayout()
  3. 父View的尺寸发生变化时
  4. 子View的尺寸发生变化时

onMeasure()的实现

View的onMeasure()实现
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
    );
}

protected 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;  // 使用MeasureSpec指定的尺寸
            break;
    }
    return result;
}
自定义View的onMeasure()实现
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
    int width = 0;
    int height = 0;
    
    // 根据测量模式计算宽度
    switch (widthMode) {
        case MeasureSpec.EXACTLY:
            width = widthSize;  // 精确值,直接使用
            break;
        case MeasureSpec.AT_MOST:
            width = Math.min(calculateWidth(), widthSize);  // 不能超过指定值
            break;
        case MeasureSpec.UNSPECIFIED:
            width = calculateWidth();  // 没有限制,使用计算值
            break;
    }
    
    // 根据测量模式计算高度
    switch (heightMode) {
        case MeasureSpec.EXACTLY:
            height = heightSize;
            break;
        case MeasureSpec.AT_MOST:
            height = Math.min(calculateHeight(), heightSize);
            break;
        case MeasureSpec.UNSPECIFIED:
            height = calculateHeight();
            break;
    }
    
    // 必须调用setMeasuredDimension设置测量结果
    setMeasuredDimension(width, height);
}

private int calculateWidth() {
    // 根据View的内容计算宽度
    return 200;  // 示例
}

private int calculateHeight() {
    // 根据View的内容计算高度
    return 200;  // 示例
}

onMeasure()的注意事项

  1. 必须调用setMeasuredDimension():否则会抛出异常
  2. 测量结果应该考虑paddingwidth = contentWidth + paddingLeft + paddingRight
  3. 不要修改MeasureSpec:只能读取,不能修改
  4. 避免无限递归:不要在onMeasure()中调用requestLayout()

onLayout()详解

onLayout()的作用

onLayout()用于布局View的位置,确定View在父容器中的位置(left, top, right, bottom)。

onLayout()的调用时机

  1. View首次添加到View树时
  2. 调用requestLayout()
  3. 父View的尺寸或位置发生变化时

onLayout()的实现

View的onLayout()实现
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    // View的onLayout()是空实现
    // ViewGroup需要重写此方法来布局子View
}
ViewGroup的onLayout()实现
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // 遍历所有子View,调用子View的layout()方法
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            // 计算子View的位置
            int childLeft = calculateChildLeft(i);
            int childTop = calculateChildTop(i);
            int childRight = childLeft + child.getMeasuredWidth();
            int childBottom = childTop + child.getMeasuredHeight();
            
            // 布局子View
            child.layout(childLeft, childTop, childRight, childBottom);
        }
    }
}

layout()方法

public void layout(int l, int t, int r, int b) {
    // 设置View的四个边界
    setFrame(l, t, r, b);
    
    // 调用onLayout()
    onLayout(changed, l, t, r, b);
}

onLayout()的注意事项

  1. 使用getMeasuredWidth()和getMeasuredHeight():不要使用getWidth()和getHeight()
  2. 考虑padding:子View的位置应该考虑父View的padding
  3. 处理GONE的View:不需要布局GONE的View
  4. 避免无限递归:不要在onLayout()中调用requestLayout()

onDraw()详解

onDraw()的作用

onDraw()用于绘制View的内容,通过Canvas绘制各种图形、文字、图片等。

onDraw()的调用时机

  1. View首次显示时
  2. 调用invalidate()
  3. View的内容发生变化时
  4. 动画执行时

onDraw()的实现

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    // 绘制背景(系统自动绘制)
    // drawBackground(canvas);
    
    // 绘制内容
    canvas.drawColor(Color.WHITE);  // 绘制背景色
    canvas.drawCircle(100, 100, 50, paint);  // 绘制圆形
    canvas.drawText("Hello", 50, 50, paint);  // 绘制文字
    
    // 绘制子View(ViewGroup需要)
    // dispatchDraw(canvas);
    
    // 绘制前景、滚动条等(系统自动绘制)
    // onDrawForeground(canvas);
}

View.draw()的完整流程

public void draw(Canvas canvas) {
    // 1. 绘制背景
    drawBackground(canvas);
    
    // 2. 保存图层(如果需要)
    if (!dirtyOpaque) {
        drawShadow(canvas);
    }
    
    // 3. 绘制内容
    onDraw(canvas);
    
    // 4. 绘制子View(ViewGroup)
    dispatchDraw(canvas);
    
    // 5. 绘制前景、滚动条等
    onDrawForeground(canvas);
}

onDraw()的注意事项

  1. 不要创建新对象:避免在onDraw()中创建Paint、Path等对象
  2. 使用缓存:复用Paint、Path等对象
  3. 避免过度绘制:不要绘制不必要的区域
  4. 使用硬件加速:利用GPU加速绘制

View绘制相关方法

invalidate()和requestLayout()

invalidate()

作用: 标记View需要重绘,触发onDraw()

使用场景:

  • View的内容发生变化
  • 需要更新显示

特点:

  • 只触发draw阶段
  • 不触发measure和layout阶段
  • 会向上传播到ViewRootImpl

代码示例:

// 修改View的内容后,调用invalidate()重绘
textView.setText("新文本");
textView.invalidate();  // 触发onDraw()
requestLayout()

作用: 请求重新布局,触发measure和layout阶段

使用场景:

  • View的尺寸发生变化
  • View的位置需要调整

特点:

  • 触发measure和layout阶段
  • 也会触发draw阶段
  • 会向上传播到ViewRootImpl

代码示例:

// 修改View的尺寸后,调用requestLayout()重新布局
view.setLayoutParams(new LayoutParams(200, 200));
view.requestLayout();  // 触发measure()和layout()
区别对比
方法触发阶段使用场景性能影响
invalidate()只触发draw内容变化较小
requestLayout()触发measure、layout、draw尺寸/位置变化较大

getWidth()和getMeasuredWidth()

getWidth()

作用: 获取View的实际宽度(布局后的宽度)

计算方式: getWidth() = right - left

特点:

  • 在layout阶段之后才有值
  • 表示View在屏幕上的实际宽度
getMeasuredWidth()

作用: 获取View的测量宽度(测量后的宽度)

计算方式: getMeasuredWidth() = measuredWidth

特点:

  • 在measure阶段之后就有值
  • 表示View测量后的宽度
区别对比
方法获取时机值来源使用场景
getWidth()layout之后right - left布局时使用
getMeasuredWidth()measure之后measuredWidth测量时使用

注意: 在onLayout()中应该使用getMeasuredWidth(),而不是getWidth()


自定义View绘制

自定义View的步骤

  1. 继承View或ViewGroup
  2. 重写onMeasure():测量View的尺寸
  3. 重写onLayout()(ViewGroup需要):布局子View
  4. 重写onDraw():绘制View的内容
  5. 处理触摸事件(可选):重写onTouchEvent()

自定义View示例

public class CustomView extends View {
    private Paint paint;
    
    public CustomView(Context context) {
        super(context);
        init();
    }
    
    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    
    private void init() {
        // 初始化Paint,避免在onDraw()中创建
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.FILL);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        
        int width = 200;  // 默认宽度
        int height = 200;  // 默认高度
        
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(width, widthSize);
        }
        
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(height, heightSize);
        }
        
        setMeasuredDimension(width, height);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        // 绘制圆形
        int centerX = getWidth() / 2;
        int centerY = getHeight() / 2;
        int radius = Math.min(getWidth(), getHeight()) / 2 - 20;
        
        canvas.drawCircle(centerX, centerY, radius, paint);
    }
}

自定义View的注意事项

  1. 不要在onDraw()中创建对象:应该提前创建并复用
  2. 正确处理padding:在onMeasure()和onDraw()中考虑padding
  3. 支持wrap_content:在onMeasure()中处理AT_MOST模式
  4. 避免过度绘制:只绘制必要的区域
  5. 使用硬件加速:在AndroidManifest.xml中启用

自定义ViewGroup绘制

自定义ViewGroup的步骤

  1. 继承ViewGroup
  2. 重写onMeasure():测量ViewGroup和子View的尺寸
  3. 重写onLayout():布局子View的位置
  4. 处理触摸事件(可选):重写onInterceptTouchEvent()和onTouchEvent()

自定义ViewGroup示例

public class CustomViewGroup extends ViewGroup {
    public CustomViewGroup(Context context) {
        super(context);
    }
    
    public CustomViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 1. 测量所有子View
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                // 为子View生成MeasureSpec
                int childWidthSpec = getChildMeasureSpec(
                    widthMeasureSpec,
                    getPaddingLeft() + getPaddingRight(),
                    child.getLayoutParams().width
                );
                int childHeightSpec = getChildMeasureSpec(
                    heightMeasureSpec,
                    getPaddingTop() + getPaddingBottom(),
                    child.getLayoutParams().height
                );
                
                // 测量子View
                child.measure(childWidthSpec, childHeightSpec);
            }
        }
        
        // 2. 根据子View确定自己的尺寸
        int width = 0;
        int height = 0;
        
        // 计算所有子View的最大宽度和高度
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                width = Math.max(width, child.getMeasuredWidth());
                height += child.getMeasuredHeight();
            }
        }
        
        // 考虑padding
        width += getPaddingLeft() + getPaddingRight();
        height += getPaddingTop() + getPaddingBottom();
        
        // 3. 根据MeasureSpec调整尺寸
        width = resolveSize(width, widthMeasureSpec);
        height = resolveSize(height, heightMeasureSpec);
        
        setMeasuredDimension(width, height);
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int currentTop = getPaddingTop();
        
        // 布局所有子View
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                int childLeft = getPaddingLeft();
                int childTop = currentTop;
                int childRight = childLeft + child.getMeasuredWidth();
                int childBottom = childTop + child.getMeasuredHeight();
                
                child.layout(childLeft, childTop, childRight, childBottom);
                
                currentTop = childBottom;
            }
        }
    }
}

自定义ViewGroup的注意事项

  1. 先测量子View,再确定自己的尺寸:在onMeasure()中先测量所有子View
  2. 使用getChildMeasureSpec():为子View生成正确的MeasureSpec
  3. 考虑padding和margin:在测量和布局时都要考虑
  4. 处理GONE的View:不需要测量和布局GONE的View

Canvas绘制详解

Canvas的绘制操作

1. 绘制基本图形
// 绘制点
canvas.drawPoint(x, y, paint);

// 绘制线
canvas.drawLine(startX, startY, endX, endY, paint);

// 绘制矩形
canvas.drawRect(left, top, right, bottom, paint);

// 绘制圆角矩形
canvas.drawRoundRect(left, top, right, bottom, rx, ry, paint);

// 绘制圆形
canvas.drawCircle(centerX, centerY, radius, paint);

// 绘制椭圆
canvas.drawOval(left, top, right, bottom, paint);

// 绘制弧形
canvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint);
2. 绘制文字
// 绘制文字
canvas.drawText(text, x, y, paint);

// 绘制多行文字
canvas.drawText(text, start, end, x, y, paint);

// 沿路径绘制文字
canvas.drawTextOnPath(text, path, hOffset, vOffset, paint);
3. 绘制图片
// 绘制Bitmap
canvas.drawBitmap(bitmap, left, top, paint);

// 绘制Bitmap(指定区域)
canvas.drawBitmap(bitmap, src, dst, paint);

// 绘制Drawable
drawable.draw(canvas);
4. 绘制路径
Path path = new Path();
path.moveTo(0, 0);
path.lineTo(100, 100);
path.quadTo(150, 50, 200, 100);
path.close();

canvas.drawPath(path, paint);

Paint的使用

Paint paint = new Paint();
paint.setAntiAlias(true);  // 抗锯齿
paint.setColor(Color.BLACK);  // 颜色
paint.setStyle(Paint.Style.FILL);  // 填充模式
paint.setStrokeWidth(5);  // 线条宽度
paint.setTextSize(30);  // 文字大小
paint.setTypeface(Typeface.DEFAULT_BOLD);  // 字体

Canvas的变换操作

// 平移
canvas.translate(dx, dy);

// 旋转
canvas.rotate(degrees);
canvas.rotate(degrees, px, py);

// 缩放
canvas.scale(sx, sy);
canvas.scale(sx, sy, px, py);

// 错切
canvas.skew(sx, sy);

// 保存和恢复
canvas.save();
// 进行变换
canvas.restore();

绘制优化

过度绘制(Overdraw)

什么是过度绘制?

过度绘制是指同一个像素被绘制了多次,造成性能浪费。

如何检测过度绘制?
  1. 开启开发者选项中的"显示过度绘制"
  2. 颜色含义:
    • 蓝色:绘制1次(正常)
    • 绿色:绘制2次(可接受)
    • 浅红色:绘制3次(需要优化)
    • 深红色:绘制4次以上(必须优化)
如何减少过度绘制?
  1. 移除不必要的背景
// 如果View的背景和父View相同,可以移除
view.setBackground(null);
  1. 使用clipRect()裁剪绘制区域
canvas.clipRect(left, top, right, bottom);
canvas.drawBitmap(bitmap, x, y, paint);
  1. 使用ViewStub延迟加载
<ViewStub
    android:id="@+id/view_stub"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout="@layout/heavy_layout" />
  1. 使用merge标签减少布局层级
<merge>
    <TextView ... />
    <ImageView ... />
</merge>

绘制性能优化

1. 避免在onDraw()中创建对象
// ❌ 错误:在onDraw()中创建对象
@Override
protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();  // 每次绘制都创建新对象
    canvas.drawCircle(100, 100, 50, paint);
}

// ✅ 正确:提前创建并复用
private Paint paint = new Paint();

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawCircle(100, 100, 50, paint);  // 复用对象
}
2. 使用硬件加速
<!-- AndroidManifest.xml -->
<application
    android:hardwareAccelerated="true">
    ...
</application>
3. 使用缓存
// 使用Bitmap缓存绘制结果
private Bitmap cacheBitmap;
private Canvas cacheCanvas;

private void initCache() {
    cacheBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
    cacheCanvas = new Canvas(cacheBitmap);
}

@Override
protected void onDraw(Canvas canvas) {
    if (cacheBitmap != null) {
        // 先绘制到缓存
        drawToCache(cacheCanvas);
        // 再绘制到屏幕
        canvas.drawBitmap(cacheBitmap, 0, 0, null);
    }
}
4. 减少绘制区域
// 只绘制变化的区域
@Override
protected void onDraw(Canvas canvas) {
    canvas.clipRect(dirtyRect);  // 裁剪到脏区域
    // 绘制内容
}
5. 使用invalidate()的带参数版本
// 只重绘指定区域
invalidate(left, top, right, bottom);

常见问题与解决方案

1. View不显示

原因:

  • onMeasure()没有调用setMeasuredDimension()
  • View的尺寸为0
  • View被其他View遮挡

解决方案:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 确保调用了setMeasuredDimension()
    setMeasuredDimension(width, height);
}

2. wrap_content不生效

原因:

  • onMeasure()中没有处理AT_MOST模式

解决方案:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    
    int width = 0;
    if (widthMode == MeasureSpec.AT_MOST) {
        width = calculateContentWidth();  // 计算内容宽度
        width = Math.min(width, widthSize);  // 不能超过父View
    } else if (widthMode == MeasureSpec.EXACTLY) {
        width = widthSize;
    }
    
    setMeasuredDimension(width, height);
}

3. padding不生效

原因:

  • onMeasure()和onDraw()中没有考虑padding

解决方案:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = contentWidth + getPaddingLeft() + getPaddingRight();
    int height = contentHeight + getPaddingTop() + getPaddingBottom();
    setMeasuredDimension(width, height);
}

@Override
protected void onDraw(Canvas canvas) {
    int left = getPaddingLeft();
    int top = getPaddingTop();
    int right = getWidth() - getPaddingRight();
    int bottom = getHeight() - getPaddingBottom();
    // 在考虑padding的区域内绘制
}

4. 内存泄漏

原因:

  • Paint、Path等对象持有Context引用
  • Bitmap没有及时回收

解决方案:

// 使用Application Context
private Context appContext;

public CustomView(Context context) {
    super(context);
    appContext = context.getApplicationContext();
}

// 及时回收Bitmap
if (bitmap != null && !bitmap.isRecycled()) {
    bitmap.recycle();
    bitmap = null;
}

快速查询表

MeasureSpec快速查询

layout_width/layout_height父View的Mode子View的Mode子View的Size
100dp (具体数值)任意EXACTLY100dp
match_parentEXACTLYEXACTLY父View的Size
match_parentAT_MOSTAT_MOST父View的Size
wrap_contentEXACTLYAT_MOST父View的Size
wrap_contentAT_MOSTAT_MOST父View的Size

三个方法快速查询

方法作用必须做什么使用什么方法获取尺寸
onMeasure()测量尺寸调用setMeasuredDimension()使用MeasureSpec.getSize()
onLayout()布局位置调用child.layout()使用getMeasuredWidth()
onDraw()绘制内容使用Canvas绘制使用getWidth()getHeight()

常见方法对比

方法获取时机值来源使用场景
getWidth()layout之后right - leftonDraw()中使用
getMeasuredWidth()measure之后measuredWidthonLayout()中使用
getSuggestedMinimumWidth()任何时候背景Drawable或minWidthonMeasure()中计算最小尺寸

记忆口诀

三个阶段的记忆

"多大在哪画什么"

  • Measure(多大):确定View的尺寸
  • Layout(在哪):确定View的位置
  • Draw(画什么):绘制View的内容

三种模式的记忆

"精确最大无限制"

  • EXACTLY(精确):精确值,必须用
  • AT_MOST(最大):最大不超过,可以更小
  • UNSPECIFIED(无限制):无限制,任意大

三个方法的记忆

"测量布局绘制"

  • onMeasure():测量尺寸,必须调用setMeasuredDimension()
  • onLayout():布局位置,设置left/top/right/bottom
  • onDraw():绘制内容,使用Canvas

MeasureSpec传递规则记忆

"具体数值精确,match_parent跟随,wrap_content最大"

  • 具体数值 → EXACTLY
  • match_parent → 跟随父View的Mode
  • wrap_content → AT_MOST

常见错误对比

❌ 错误1:onMeasure()中忘记调用setMeasuredDimension()

// ❌ 错误:忘记调用setMeasuredDimension()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = 200;
    int height = 200;
    // 忘记调用setMeasuredDimension(),会抛出异常!
}

// ✅ 正确:必须调用setMeasuredDimension()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = 200;
    int height = 200;
    setMeasuredDimension(width, height);  // ✅ 必须调用
}

❌ 错误2:wrap_content不生效

// ❌ 错误:没有处理AT_MOST模式
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = 200;  // 固定值,wrap_content不生效
    int height = 200;
    setMeasuredDimension(width, height);
}

// ✅ 正确:处理AT_MOST模式
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int width = 0;
    
    if (widthMode == MeasureSpec.AT_MOST) {
        width = calculateContentWidth();  // 计算内容宽度
        width = Math.min(width, widthSize);  // 不能超过父View
    } else if (widthMode == MeasureSpec.EXACTLY) {
        width = widthSize;
    }
    
    setMeasuredDimension(width, height);
}

❌ 错误3:在onDraw()中创建对象

// ❌ 错误:在onDraw()中创建对象
@Override
protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();  // ❌ 每次绘制都创建新对象
    canvas.drawCircle(100, 100, 50, paint);
}

// ✅ 正确:提前创建并复用
private Paint paint = new Paint();  // ✅ 提前创建

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawCircle(100, 100, 50, paint);  // ✅ 复用对象
}

❌ 错误4:padding不生效

// ❌ 错误:没有考虑padding
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = contentWidth;  // ❌ 没有考虑padding
    int height = contentHeight;
    setMeasuredDimension(width, height);
}

// ✅ 正确:考虑padding
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = contentWidth + getPaddingLeft() + getPaddingRight();  // ✅ 考虑padding
    int height = contentHeight + getPaddingTop() + getPaddingBottom();
    setMeasuredDimension(width, height);
}

❌ 错误5:在onLayout()中使用getWidth()

// ❌ 错误:在onLayout()中使用getWidth()
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childWidth = child.getWidth();  // ❌ getWidth()此时可能为0
    child.layout(0, 0, childWidth, childHeight);
}

// ✅ 正确:使用getMeasuredWidth()
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childWidth = child.getMeasuredWidth();  // ✅ 使用测量宽度
    child.layout(0, 0, childWidth, childHeight);
}

实战案例

案例1:自定义圆形View

public class CircleView extends View {
    private Paint paint;
    private int radius = 50;
    
    public CircleView(Context context) {
        super(context);
        init();
    }
    
    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    
    private void init() {
        // ✅ 提前创建Paint,不在onDraw()中创建
        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.FILL);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        
        int width = 0;
        int height = 0;
        
        // ✅ 处理三种测量模式
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else if (widthMode == MeasureSpec.AT_MOST) {
            // ✅ 支持wrap_content
            width = Math.min(radius * 2 + getPaddingLeft() + getPaddingRight(), widthSize);
        } else {
            width = radius * 2 + getPaddingLeft() + getPaddingRight();
        }
        
        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else if (heightMode == MeasureSpec.AT_MOST) {
            height = Math.min(radius * 2 + getPaddingTop() + getPaddingBottom(), heightSize);
        } else {
            height = radius * 2 + getPaddingTop() + getPaddingBottom();
        }
        
        // ✅ 必须调用setMeasuredDimension()
        setMeasuredDimension(width, height);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        
        // ✅ 考虑padding
        int centerX = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
        int centerY = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
        int actualRadius = Math.min(
            (getWidth() - getPaddingLeft() - getPaddingRight()) / 2,
            (getHeight() - getPaddingTop() - getPaddingBottom()) / 2
        );
        
        // ✅ 复用Paint对象
        canvas.drawCircle(centerX, centerY, actualRadius, paint);
    }
}

案例2:自定义垂直布局ViewGroup

public class VerticalLayout extends ViewGroup {
    public VerticalLayout(Context context) {
        super(context);
    }
    
    public VerticalLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int childCount = getChildCount();
        int totalHeight = 0;
        int maxWidth = 0;
        
        // ✅ 1. 先测量所有子View
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                // ✅ 为子View生成MeasureSpec
                int childWidthSpec = getChildMeasureSpec(
                    widthMeasureSpec,
                    getPaddingLeft() + getPaddingRight(),
                    child.getLayoutParams().width
                );
                int childHeightSpec = getChildMeasureSpec(
                    heightMeasureSpec,
                    getPaddingTop() + getPaddingBottom(),
                    child.getLayoutParams().height
                );
                
                child.measure(childWidthSpec, childHeightSpec);
                
                // 累计高度和最大宽度
                totalHeight += child.getMeasuredHeight();
                maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
            }
        }
        
        // ✅ 2. 根据子View确定自己的尺寸
        int width = maxWidth + getPaddingLeft() + getPaddingRight();
        int height = totalHeight + getPaddingTop() + getPaddingBottom();
        
        // ✅ 3. 根据MeasureSpec调整尺寸
        width = resolveSize(width, widthMeasureSpec);
        height = resolveSize(height, heightMeasureSpec);
        
        setMeasuredDimension(width, height);
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int currentTop = getPaddingTop();
        
        // ✅ 布局所有子View
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                // ✅ 使用getMeasuredWidth(),不是getWidth()
                int childLeft = getPaddingLeft();
                int childTop = currentTop;
                int childRight = childLeft + child.getMeasuredWidth();
                int childBottom = childTop + child.getMeasuredHeight();
                
                child.layout(childLeft, childTop, childRight, childBottom);
                
                currentTop = childBottom;
            }
        }
    }
}

最佳实践

1. 测量阶段

  • ✅ 正确处理三种测量模式(EXACTLY、AT_MOST、UNSPECIFIED)
  • ✅ 支持wrap_content
  • ✅ 考虑padding
  • ✅ 必须调用setMeasuredDimension()

2. 布局阶段

  • ✅ 使用getMeasuredWidth()和getMeasuredHeight()
  • ✅ 考虑padding和margin
  • ✅ 处理GONE的View

3. 绘制阶段

  • ✅ 不在onDraw()中创建对象
  • ✅ 复用Paint、Path等对象
  • ✅ 避免过度绘制
  • ✅ 使用硬件加速

4. 性能优化

  • ✅ 减少布局层级
  • ✅ 使用ViewStub延迟加载
  • ✅ 使用merge标签
  • ✅ 合理使用缓存

学习路径建议

🎯 学习路径(从易到难)

阶段1:基础理解(1-2天)
  1. ✅ 理解View绘制的三个阶段(Measure、Layout、Draw)
  2. ✅ 理解三个核心方法的作用(onMeasure、onLayout、onDraw)
  3. ✅ 理解MeasureSpec的三种模式

学习资源:

  • 阅读本文档的"View绘制机制概述"和"View绘制的三个核心方法"章节
  • 理解"多大在哪画什么"的记忆口诀
阶段2:深入理解(2-3天)
  1. ✅ 深入理解onMeasure()的实现
  2. ✅ 深入理解onLayout()的实现
  3. ✅ 深入理解onDraw()的实现
  4. ✅ 理解MeasureSpec的传递规则

学习资源:

  • 阅读本文档的"onMeasure()详解"、"onLayout()详解"、"onDraw()详解"章节
  • 阅读"MeasureSpec详解"章节
  • 理解"精确最大无限制"的记忆口诀
阶段3:实践应用(3-5天)
  1. ✅ 实现简单的自定义View(如圆形View)
  2. ✅ 实现简单的自定义ViewGroup(如垂直布局)
  3. ✅ 处理wrap_content和padding
  4. ✅ 优化绘制性能

学习资源:

  • 阅读本文档的"自定义View绘制"和"自定义ViewGroup绘制"章节
  • 参考"实战案例"章节
  • 参考"常见错误对比"章节,避免常见错误
阶段4:高级优化(2-3天)
  1. ✅ 理解绘制优化原理
  2. ✅ 掌握过度绘制的检测和优化
  3. ✅ 掌握性能优化技巧
  4. ✅ 掌握调试技巧

学习资源:

  • 阅读本文档的"绘制优化"章节
  • 阅读"最佳实践"章节
  • 使用Android Studio的调试工具
阶段5:面试准备(1-2天)
  1. ✅ 复习所有面试题
  2. ✅ 理解常见问题的答案
  3. ✅ 准备实际项目经验

学习资源:

  • 阅读本文档的"面试题大全"章节
  • 复习"快速查询表"和"记忆口诀"

📚 推荐学习顺序

1天:基础理解
  ├─ View绘制机制概述
  ├─ View绘制的三个核心方法
  └─ View绘制流程详解

第2-3天:深入理解
  ├─ MeasureSpec详解
  ├─ onMeasure()详解
  ├─ onLayout()详解
  └─ onDraw()详解

第4-5天:实践应用
  ├─ 自定义View绘制
  ├─ 自定义ViewGroup绘制
  └─ 实战案例

第6-7天:高级优化
  ├─ 绘制优化
  ├─ 常见问题与解决方案
  └─ 最佳实践

第8天:面试准备
  └─ 面试题大全

💡 学习技巧

  1. 理解记忆口诀:使用"多大在哪画什么"等口诀快速记忆
  2. 对比学习:使用"常见错误对比"章节,理解错误和正确的区别
  3. 实践为主:多写代码,参考"实战案例"章节
  4. 快速查询:使用"快速查询表"快速查找知识点
  5. 面试准备:重点复习"面试题大全"章节

API索引

View绘制核心API

Measure相关API
API所属类作用使用场景
measure(int widthMeasureSpec, int heightMeasureSpec)View测量View的尺寸由系统调用,一般不需要手动调用
onMeasure(int widthMeasureSpec, int heightMeasureSpec)View测量View的尺寸(需重写)自定义View时必须重写
setMeasuredDimension(int measuredWidth, int measuredHeight)View设置测量结果onMeasure()中必须调用
getMeasuredWidth()View获取测量宽度onLayout()中使用
getMeasuredHeight()View获取测量高度onLayout()中使用
getSuggestedMinimumWidth()View获取建议的最小宽度onMeasure()中计算最小尺寸
getSuggestedMinimumHeight()View获取建议的最小高度onMeasure()中计算最小高度
Layout相关API
API所属类作用使用场景
layout(int l, int t, int r, int b)View布局View的位置由系统调用,ViewGroup中调用子View的layout()
onLayout(boolean changed, int l, int t, int r, int b)View布局View的位置(需重写)ViewGroup必须重写
getWidth()View获取实际宽度onDraw()中使用
getHeight()View获取实际高度onDraw()中使用
getLeft()View获取左边界获取View位置
getTop()View获取上边界获取View位置
getRight()View获取右边界获取View位置
getBottom()View获取下边界获取View位置
Draw相关API
API所属类作用使用场景
draw(Canvas canvas)View绘制View(系统调用)由系统调用,包含完整绘制流程
onDraw(Canvas canvas)View绘制View内容(需重写)自定义View时重写
dispatchDraw(Canvas canvas)ViewGroup绘制子ViewViewGroup绘制子View
invalidate()View标记需要重绘View内容变化时调用
invalidate(int l, int t, int r, int b)View标记指定区域需要重绘只重绘指定区域
postInvalidate()View在非UI线程中标记重绘非UI线程中调用
postInvalidate(int l, int t, int r, int b)View在非UI线程中标记指定区域重绘非UI线程中调用
MeasureSpec相关API
API所属类作用使用场景
MeasureSpec.makeMeasureSpec(int size, int mode)MeasureSpec创建MeasureSpec手动创建MeasureSpec
MeasureSpec.getMode(int measureSpec)MeasureSpec获取测量模式从MeasureSpec中提取Mode
MeasureSpec.getSize(int measureSpec)MeasureSpec获取测量尺寸从MeasureSpec中提取Size
getChildMeasureSpec(int spec, int padding, int childDimension)ViewGroup为子View生成MeasureSpecViewGroup中为子View生成MeasureSpec
resolveSize(int size, int measureSpec)View根据MeasureSpec解析尺寸onMeasure()中调整尺寸
ViewGroup相关API
API所属类作用使用场景
getChildCount()ViewGroup获取子View数量遍历子View
getChildAt(int index)ViewGroup获取指定位置的子View访问子View
addView(View child)ViewGroup添加子View动态添加子View
removeView(View view)ViewGroup移除子View动态移除子View
removeAllViews()ViewGroup移除所有子View清空所有子View
Padding相关API
API所属类作用使用场景
getPaddingLeft()View获取左内边距onMeasure()和onDraw()中使用
getPaddingTop()View获取上内边距onMeasure()和onDraw()中使用
getPaddingRight()View获取右内边距onMeasure()和onDraw()中使用
getPaddingBottom()View获取下内边距onMeasure()和onDraw()中使用
setPadding(int left, int top, int right, int bottom)View设置内边距设置View的内边距
布局相关API
API所属类作用使用场景
requestLayout()View请求重新布局View尺寸或位置变化时调用
forceLayout()View强制重新布局强制View重新布局
isLayoutRequested()View检查是否需要布局检查View是否需要布局
Canvas绘制API
API所属类作用使用场景
drawColor(int color)Canvas绘制颜色绘制背景色
drawCircle(float cx, float cy, float radius, Paint paint)Canvas绘制圆形绘制圆形
drawRect(float left, float top, float right, float bottom, Paint paint)Canvas绘制矩形绘制矩形
drawLine(float startX, float startY, float stopX, float stopY, Paint paint)Canvas绘制线条绘制线条
drawText(String text, float x, float y, Paint paint)Canvas绘制文字绘制文字
drawBitmap(Bitmap bitmap, float left, float top, Paint paint)Canvas绘制图片绘制图片
drawPath(Path path, Paint paint)Canvas绘制路径绘制复杂路径
clipRect(float left, float top, float right, float bottom)Canvas裁剪绘制区域减少过度绘制
save()Canvas保存Canvas状态保存变换状态
restore()Canvas恢复Canvas状态恢复变换状态
translate(float dx, float dy)Canvas平移变换Canvas
rotate(float degrees)Canvas旋转变换Canvas
scale(float sx, float sy)Canvas缩放变换Canvas
Paint相关API
API所属类作用使用场景
setColor(int color)Paint设置颜色设置绘制颜色
setStyle(Paint.Style style)Paint设置样式设置填充或描边
setStrokeWidth(float width)Paint设置线条宽度设置描边宽度
setTextSize(float textSize)Paint设置文字大小设置文字大小
setAntiAlias(boolean aa)Paint设置抗锯齿启用抗锯齿
setTypeface(Typeface typeface)Paint设置字体设置文字字体

方法对比表

getWidth() vs getMeasuredWidth()

方法获取时机值来源使用场景注意事项
getWidth()layout之后right - leftonDraw()中使用在onLayout()中可能为0
getMeasuredWidth()measure之后measuredWidthonLayout()中使用测量后的宽度,可能不等于实际宽度

使用建议:

  • ✅ onLayout()中使用getMeasuredWidth()
  • ✅ onDraw()中使用getWidth()
  • ❌ 不要在onLayout()中使用getWidth()

invalidate() vs requestLayout()

方法触发阶段使用场景性能影响调用时机
invalidate()只触发drawView内容变化较小内容变化时
requestLayout()触发measure、layout、drawView尺寸/位置变化较大尺寸/位置变化时

使用建议:

  • ✅ 内容变化时使用invalidate()
  • ✅ 尺寸/位置变化时使用requestLayout()
  • ❌ 不要在不必要时调用requestLayout()

onMeasure() vs onLayout() vs onDraw()

方法作用必须做什么使用什么获取尺寸调用顺序
onMeasure()测量尺寸调用setMeasuredDimension()使用MeasureSpec.getSize()第1个
onLayout()布局位置调用child.layout()使用getMeasuredWidth()第2个
onDraw()绘制内容使用Canvas绘制使用getWidth()getHeight()第3个

MeasureSpec三种模式对比

模式含义使用场景处理方式记忆口诀
EXACTLY精确值,必须用具体数值、match_parentwidth = widthSize"精确值,必须用"
AT_MOST最大不超过,可以更小wrap_contentwidth = Math.min(计算值, widthSize)"最大不超过,可以更小"
UNSPECIFIED无限制,任意大ScrollView子Viewwidth = 计算值"无限制,任意大"

layout_width/layout_height与MeasureSpec的对应关系

layout_width/layout_height父View的Mode子View的Mode子View的Size
100dp (具体数值)任意EXACTLY100dp
match_parentEXACTLYEXACTLY父View的Size
match_parentAT_MOSTAT_MOST父View的Size
wrap_contentEXACTLYAT_MOST父View的Size
wrap_contentAT_MOSTAT_MOST父View的Size

面试题大全

一、基础概念(10题)

1. View的绘制流程是什么?

答案: View的绘制流程分为三个阶段:

  1. Measure(测量):确定View的宽高
  2. Layout(布局):确定View的位置
  3. Draw(绘制):绘制View的内容

详细流程:

ViewRootImpl.performTraversals()
    ↓
performMeasure() → measure() → onMeasure()
    ↓
performLayout() → layout() → onLayout()
    ↓
performDraw() → draw() → onDraw()
2. onMeasure()、onLayout()、onDraw()的作用是什么?

答案:

  • onMeasure():测量View的宽高,必须调用setMeasuredDimension()设置测量结果
  • onLayout():布局View的位置,确定View在父容器中的位置(left, top, right, bottom)
  • onDraw():绘制View的内容,通过Canvas绘制各种图形、文字、图片等
3. View的MeasureSpec是什么?

答案: MeasureSpec是View测量时的约束条件,包含两个信息:

  1. 测量模式(Mode):EXACTLY(精确值)、AT_MOST(最大不超过)、UNSPECIFIED(未指定)
  2. 测量尺寸(Size):测量的大小

MeasureSpec用一个int值表示,高2位表示Mode,低30位表示Size。

4. View的测量模式有哪些?

答案: 三种测量模式:

  1. EXACTLY(精确模式):View的尺寸已经确定,必须使用指定的尺寸
  2. AT_MOST(最大模式):View的尺寸不能超过指定的尺寸,但可以小于
  3. UNSPECIFIED(未指定模式):View的尺寸没有限制,可以任意大小
5. View的布局流程是什么?

答案: 布局流程:

  1. ViewRootImpl调用performLayout()
  2. 从根View开始,调用View.layout()
  3. View.layout()调用onLayout()
  4. ViewGroup在onLayout()中遍历子View,调用子View.layout()
  5. 确定View的四个边界(left, top, right, bottom)
6. invalidate()和requestLayout()的区别是什么?

答案:

  • invalidate():只触发draw阶段,用于View内容变化时重绘
  • requestLayout():触发measure、layout、draw三个阶段,用于View尺寸或位置变化时重新布局

对比:

方法触发阶段使用场景性能影响
invalidate()只触发draw内容变化较小
requestLayout()触发measure、layout、draw尺寸/位置变化较大
7. getWidth()和getMeasuredWidth()的区别是什么?

答案:

  • getWidth():获取View的实际宽度(布局后的宽度),计算方式:right - left,在layout阶段之后才有值
  • getMeasuredWidth():获取View的测量宽度(测量后的宽度),在measure阶段之后就有值

注意: 在onLayout()中应该使用getMeasuredWidth(),而不是getWidth()

8. View的绘制顺序是什么?

答案: View.draw()的绘制顺序:

  1. 绘制背景(drawBackground)
  2. 绘制内容(onDraw)
  3. 绘制子View(dispatchDraw,ViewGroup才有)
  4. 绘制前景、滚动条等(onDrawForeground)
9. ViewGroup的绘制流程和View有什么区别?

答案: ViewGroup继承自View,绘制流程基本相同,但有以下区别:

  1. onMeasure():ViewGroup需要先测量所有子View,再根据子View确定自己的尺寸
  2. onLayout():ViewGroup需要布局所有子View的位置
  3. dispatchDraw():ViewGroup需要绘制子View(View的dispatchDraw()是空实现)
10. 为什么View的onLayout()是空实现?

答案: 因为View是叶子节点,没有子View,所以不需要布局子View。ViewGroup需要重写onLayout()来布局子View。


二、MeasureSpec详解(8题)

11. MeasureSpec的三种模式分别对应什么场景?

答案:

  • EXACTLY
    • 指定了具体数值:android:layout_width="100dp"
    • 指定了match_parent:父View已经确定了尺寸
  • AT_MOST
    • 指定了wrap_content:View可以根据内容确定尺寸,但不能超过父View
  • UNSPECIFIED
    • ScrollView中的子View:可以超出屏幕
    • 自定义View的特殊情况
12. 父View的MeasureSpec如何传递给子View?

答案: 根据子View的layout_width/layout_height和父View的MeasureSpec,生成子View的MeasureSpec:

  • match_parent + 父View的EXACTLY → 子View的EXACTLY
  • match_parent + 父View的AT_MOST → 子View的AT_MOST
  • wrap_content + 父View的EXACTLY → 子View的AT_MOST
  • wrap_content + 父View的AT_MOST → 子View的AT_MOST
  • 具体数值 → 子View的EXACTLY
13. 如何从MeasureSpec中获取Mode和Size?

答案:

int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
14. 如何创建MeasureSpec?

答案:

int measureSpec = MeasureSpec.makeMeasureSpec(size, mode);
15. ViewGroup如何为子View生成MeasureSpec?

答案: 使用getChildMeasureSpec()方法:

int childWidthSpec = ViewGroup.getChildMeasureSpec(
    parentWidthMeasureSpec,
    padding,
    childLayoutParams.width
);
16. wrap_content为什么需要处理AT_MOST模式?

答案: 因为wrap_content时,父View会传递AT_MOST模式的MeasureSpec,表示View的尺寸不能超过父View,但可以根据内容确定。如果不处理AT_MOST模式,wrap_content就不会生效。

17. 自定义View如何支持wrap_content?

答案: 在onMeasure()中处理AT_MOST模式:

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int width = 0;
if (widthMode == MeasureSpec.AT_MOST) {
    width = calculateContentWidth();  // 计算内容宽度
    width = Math.min(width, widthSize);  // 不能超过父View
} else if (widthMode == MeasureSpec.EXACTLY) {
    width = widthSize;
}

setMeasuredDimension(width, height);
18. UNSPECIFIED模式的使用场景是什么?

答案:

  • ScrollView中的子View:可以超出屏幕,没有尺寸限制
  • 自定义View的特殊情况:需要无限大的尺寸
  • 测量View的最大尺寸:用于确定View的最大可能尺寸

三、onMeasure()详解(10题)

19. onMeasure()的调用时机是什么?

答案:

  1. View首次添加到ViewTree时
  2. 调用requestLayout()时
  3. 父View的尺寸发生变化时
  4. 子View的尺寸发生变化时
20. onMeasure()中必须做什么?

答案: 必须调用setMeasuredDimension(width, height)设置测量结果,否则会抛出异常。

21. 自定义View的onMeasure()如何实现?

答案:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
    int width = 0;
    int height = 0;
    
    // 根据测量模式计算宽度
    switch (widthMode) {
        case MeasureSpec.EXACTLY:
            width = widthSize;
            break;
        case MeasureSpec.AT_MOST:
            width = Math.min(calculateWidth(), widthSize);
            break;
        case MeasureSpec.UNSPECIFIED:
            width = calculateWidth();
            break;
    }
    
    // 根据测量模式计算高度
    switch (heightMode) {
        case MeasureSpec.EXACTLY:
            height = heightSize;
            break;
        case MeasureSpec.AT_MOST:
            height = Math.min(calculateHeight(), heightSize);
            break;
        case MeasureSpec.UNSPECIFIED:
            height = calculateHeight();
            break;
    }
    
    setMeasuredDimension(width, height);
}
22. onMeasure()中如何考虑padding?

答案:

int width = contentWidth + getPaddingLeft() + getPaddingRight();
int height = contentHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, height);
23. 为什么不能在onMeasure()中调用requestLayout()?

答案: 会导致无限递归:

  1. onMeasure()中调用requestLayout()
  2. requestLayout()触发measure()
  3. measure()又调用onMeasure()
  4. 形成无限循环
24. View的默认onMeasure()实现是什么?

答案:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
    );
}
25. getSuggestedMinimumWidth()的作用是什么?

答案: 返回View建议的最小宽度,考虑背景Drawable的最小宽度和minWidth属性。

26. ViewGroup的onMeasure()如何实现?

答案:

  1. 遍历所有子View,调用子View.measure()测量子View
  2. 根据子View的尺寸确定自己的尺寸
  3. 调用setMeasuredDimension()设置自己的尺寸
27. 如何测量GONE的View?

答案: 不需要测量GONE的View,在onMeasure()中跳过:

if (child.getVisibility() != GONE) {
    child.measure(childWidthSpec, childHeightSpec);
}
28. 自定义ViewGroup的onMeasure()如何实现?

答案:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 1. 测量所有子View
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            int childWidthSpec = getChildMeasureSpec(...);
            int childHeightSpec = getChildMeasureSpec(...);
            child.measure(childWidthSpec, childHeightSpec);
        }
    }
    
    // 2. 根据子View确定自己的尺寸
    int width = calculateWidthFromChildren();
    int height = calculateHeightFromChildren();
    
    // 3. 根据MeasureSpec调整尺寸
    width = resolveSize(width, widthMeasureSpec);
    height = resolveSize(height, heightMeasureSpec);
    
    setMeasuredDimension(width, height);
}

四、onLayout()详解(8题)

29. onLayout()的调用时机是什么?

答案:

  1. View首次添加到ViewTree时
  2. 调用requestLayout()时
  3. 父View的尺寸或位置发生变化时
30. onLayout()的参数含义是什么?

答案:

protected void onLayout(boolean changed, int l, int t, int r, int b)
  • changed:View的尺寸或位置是否发生变化
  • l, t, r, b:View的四个边界(left, top, right, bottom)
31. 自定义View的onLayout()如何实现?

答案: View的onLayout()是空实现,因为View是叶子节点,没有子View。自定义View一般不需要重写onLayout()。

32. 自定义ViewGroup的onLayout()如何实现?

答案:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            // 计算子View的位置
            int childLeft = calculateChildLeft(i);
            int childTop = calculateChildTop(i);
            int childRight = childLeft + child.getMeasuredWidth();
            int childBottom = childTop + child.getMeasuredHeight();
            
            // 布局子View
            child.layout(childLeft, childTop, childRight, childBottom);
        }
    }
}
33. layout()方法的作用是什么?

答案: 设置View的四个边界(left, top, right, bottom),并调用onLayout()。

34. 在onLayout()中应该使用getWidth()还是getMeasuredWidth()?

答案: 应该使用getMeasuredWidth(),因为:

  • getMeasuredWidth()在measure阶段之后就有值
  • getWidth()在layout阶段之后才有值
  • onLayout()在layout阶段执行,此时getWidth()可能还没有值
35. onLayout()中如何考虑padding?

答案:

int childLeft = getPaddingLeft();
int childTop = getPaddingTop();
int childRight = getWidth() - getPaddingRight();
int childBottom = getHeight() - getPaddingBottom();
36. 如何布局GONE的View?

答案: 不需要布局GONE的View,在onLayout()中跳过。


五、onDraw()详解(10题)

37. onDraw()的调用时机是什么?

答案:

  1. View首次显示时
  2. 调用invalidate()时
  3. View的内容发生变化时
  4. 动画执行时
38. 自定义View的onDraw()如何实现?

答案:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    // 绘制内容
    canvas.drawColor(Color.WHITE);
    canvas.drawCircle(100, 100, 50, paint);
    canvas.drawText("Hello", 50, 50, paint);
}
39. View.draw()的完整流程是什么?

答案:

public void draw(Canvas canvas) {
    // 1. 绘制背景
    drawBackground(canvas);
    
    // 2. 绘制内容
    onDraw(canvas);
    
    // 3. 绘制子View(ViewGroup)
    dispatchDraw(canvas);
    
    // 4. 绘制前景、滚动条等
    onDrawForeground(canvas);
}
40. 为什么不能在onDraw()中创建对象?

答案: 因为onDraw()会被频繁调用,在onDraw()中创建对象会导致:

  1. 频繁创建对象,增加GC压力
  2. 影响绘制性能
  3. 可能导致内存泄漏

正确做法: 提前创建对象并复用。

41. Canvas的绘制操作有哪些?

答案:

  • 绘制基本图形:drawPoint、drawLine、drawRect、drawCircle等
  • 绘制文字:drawText
  • 绘制图片:drawBitmap
  • 绘制路径:drawPath
  • 变换操作:translate、rotate、scale、skew
42. Paint的使用注意事项是什么?

答案:

  1. 提前创建Paint对象,不要在onDraw()中创建
  2. 复用Paint对象
  3. 合理设置Paint的属性(颜色、样式、抗锯齿等)
43. Canvas的save()和restore()的作用是什么?

答案:

  • save():保存当前Canvas的状态(变换矩阵、裁剪区域等)
  • restore():恢复之前保存的Canvas状态

用于在变换Canvas后恢复原始状态。

44. 如何实现自定义View的动画效果?

答案:

  1. 使用ValueAnimator或ObjectAnimator
  2. 在动画回调中更新View的状态
  3. 调用invalidate()重绘
  4. 在onDraw()中根据状态绘制
45. ViewGroup如何绘制子View?

答案: ViewGroup的draw()方法会调用dispatchDraw(),dispatchDraw()会遍历所有子View,调用子View的draw()方法。

46. 如何优化onDraw()的性能?

答案:

  1. 不在onDraw()中创建对象
  2. 复用Paint、Path等对象
  3. 使用clipRect()裁剪绘制区域
  4. 使用硬件加速
  5. 减少不必要的绘制

六、绘制优化(10题)

47. 过度绘制(Overdraw)是什么?

答案: 过度绘制是指同一个像素被绘制了多次,造成性能浪费。

48. 如何检测过度绘制?

答案:

  1. 开启开发者选项中的"显示过度绘制"
  2. 颜色含义:
    • 蓝色:绘制1次(正常)
    • 绿色:绘制2次(可接受)
    • 浅红色:绘制3次(需要优化)
    • 深红色:绘制4次以上(必须优化)
49. 如何减少过度绘制?

答案:

  1. 移除不必要的背景
  2. 使用clipRect()裁剪绘制区域
  3. 使用ViewStub延迟加载
  4. 使用merge标签减少布局层级
  5. 使用invalidate()的带参数版本
50. 自定义View的绘制优化有哪些?

答案:

  1. 不在onDraw()中创建对象
  2. 复用Paint、Path等对象
  3. 使用硬件加速
  4. 使用缓存(Bitmap缓存)
  5. 减少绘制区域(clipRect)
  6. 避免不必要的绘制
51. 图片的绘制优化有哪些?

答案:

  1. 使用合适的图片格式(WebP、PNG、JPG)
  2. 压缩图片大小
  3. 使用图片缓存(LruCache)
  4. 使用缩略图
  5. 异步加载图片
52. 动画的绘制优化有哪些?

答案:

  1. 使用硬件加速
  2. 使用属性动画代替补间动画
  3. 减少动画的复杂度
  4. 使用ViewPropertyAnimator
  5. 避免在动画中创建对象
53. 绘制优化的最佳实践有哪些?

答案:

  1. 减少布局层级
  2. 使用ViewStub延迟加载
  3. 使用merge标签
  4. 合理使用缓存
  5. 使用硬件加速
  6. 避免过度绘制
  7. 优化onDraw()方法
54. 绘制优化的监控如何实现?

答案:

  1. 使用Systrace监控绘制性能
  2. 使用GPU渲染模式分析
  3. 使用Layout Inspector分析布局
  4. 使用过度绘制检测工具
  5. 使用性能分析工具(Android Profiler)
55. 硬件加速的作用是什么?

答案: 硬件加速利用GPU进行绘制,提高绘制性能,减少CPU负担。

56. 如何启用硬件加速?

答案:

<!-- AndroidManifest.xml -->
<application
    android:hardwareAccelerated="true">
    ...
</application>

七、自定义View实践(10题)

57. 自定义View的方式有哪些?

答案:

  1. 继承View:实现完全自定义的View
  2. 继承ViewGroup:实现自定义的布局容器
  3. 组合现有View:通过组合多个View实现
  4. 继承现有View:扩展现有View的功能
58. 自定义View的步骤是什么?

答案:

  1. 继承View或ViewGroup
  2. 重写onMeasure():测量View的尺寸
  3. 重写onLayout()(ViewGroup需要):布局子View
  4. 重写onDraw():绘制View的内容
  5. 处理触摸事件(可选):重写onTouchEvent()
59. 自定义View的性能优化有哪些?

答案:

  1. 不在onDraw()中创建对象
  2. 复用Paint、Path等对象
  3. 使用硬件加速
  4. 使用缓存
  5. 减少绘制区域
  6. 避免过度绘制
60. 自定义View的内存泄漏如何避免?

答案:

  1. 使用Application Context
  2. 及时回收Bitmap
  3. 移除Handler的Callback
  4. 避免持有Activity的引用
  5. 使用WeakReference
61. 自定义View的触摸事件如何处理?

答案: 重写onTouchEvent()方法:

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 处理按下事件
            return true;
        case MotionEvent.ACTION_MOVE:
            // 处理移动事件
            return true;
        case MotionEvent.ACTION_UP:
            // 处理抬起事件
            return true;
    }
    return super.onTouchEvent(event);
}
62. 自定义View的动画如何实现?

答案:

  1. 使用ValueAnimator或ObjectAnimator
  2. 在动画回调中更新View的状态
  3. 调用invalidate()重绘
  4. 在onDraw()中根据状态绘制
63. 自定义View的最佳实践有哪些?

答案:

  1. 正确处理三种测量模式
  2. 支持wrap_content
  3. 考虑padding
  4. 不在onDraw()中创建对象
  5. 使用硬件加速
  6. 避免过度绘制
  7. 处理内存泄漏
64. 自定义View的常见问题有哪些?

答案:

  1. View不显示
  2. wrap_content不生效
  3. padding不生效
  4. 内存泄漏
  5. 性能问题
  6. 触摸事件不响应
65. 自定义View的测试如何进行?

答案:

  1. 单元测试:测试View的逻辑
  2. UI测试:测试View的显示
  3. 性能测试:测试View的性能
  4. 兼容性测试:测试不同版本的兼容性
66. 自定义ViewGroup的onMeasure()如何实现?

答案:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 1. 测量所有子View
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            int childWidthSpec = getChildMeasureSpec(...);
            int childHeightSpec = getChildMeasureSpec(...);
            child.measure(childWidthSpec, childHeightSpec);
        }
    }
    
    // 2. 根据子View确定自己的尺寸
    int width = calculateWidthFromChildren();
    int height = calculateHeightFromChildren();
    
    // 3. 设置测量结果
    setMeasuredDimension(width, height);
}