目录
  1. 1. 简介
  2. 2. View 体系概览
    1. 2.1. View 的类继承关系
    2. 2.2. View 的核心职责
  3. 3. MeasureSpec 与测量规格
    1. 3.1. MeasureSpec 的定义
    2. 3.2. 三种测量模式详解
      1. 3.2.1. EXACTLY(精确模式)
      2. 3.2.2. AT_MOST(至多模式)
      3. 3.2.3. UNSPECIFIED(未指定模式)
    3. 3.3. MeasureSpec 的传递规则
  4. 4. onMeasure:测量流程深度解析
    1. 4.1. View.onMeasure 的默认实现
    2. 4.2. 正确的 wrap_content 处理
    3. 4.3. ViewGroup 的 onMeasure:measureChildren
    4. 4.4. resolveSizeAndState 的使用
  5. 5. onLayout:布局流程深度解析
    1. 5.1. View.layout 的四步走
    2. 5.2. ViewGroup 的 onLayout
  6. 6. onDraw:绘制流程深度解析
    1. 6.1. View.draw 的绘制顺序
    2. 6.2. Canvas 的核心 API
      1. 6.2.1. 变换操作
      2. 6.2.2. 裁剪操作
      3. 6.2.3. 绘制基本图形
      4. 6.2.4. 绘制文本
      5. 6.2.5. 绘制位图
    3. 6.3. Paint 的核心 API
  7. 7. 自定义属性:declare-styleable
    1. 7.1. 步骤一:在 attrs.xml 中声明
    2. 7.2. 步骤二:在构造函数中解析
    3. 7.3. 步骤三:在 XML 中使用
  8. 8. 触摸事件处理
    1. 8.1. 基本事件处理
    2. 8.2. GestureDetector 的使用
  9. 9. 请求重新布局和重新绘制
    1. 9.1. requestLayout vs invalidate
  10. 10. 状态保存与恢复
    1. 10.1. onSaveInstanceState / onRestoreInstanceState
  11. 11. 性能优化要点
    1. 11.1. 避免在 onDraw 中分配对象
    2. 11.2. 硬件层优化
    3. 11.3. 减少过度绘制
    4. 11.4. 使用 ViewStub 延迟加载
  12. 12. 完整示例一:渐变环形进度指示器
    1. 12.1. 使用方式
  13. 13. 完整示例二:流式标签布局(FlowLayout)
  14. 14. 总结
    1. 14.1. 关键源码文件
UI进阶之自定义View

简介

自定义View是Android UI开发中绕不开的核心技能。无论是实现设计师天马行空的交互效果,还是构建可复用的UI组件库,深入理解View的测量、布局、绘制三大流程都是基本功。

本文从AOSP源码角度出发,以 frameworks/base/core/java/android/view/View.java 为核心分析对象,系统讲解自定义View的完整知识体系:MeasureSpec的传递规则、onMeasure的测量逻辑、onLayout的布局策略、onDraw的绘制技巧,以及自定义属性、触摸事件处理、性能优化等实战要点。文末提供两个完整的自定义View示例:一个渐变环形进度指示器和一个流式标签布局(FlowLayout)。

注意:本文源码分析基于 Android-30(Android 11),不同版本间实现可能有细微差异。

View 体系概览

在深入自定义View之前,有必要先理清View在整个Android UI体系中的位置。

View 的类继承关系

java.lang.Object
└── android.view.View
├── android.view.ViewGroup
│ ├── android.widget.FrameLayout
│ │ └── com.android.internal.policy.DecorView
│ ├── android.widget.LinearLayout
│ ├── android.widget.RelativeLayout
│ ├── android.widget.ConstraintLayout
│ └── ...
└── android.widget.TextView
└── android.widget.Button
└── ...

View 是所有UI组件的基类。ViewGroup 继承自 View,同时作为容器管理一组子 View。这种 Composite 设计模式使得整个视图树可以递归地执行测量、布局和绘制流程。

View 的核心职责

从源码角度看,一个 View 对象承载了以下核心职责:

  1. 测量:计算自身以及子 View 的尺寸(measure / onMeasure)。
  2. 布局:确定自身以及子 View 的位置(layout / onLayout)。
  3. 绘制:将自身渲染到 Canvas 上(draw / onDraw)。
  4. 事件处理:接收和处理触摸事件(dispatchTouchEvent / onTouchEvent)。
  5. 状态管理:保存和恢复视图状态(onSaveInstanceState / onRestoreInstanceState)。

自定义View本质上就是根据自己的需求,重写上述方法中的某一个或某几个。

MeasureSpec 与测量规格

在开始讲 onMeasure 之前,必须先理解 MeasureSpec 这个核心概念。MeasureSpec 是 View 测量过程中父容器向子 View 传递约束信息的载体。

MeasureSpec 的定义

源码位置:frameworks/base/core/java/android/view/View.java

public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 1100...00

// 三种测量模式
public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 00...
public static final int EXACTLY = 1 << MODE_SHIFT; // 01...
public static final int AT_MOST = 2 << MODE_SHIFT; // 10...

// 将一个 size 和 mode 组合成一个 MeasureSpec
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}

// 提取 mode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}

// 提取 size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}

MeasureSpec 是一个 32 位的 int 值,高 2 位存储测量模式(mode),低 30 位存储具体的尺寸值(size)。这种打包方式避免了额外的对象分配,GC友好的设计贯穿了整个 Android 框架。

三种测量模式详解

EXACTLY(精确模式)

当父容器能够确定子 View 的精确大小时使用。典型场景:

  • 子 View 的 layout_width / layout_height 设置为具体数值,如 100dp
  • 子 View 的 layout_width / layout_height 设置为 match_parent(填充父容器),且父容器自身有精确尺寸。

在 EXACTLY 模式下,子 View 的最终尺寸就是 MeasureSpec 中的 size,不可超越。

AT_MOST(至多模式)

当父容器无法确定子 View 的精确大小,但可以给出一个最大限制时使用。典型场景:

  • 子 View 的 layout_width / layout_height 设置为 wrap_content
  • 子 View 的尺寸可以在 0 到 MeasureSpec 的 size 之间任意取值。

在 AT_MOST 模式下,子 View 需要根据自己的内容计算出实际所需大小,但不能超过父容器给定的最大值。这是自定义 View 中最需要正确处理的一种模式。

UNSPECIFIED(未指定模式)

父容器对子 View 没有任何约束,子 View 可以是任意大小。这个模式在常规应用开发中较少出现,主要出现在以下场景:

  • ScrollView 等可滚动容器测量子 View 时,滚动方向上的尺寸不受限制。
  • RecyclerView 测量 item 时,如果 item 尺寸未知。
  • 系统内部测量 DecorView 时的初始 pass。

很多初学者容易忽略 UNSPECIFIED 模式的处理,导致自定义 View 在 ScrollView 或 RecyclerView 中出现尺寸异常。

MeasureSpec 的传递规则

父容器在测量子 View 时,会根据自身的 MeasureSpec 和子 View 的 LayoutParams 共同决定子 View 的 MeasureSpec。这个逻辑在 ViewGroup 的 getChildMeasureSpec 方法中,是自定义 ViewGroup 必须理解的核心逻辑。

源码位置:frameworks/base/core/java/android/view/ViewGroup.java

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
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;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

理解这个方法的规则,可以用以下表格总结:

父 SpecMode \ 子 LayoutParam 精确值 (dp) MATCH_PARENT WRAP_CONTENT
EXACTLY EXACTLY / childDim EXACTLY / parentSize AT_MOST / parentSize
AT_MOST EXACTLY / childDim AT_MOST / parentSize AT_MOST / parentSize
UNSPECIFIED EXACTLY / childDim UNSPECIFIED / 0 UNSPECIFIED / 0

掌握了这张表,就掌握了 View 测量系统的核心规则。

onMeasure:测量流程深度解析

View.onMeasure 的默认实现

源码位置:frameworks/base/core/java/android/view/View.java

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

public static 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;
break;
}
return result;
}

protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

关键点分析:

  1. getDefaultSize 在 AT_MOST 和 EXACTLY 模式下返回相同的 size——都取 specSize。这意味着直接继承 View 的自定义控件,如果不重写 onMeasure,wrap_content 和 match_parent 的效果是完全一样的。这是自定义 View 最常见的一个坑。

  2. getSuggestedMinimumWidth/Height 考虑了背景 Drawable 的最小尺寸和 mMinWidth/mMinHeight。这就是为什么给 View 设置 background 后,即使不手动设置 minWidth,它也可能有一个最小尺寸。

  3. setMeasuredDimension 必须在 onMeasure 中被调用,否则会抛出 IllegalStateException。

正确的 wrap_content 处理

处理 wrap_content 的正确做法是:在 AT_MOST 模式下,根据内容计算自身尺寸;在 EXACTLY 模式下,使用父容器给定的尺寸。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int width = widthSize;
int height = heightSize;

// 如果是 wrap_content,根据实际内容计算尺寸
if (widthMode != MeasureSpec.EXACTLY) {
width = calculateContentWidth() + getPaddingLeft() + getPaddingRight();
}
if (heightMode != MeasureSpec.EXACTLY) {
height = calculateContentHeight() + getPaddingTop() + getPaddingBottom();
}

// 确保不超过父容器给定的最大值
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(width, widthSize);
}
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
}

setMeasuredDimension(width, height);
}

ViewGroup 的 onMeasure:measureChildren

对于自定义 ViewGroup,测量过程中需要遍历子 View 并触发它们的测量。ViewGroup 提供了几个便捷方法:

// 测量所有子 View,但不考虑自身的 padding
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

// 测量单个子 View,考虑父容器的 padding
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(
parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(
parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// 测量单个子 View,额外考虑 margin
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(
parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed,
lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(
parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed,
lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

注意:measureChildren 会跳过 visibility 为 GONE 的子 View。GONE 的子 View 不参与测量,也不占据布局空间。

resolveSizeAndState 的使用

当测量结果需要同时反映尺寸和状态(如超出边界)时,应使用 resolveSizeAndState

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}

MEASURED_STATE_TOO_SMALL 标记表示子 View 的内容超出了分配的尺寸。这在实现类似 “无论内容多大,先给出一个初始尺寸” 的逻辑时非常有用。

onLayout:布局流程深度解析

View.layout 的四步走

layout 方法负责确定 View 自身的位置:

public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_IS_LAID_OUT) == 0
|| (mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
onLayoutChangeListeners...;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

// setFrame / setOpticalFrame 设置 mLeft/mTop/mRight/mBottom
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b); // 回调子类

if (changed) {
// 通知 onLayoutChange 监听器
...
}
}

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

// 通知 autofill...
}

关键参数:ltrb 分别是相对于父容器的左、上、右、下坐标。View 的宽高可以通过 r - lb - t 计算得出。

ViewGroup 的 onLayout

ViewGroup 的 onLayout 是一个抽象方法,每个具体的 ViewGroup 子类必须实现它来确定所有子 View 的位置:

@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

在自定义 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) {
continue;
}
// 根据业务逻辑计算每个子 View 的 left/top/right/bottom
int childLeft = ...;
int childTop = ...;
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
child.layout(childLeft, childTop, childRight, childBottom);
}
}

注意:调用 child.layout() 时,right 和 bottom 分别是用 left + measuredWidth 和 top + measuredHeight 计算出来的。宽度和高度必须使用 getMeasuredWidth()getMeasuredHeight(),而不是 getWidth()getHeight(),因为在 layout 执行之前,getWidth/getHeight 可能还不可用。

onDraw:绘制流程深度解析

View.draw 的绘制顺序

源码位置:frameworks/base/core/java/android/view/View.java

View 的绘制过程遵循严格的顺序:

public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

// 步骤 1:绘制背景
drawBackground(canvas);

// 步骤 2:如有必要,保存 canvas 图层
if (!verticalEdges && !horizontalEdges) {
// 步骤 3:绘制自身内容
onDraw(canvas);

// 步骤 4:绘制子 View
dispatchDraw(canvas);

// ... 绘制叠加层
...

// 步骤 5:绘制前景(滚动条等装饰)
onDrawForeground(canvas);

// 步骤 6:绘制默认焦点高亮
drawDefaultFocusHighlight(canvas);
return;
}
// ... 处理渐变边缘的情况
}

这个顺序意味着:

  • 背景在最下层。
  • 自身内容在背景之上。
  • 子 View 在自身内容之上(对于 ViewGroup)。
  • 滚动条等装饰在最上层。

Canvas 的核心 API

Canvas 是所有绘制操作的载体。关键 API 分类如下:

变换操作

canvas.translate(dx, dy);     // 平移
canvas.scale(sx, sy); // 缩放
canvas.rotate(degrees); // 旋转
canvas.skew(sx, sy); // 倾斜
canvas.save(); // 保存当前状态到栈
canvas.restore(); // 恢复到上一次 save 的状态

save/restore 是成对使用的,类似于 OpenGL 的 pushMatrix/popMatrix。每次 save 都会保存当前的变换矩阵和裁剪区域,restore 时恢复。这是实现复杂绘制效果的基石。

裁剪操作

canvas.clipRect(left, top, right, bottom);
canvas.clipPath(path);
canvas.clipOutRect(left, top, right, bottom); // API 26+
canvas.clipOutPath(path); // API 26+

裁剪可以限制绘制区域,减少不必要的绘制操作(避免 overdraw),同时也可以创造特殊的视觉效果(如圆形头像)。

绘制基本图形

canvas.drawRect(rect, paint);
canvas.drawRoundRect(rect, rx, ry, paint);
canvas.drawCircle(cx, cy, radius, paint);
canvas.drawOval(rect, paint);
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
canvas.drawPath(path, paint);
canvas.drawLine(startX, startY, stopX, stopY, paint);
canvas.drawPoint(x, y, paint);

绘制文本

canvas.drawText(text, x, y, paint);
canvas.drawTextOnPath(text, path, hOffset, vOffset, paint);

绘制位图

canvas.drawBitmap(bitmap, left, top, paint);
canvas.drawBitmap(bitmap, src, dst, paint);
canvas.drawBitmap(bitmap, matrix, paint);

Paint 的核心 API

Paint 控制绘制的风格和效果。它是绘制 API 中最重要的配置对象:

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);  // 开启抗锯齿

// 基本属性
paint.setColor(0xFF333333); // 颜色
paint.setStyle(Paint.Style.FILL); // 填充 / 描边 / 填充描边
paint.setStrokeWidth(dpToPx(2)); // 描边宽度
paint.setStrokeCap(Paint.Cap.ROUND); // 线端点形状
paint.setStrokeJoin(Paint.Join.ROUND); // 线段连接处形状

// 文本属性
paint.setTextSize(spToPx(14));
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setTextAlign(Paint.Align.CENTER);

// 效果
paint.setAntiAlias(true); // 抗锯齿
paint.setDither(true); // 抖动(用于低色深位图)
paint.setAlpha(128); // 透明度
paint.setShader(shader); // 着色器(渐变/图片填充)
paint.setColorFilter(colorFilter); // 颜色过滤器
paint.setXfermode(xfermode); // 混合模式
paint.setMaskFilter(maskFilter); // 遮罩滤镜(模糊等)
paint.setPathEffect(pathEffect); // 路径效果(虚线等)

自定义属性:declare-styleable

自定义 View 通常需要支持 XML 可配置属性。这需要三个步骤。

步骤一:在 attrs.xml 中声明

<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CircleProgressView">
<attr name="progress" format="float" />
<attr name="maxProgress" format="float" />
<attr name="progressColor" format="color" />
<attr name="progressBgColor" format="color" />
<attr name="progressWidth" format="dimension" />
<attr name="startAngle" format="float" />
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
<attr name="showText" format="boolean" />
<attr name="gradientEndColor" format="color" />
</declare-styleable>
</resources>

支持的 format 类型:boolean、color、dimension、float、integer、string、fraction、enum、flag、reference。

步骤二:在构造函数中解析

public CircleProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView);

progress = ta.getFloat(R.styleable.CircleProgressView_progress, 0f);
maxProgress = ta.getFloat(R.styleable.CircleProgressView_maxProgress, 100f);
progressColor = ta.getColor(R.styleable.CircleProgressView_progressColor, Color.BLUE);
progressBgColor = ta.getColor(R.styleable.CircleProgressView_progressBgColor, Color.LTGRAY);
progressWidth = ta.getDimension(R.styleable.CircleProgressView_progressWidth, dpToPx(4));
startAngle = ta.getFloat(R.styleable.CircleProgressView_startAngle, -90f);
showText = ta.getBoolean(R.styleable.CircleProgressView_showText, true);
textSize = ta.getDimension(R.styleable.CircleProgressView_textSize, spToPx(14));
textColor = ta.getColor(R.styleable.CircleProgressView_textColor, Color.BLACK);
gradientEndColor = ta.getColor(R.styleable.CircleProgressView_gradientEndColor, progressColor);

ta.recycle(); // 必须调用 recycle,TypedArray 是共享的

init();
}

obtainStyledAttributes 会从资源池中取出一个 TypedArray 实例,使用完毕后必须调用 recycle() 归还,否则会造成资源泄漏。

步骤三:在 XML 中使用

<com.example.uipractice.CircleProgressView
android:layout_width="120dp"
android:layout_height="120dp"
app:progress="65"
app:maxProgress="100"
app:progressColor="#FF6B6B"
app:gradientEndColor="#FFE66D"
app:progressWidth="6dp"
app:showText="true"
app:textSize="16sp" />

触摸事件处理

自定义 View 的触摸交互是另一个重要维度。

基本事件处理

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 手指按下
return true; // 返回 true 表示要处理后续事件
case MotionEvent.ACTION_MOVE:
// 手指移动
break;
case MotionEvent.ACTION_UP:
// 手指抬起
performClick(); // 触发点击回调
break;
case MotionEvent.ACTION_CANCEL:
// 事件被取消(如滑出边界、父容器拦截)
break;
}
return super.onTouchEvent(event);
}

注意:如果在 ACTION_DOWN 中返回 false,后续的 MOVE、UP 等事件都不会再传递给该 View。另外,如果覆盖了 onTouchEvent,记得在 UP 中调用 performClick(),否则无障碍服务可能无法正常工作。

GestureDetector 的使用

对于复杂的手势(单击、双击、长按、滑动等),可以使用 GestureDetector:

private GestureDetector mGestureDetector;

private void init() {
mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return true; // 必须返回 true,否则其他事件不会被处理
}

@Override
public boolean onSingleTapUp(MotionEvent e) {
performClick();
return true;
}

@Override
public void onLongPress(MotionEvent e) {
// 长按处理
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 滑动处理
return true;
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// 快速滑动处理
return true;
}
});
}

@Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event) || super.onTouchEvent(event);
}

请求重新布局和重新绘制

requestLayout vs invalidate

这是两个最容易混淆的方法:

  • requestLayout():当 View 的尺寸或位置发生变化时调用。它会沿着视图树向上标记,最终触发 ViewRootImpl 的 performTraversals,完成完整的 measure + layout + draw 流程。
  • invalidate():当 View 的内容(外观)发生变化但尺寸不变时调用。它只会触发 draw 流程,不会重新 measure 和 layout。

源码位置:frameworks/base/core/java/android/view/View.java

public void requestLayout() {
// 清理测量缓存
if (mMeasureCache != null) mMeasureCache.clear();

if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}

mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;

if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout(); // 向上传递
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
public void invalidate() {
invalidate(true);
}

void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullRefresh) {
// 如果 View 不可见 且 没有动画运行,直接返回
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) != (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED) {
...
}

final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage); // 最终会传到 ViewRootImpl
}
...
}

一个常见的坑:在 onMeasureonLayout 内部调用 requestLayout 会导致无限循环。

状态保存与恢复

当 Activity 因配置变化(如旋转屏幕)被重建时,自定义 View 需要保存和恢复自己的状态。

onSaveInstanceState / onRestoreInstanceState

@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
// 先保存父类的状态
bundle.putParcelable("superState", super.onSaveInstanceState());
// 再保存自己的状态
bundle.putFloat("progress", progress);
bundle.putFloat("maxProgress", maxProgress);
return bundle;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
progress = bundle.getFloat("progress");
maxProgress = bundle.getFloat("maxProgress");
// 恢复父类的状态
state = bundle.getParcelable("superState");
}
super.onRestoreInstanceState(state);
}

注意:View 必须要有 id 才能参与状态保存。没有设置 id 的 View,其 onSaveInstanceState 不会被系统调用。

性能优化要点

避免在 onDraw 中分配对象

onDraw 的调用频率非常高(每帧都可能触发),在其中创建对象会触发频繁 GC,导致帧率下降。Paint、Path、RectF 等对象应该在构造或初始化时创建,在 onDraw 中只做值修改。

// 错误做法
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint(); // 每帧创建一个新对象
RectF rect = new RectF(); // 同上
...
}

// 正确做法
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private RectF mRectF = new RectF();

@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(currentColor);
mRectF.set(0, 0, getWidth(), getHeight());
...
}

硬件层优化

对于内容不经常变化但绘制复杂度较高的 View(如复杂的静态图表),可以使用硬件层来缓存绘制结果:

// 设置硬件层类型
setLayerType(View.LAYER_TYPE_HARDWARE, null);

// 内容变化时更新层
invalidate();

// 动画结束后可以移除层以释放显存
setLayerType(View.LAYER_TYPE_NONE, null);

源码位置:frameworks/base/core/java/android/view/View.java

/**
* LAYER_TYPE_NONE: 不启用层,正常绘制
* LAYER_TYPE_SOFTWARE: 使用软件层(Bitmap),适用于需要 Xfermode 的场景
* LAYER_TYPE_HARDWARE: 使用硬件层(GPU texture),适用于复杂的静态内容
*/

LAYER_TYPE_HARDWARE 会将 View 渲染到一个 GPU 纹理中,后续帧只需将这个纹理合成到屏幕上,避免了每帧都执行复杂的绘制命令。但要注意,显存是有限的,不要对大量 View 同时使用硬件层。

减少过度绘制

  • 移除不必要的背景:getWindow().setBackgroundDrawable(null) 或者在 theme 中设置 windowBackground 为 null。
  • 使用 clipRect 限制绘制区域。
  • 避免重叠的不透明 View 都设置背景。

使用 ViewStub 延迟加载

对于不是立即需要的 View,使用 ViewStub 进行延迟加载:

<ViewStub
android:id="@+id/stub_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout="@layout/layout_error" />

完整示例一:渐变环形进度指示器

以下是一个完整的自定义圆形进度 View 实现,包含渐变效果、文字显示、动画支持:

package com.example.uipractice;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.*;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.DecelerateInterpolator;

public class CircleProgressView extends View {

// 属性
private float progress = 0f;
private float maxProgress = 100f;
private int progressColor = Color.BLUE;
private int progressBgColor = Color.LTGRAY;
private float progressWidth = 4f; // dp
private float startAngle = -90f;
private boolean showText = true;
private float textSize = 14f; // sp
private int textColor = Color.BLACK;
private int gradientEndColor = Color.BLUE;

// 预创建对象,避免 onDraw 中分配
private Paint mBgPaint;
private Paint mProgressPaint;
private Paint mTextPaint;
private RectF mOval;
private Shader mShader;
private float mDensity;

public CircleProgressView(Context context) {
this(context, null);
}

public CircleProgressView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public CircleProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDensity = context.getResources().getDisplayMetrics().density;

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView);
progress = ta.getFloat(R.styleable.CircleProgressView_progress, 0f);
maxProgress = ta.getFloat(R.styleable.CircleProgressView_maxProgress, 100f);
progressColor = ta.getColor(R.styleable.CircleProgressView_progressColor, Color.BLUE);
progressBgColor = ta.getColor(R.styleable.CircleProgressView_progressBgColor, Color.LTGRAY);
progressWidth = ta.getDimension(R.styleable.CircleProgressView_progressWidth, mDensity * 4);
startAngle = ta.getFloat(R.styleable.CircleProgressView_startAngle, -90f);
showText = ta.getBoolean(R.styleable.CircleProgressView_showText, true);
textSize = ta.getDimension(R.styleable.CircleProgressView_textSize, mDensity * 14);
textColor = ta.getColor(R.styleable.CircleProgressView_textColor, Color.BLACK);
gradientEndColor = ta.getColor(R.styleable.CircleProgressView_gradientEndColor, progressColor);
ta.recycle();

init();
}

private void init() {
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBgPaint.setStyle(Paint.Style.STROKE);
mBgPaint.setStrokeWidth(progressWidth);
mBgPaint.setColor(progressBgColor);
mBgPaint.setStrokeCap(Paint.Cap.ROUND);

mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setStrokeWidth(progressWidth);
mProgressPaint.setStrokeCap(Paint.Cap.ROUND);

mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setTextSize(textSize);
mTextPaint.setColor(textColor);
mTextPaint.setTextAlign(Paint.Align.CENTER);

mOval = new RectF();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int width, height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
width = (int) (mDensity * 100); // 默认 100dp
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(width, widthSize);
}
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = (int) (mDensity * 100);
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
}
}

// 保证正方形
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 尺寸变化时,重新计算圆环区域和渐变
float padding = progressWidth / 2f;
mOval.set(padding, padding, w - padding, h - padding);

// 创建扫描渐变
int centerX = w / 2;
int centerY = h / 2;
mShader = new SweepGradient(centerX, centerY, progressColor, gradientEndColor);
mProgressPaint.setShader(mShader);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

float sweepAngle = (progress / maxProgress) * 360f;

// 绘制背景圆环
canvas.drawArc(mOval, 0, 360, false, mBgPaint);

// 绘制进度圆环(渐变效果由 mShader 提供)
canvas.drawArc(mOval, startAngle, sweepAngle, false, mProgressPaint);

// 绘制文字
if (showText) {
float percent = (progress / maxProgress) * 100;
String text = String.format("%.0f%%", percent);
Paint.FontMetrics fm = mTextPaint.getFontMetrics();
float textY = getHeight() / 2f - (fm.ascent + fm.descent) / 2f;
canvas.drawText(text, getWidth() / 2f, textY, mTextPaint);
}
}

// ------------ 公开 API ------------

public void setProgress(float progress) {
this.progress = Math.max(0, Math.min(progress, maxProgress));
invalidate();
}

public void setProgressAnimated(float targetProgress, long duration) {
ValueAnimator animator = ValueAnimator.ofFloat(progress, targetProgress);
animator.setDuration(duration);
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(animation -> {
progress = (float) animation.getAnimatedValue();
invalidate();
});
animator.start();
}

public float getProgress() { return progress; }
public void setMaxProgress(float max) { this.maxProgress = max; invalidate(); }
public void setProgressColor(int color) {
this.progressColor = color;
mShader = new SweepGradient(getWidth() / 2f, getHeight() / 2f, progressColor, gradientEndColor);
mProgressPaint.setShader(mShader);
invalidate();
}

@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("superState", super.onSaveInstanceState());
bundle.putFloat("progress", progress);
bundle.putFloat("maxProgress", maxProgress);
return bundle;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
progress = bundle.getFloat("progress");
maxProgress = bundle.getFloat("maxProgress");
state = bundle.getParcelable("superState");
}
super.onRestoreInstanceState(state);
}
}

使用方式

<com.example.uipractice.CircleProgressView
android:id="@+id/progressView"
android:layout_width="120dp"
android:layout_height="120dp"
app:progress="0"
app:maxProgress="100"
app:progressColor="#FF6B6B"
app:gradientEndColor="#FFE66D"
app:progressWidth="6dp"
app:showText="true"
app:textSize="16sp"
app:textColor="#333333" />
CircleProgressView progressView = findViewById(R.id.progressView);
progressView.setProgressAnimated(75, 1500);

完整示例二:流式标签布局(FlowLayout)

FlowLayout 是一个自动换行的 ViewGroup,适合展示标签、筛选条件等场景。这是自定义 ViewGroup 的经典案例。

package com.example.uipractice;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

public class FlowLayout extends ViewGroup {

public FlowLayout(Context context) {
this(context, null);
}

public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@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;
int lineWidth = 0;
int lineHeight = 0;

int childCount = getChildCount();

for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}

// 测量每个子 View
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

if (lineWidth + childWidth > widthSize - getPaddingLeft() - getPaddingRight()) {
// 需要换行
width = Math.max(width, lineWidth);
height += lineHeight;

lineWidth = childWidth;
lineHeight = childHeight;
} else {
// 不需要换行
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}

// 最后一个子 View
if (i == childCount - 1) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
}

// 加上 padding
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();

// 处理测量模式
if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
}
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
} else if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
}

setMeasuredDimension(width, height);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int parentWidth = r - l;
int lineWidth = 0;
int lineHeight = 0;
int top = getPaddingTop();
int left = getPaddingLeft();

int childCount = getChildCount();

for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}

MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

if (lineWidth + childWidth > parentWidth - getPaddingLeft() - getPaddingRight()) {
// 换行
top += lineHeight;
left = getPaddingLeft();
lineWidth = 0;
lineHeight = 0;
}

int childLeft = left + lp.leftMargin;
int childTop = top + lp.topMargin;
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(),
childTop + child.getMeasuredHeight());

lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
left += childWidth;
}
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
}

关键点:

  1. 必须重写 generateLayoutParams 系列方法,使其返回 MarginLayoutParams,这样子 View 的 margin 才能生效。
  2. onMeasure 中需要处理换行逻辑,计算所有行中最大的宽度和累计的高度。
  3. onLayout 中需要重新执行换行逻辑来定位子 View。注意这里不能复用 onMeasure 中的计算结果(因为 onMeasure 和 onLayout 可能被独立调用)。

总结

自定义 View 的核心在于理解三大流程:

  1. MeasureSpec 传递:父容器根据自身约束和子 View 的 LayoutParams 生成子 View 的 MeasureSpec。三种模式(EXACTLY / AT_MOST / UNSPECIFIED)需要正确理解。
  2. onMeasure:根据 MeasureSpec 计算并设置自身尺寸。对于 ViewGroup,还需要遍历子 View 触发测量。
  3. onLayout:确定子 View 的位置。只有 ViewGroup 需要实现。
  4. onDraw:使用 Canvas 和 Paint 进行内容绘制。要避免在 onDraw 中分配对象。

掌握这些基础后,结合 Canvas/Paint 的高级特性(Shader、PathEffect、Xfermode 等),可以实现几乎任何视觉效果。性能优化方面,核心原则是减少布局层级、避免过度绘制、合理使用硬件层。

关键源码文件

文件 路径
View.java frameworks/base/core/java/android/view/View.java
ViewGroup.java frameworks/base/core/java/android/view/ViewGroup.java
MeasureSpec frameworks/base/core/java/android/view/View.java (内部类)
打赏
  • 微信
  • 支付宝

评论