Android View 绘制流程及自定义示例
📊 View 绘制流程(核心三步骤)
onMeasure() → onLayout() → onDraw()
↓ ↓ ↓
测量尺寸 确定位置 实际绘制
- 测量阶段 (Measure)
目的:计算 View 需要的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// widthMeasureSpec 包含两部分:模式 + 尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width, height;
// 处理宽度
if (widthMode == MeasureSpec.EXACTLY) {
// 精确值:match_parent 或具体数值
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
// 最大值:wrap_content
width = Math.min(getSuggestedWidth(), widthSize);
} else { // UNSPECIFIED
width = getSuggestedWidth();
}
// 处理高度(类似逻辑)
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(getSuggestedHeight(), heightSize);
} else {
height = getSuggestedHeight();
}
// 必须调用!
setMeasuredDimension(width, height);
}
三种测量模式:
模式 XML 对应 说明
MeasureSpec.EXACTLY match_parent 或具体数值 精确尺寸
MeasureSpec.AT_MOST wrap_content 最大不超过父容器限制
MeasureSpec.UNSPECIFIED 系统内部使用 尺寸不受限制
- 布局阶段 (Layout) - 仅 ViewGroup 需要
目的:确定子 View 的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// l,t,r,b: View 相对于父容器的位置
int childCount = getChildCount();
int left = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// 测量子 View
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 水平排列子 View
child.layout(
left, // 子 View 左边位置
0, // 子 View 顶部位置
left + childWidth, // 子 View 右边位置
childHeight // 子 View 底部位置
);
left += childWidth + 20; // 添加间距
}
}
- 绘制阶段 (Draw)
目的:实际绘制内容
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Canvas 是画布,Paint 是画笔
Paint paint = new Paint();
paint.setAntiAlias(true); // 抗锯齿
paint.setStyle(Paint.Style.FILL); // 填充模式
// 各种绘制方法
canvas.drawColor(Color.WHITE); // 背景色
canvas.drawRect(rect, paint); // 矩形
canvas.drawCircle(x, y, r, paint); // 圆形
canvas.drawPath(path, paint); // 路径
canvas.drawText(text, x, y, paint); // 文字
canvas.drawBitmap(bitmap, x, y, paint); // 图片
}
🎨 完整自定义示例:可点击的评分星星
Step 1: 创建自定义属性 (res/values/attrs.xml)
<?xml version="1.0" encoding="utf-8"?>
Step 2: 实现 StarRatingView 类
public class StarRatingView extends View {
// 画笔
private Paint mStarPaint;
private Paint mEmptyStarPaint;
// 属性
private int mStarCount = 5; // 星星总数
private float mRating = 0.0f; // 当前评分 0-5
private int mStarColor = Color.YELLOW;
private int mEmptyStarColor = Color.LTGRAY;
private float mStarSpacing = 20f;
// 路径
private Path mStarPath;
private RectF mStarBounds = new RectF();
public StarRatingView(Context context) {
this(context, null);
}
public StarRatingView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// 读取自定义属性
if (attrs != null) {
TypedArray ta = context.obtainStyledAttributes(
attrs, R.styleable.StarRatingView
);
mStarCount = ta.getInt(R.styleable.StarRatingView_starCount, 5);
mRating = ta.getFloat(R.styleable.StarRatingView_rating, 0f);
mStarColor = ta.getColor(
R.styleable.StarRatingView_starColor,
Color.YELLOW
);
mEmptyStarColor = ta.getColor(
R.styleable.StarRatingView_emptyStarColor,
Color.LTGRAY
);
mStarSpacing = ta.getDimension(
R.styleable.StarRatingView_starSpacing,
20f
);
ta.recycle();
}
// 初始化实心星星画笔
mStarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mStarPaint.setColor(mStarColor);
mStarPaint.setStyle(Paint.Style.FILL);
// 初始化空心星星画笔
mEmptyStarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mEmptyStarPaint.setColor(mEmptyStarColor);
mEmptyStarPaint.setStyle(Paint.Style.STROKE);
mEmptyStarPaint.setStrokeWidth(2);
// 创建五角星路径
mStarPath = createStarPath();
}
// 创建五角星路径
private Path createStarPath() {
Path path = new Path();
// 五角星外接圆半径
float radius = 1.0f;
// 内接圆半径
float innerRadius = radius * 0.382f;
// 五角星的10个顶点
for (int i = 0; i < 10; i++) {
float r = (i % 2 == 0) ? radius : innerRadius;
float angle = (float) (Math.PI / 5 * i - Math.PI / 2);
float x = (float) (r * Math.cos(angle));
float y = (float) (r * Math.sin(angle));
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
path.close();
return path;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 计算期望的大小
int desiredWidth = (int) (mStarCount * 100 + (mStarCount - 1) * mStarSpacing);
int desiredHeight = 100;
// 处理测量规格
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width, height;
// 处理宽度
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
// 处理高度
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightSize);
} else {
height = desiredHeight;
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
// 单颗星星的宽度
float starWidth = (width - (mStarCount - 1) * mStarSpacing) / mStarCount;
float starSize = Math.min(starWidth, height) * 0.8f;
// 保存画布状态
canvas.save();
// 计算开始绘制的位置
float startX = (width - (mStarCount * starWidth + (mStarCount - 1) * mStarSpacing)) / 2;
canvas.translate(startX, height / 2);
for (int i = 0; i < mStarCount; i++) {
// 保存当前画布状态
canvas.save();
// 缩放路径到合适大小
mStarBounds.set(-starSize/2, -starSize/2, starSize/2, starSize/2);
Matrix matrix = new Matrix();
matrix.setRectToRect(
new RectF(-1, -1, 1, 1),
mStarBounds,
Matrix.ScaleToFit.CENTER
);
Path currentPath = new Path(mStarPath);
currentPath.transform(matrix);
// 判断是绘制实心还是空心星星
if (i < Math.floor(mRating)) {
// 完整实心星星
canvas.drawPath(currentPath, mStarPaint);
} else if (i < Math.ceil(mRating)) {
// 部分填充的星星(评分有小数)
float fraction = mRating - i;
// 绘制空心星星背景
canvas.drawPath(currentPath, mEmptyStarPaint);
// 绘制实心部分
canvas.save();
canvas.clipRect(0, 0, starSize * fraction, starSize);
canvas.drawPath(currentPath, mStarPaint);
canvas.restore();
} else {
// 空心星星
canvas.drawPath(currentPath, mEmptyStarPaint);
}
// 恢复画布并移动到下一个位置
canvas.restore();
canvas.translate(starWidth + mStarSpacing, 0);
}
canvas.restore();
}
// Step 3: 添加触摸事件处理
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN ||
event.getAction() == MotionEvent.ACTION_MOVE) {
float x = event.getX();
float starWidth = getWidth() / mStarCount;
// 计算点击的是第几颗星
int clickedStar = (int) (x / starWidth) + 1;
// 设置评分
setRating(Math.min(Math.max(clickedStar, 0), mStarCount));
// 触发点击监听
if (mOnRatingChangeListener != null) {
mOnRatingChangeListener.onRatingChanged(mRating);
}
return true;
}
return super.onTouchEvent(event);
}
// Step 4: 提供公共方法
public void setRating(float rating) {
mRating = Math.min(Math.max(rating, 0), mStarCount);
invalidate(); // 重绘
}
public float getRating() {
return mRating;
}
public void setStarCount(int count) {
mStarCount = count;
requestLayout(); // 重新测量布局
}
public int getStarCount() {
return mStarCount;
}
// Step 5: 评分变化监听接口
public interface OnRatingChangeListener {
void onRatingChanged(float rating);
}
private OnRatingChangeListener mOnRatingChangeListener;
public void setOnRatingChangeListener(OnRatingChangeListener listener) {
mOnRatingChangeListener = listener;
}
}
Step 6: 在布局中使用
<com.example.myapp.StarRatingView
android:id="@+id/starRatingView"
android:layout_width="300dp"
android:layout_height="50dp"
android:layout_margin="20dp"
app:starCount="5"
app:rating="3.5"
app:starColor="#FFD700"
app:emptyStarColor="#C0C0C0"
app:starSpacing="10dp" />
Step 7: 在 Activity 中使用
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
StarRatingView starView = findViewById(R.id.starRatingView);
// 设置初始评分
starView.setRating(3.5f);
// 监听评分变化
starView.setOnRatingChangeListener(new StarRatingView.OnRatingChangeListener() {
@Override
public void onRatingChanged(float rating) {
Toast.makeText(
MainActivity.this,
"评分: " + rating,
Toast.LENGTH_SHORT
).show();
}
});
// 通过代码修改属性
starView.setStarCount(7);
starView.setRating(4.2f);
}
}
🔧 另一个示例:自定义进度条
public class CustomProgressBar extends View {
private Paint mBgPaint, mProgressPaint, mTextPaint;
private float mProgress = 0.5f; // 0-1
private RectF mBgRect = new RectF();
public CustomProgressBar(Context context) {
super(context);
init();
}
private void init() {
// 背景画笔
mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBgPaint.setColor(Color.LTGRAY);
mBgPaint.setStyle(Paint.Style.FILL);
// 进度画笔
mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mProgressPaint.setColor(Color.BLUE);
mProgressPaint.setStyle(Paint.Style.FILL);
// 文字画笔
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(40);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
int width = getWidth();
int height = getHeight();
int cornerRadius = height / 2;
// 绘制背景
mBgRect.set(0, 0, width, height);
canvas.drawRoundRect(mBgRect, cornerRadius, cornerRadius, mBgPaint);
// 绘制进度
float progressWidth = width * mProgress;
mBgRect.set(0, 0, progressWidth, height);
canvas.drawRoundRect(mBgRect, cornerRadius, cornerRadius, mProgressPaint);
// 绘制进度文字
String text = (int)(mProgress * 100) + "%";
float textY = height / 2f - (mTextPaint.descent() + mTextPaint.ascent()) / 2;
canvas.drawText(text, width / 2f, textY, mTextPaint);
}
public void setProgress(float progress) {
mProgress = Math.max(0, Math.min(1, progress));
invalidate(); // 触发重绘
}
}
📈 性能优化建议
- 避免在 onDraw 中创建对象
// ❌ 错误:每次绘制都创建新对象
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint(); // 不要在这里创建!
// ...
}
// ✅ 正确:在构造函数中初始化
public MyView(Context context) {
super(context);
mPaint = new Paint(); // 只创建一次
mPaint.setAntiAlias(true);
}
- 使用局部刷新
// 只刷新需要更新的区域
private void updateProgress() {
// 计算需要重绘的区域
Rect dirtyRect = new Rect(left, top, right, bottom);
// 只刷新这个区域
invalidate(dirtyRect);
// 或者使用 postInvalidate 在非UI线程调用
// postInvalidate(left, top, right, bottom);
}
- 使用硬件加速
// 代码中设置
setLayerType(View.LAYER_TYPE_HARDWARE, null);
- 减少过度绘制
// 使用 canvas.clipRect 限制绘制区域
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
canvas.clipRect(0, 0, 200, 200); // 只在这个区域绘制
// 绘制内容
canvas.restore();
}
📊 绘制流程总结
方法 调用次数 主要任务 注意事项
onMeasure 多次 测量尺寸 必须调用 setMeasuredDimension()
onLayout 多次 布局子View 仅 ViewGroup 需要
onDraw 多次 绘制内容 避免创建新对象,注意性能
invalidate 手动调用 触发重绘 会调用 onDraw()
requestLayout 手动调用 重新测量布局 会调用 onMeasure() 和 onLayout()
🎯 最佳实践
-
尽量使用组合控件而不是完全自定义
-
属性化配置:通过 XML 属性让 View 可配置
-
支持 padding:在 onDraw 中考虑 padding
-
处理 wrap_content:在 onMeasure 中提供默认尺寸
-
添加预览支持:使用 @Preview 注解或 isInEditMode()
@Override
protected void onDraw(Canvas canvas) {
// 考虑 padding
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
int contentWidth = getWidth() - paddingLeft - paddingRight;
int contentHeight = getHeight() - paddingTop - paddingBottom;
// 在 content 区域内绘制
canvas.translate(paddingLeft, paddingTop);
// ... 绘制代码
}
这个完整的评分星星示例展示了自定义 View 的全过程:从测量、布局、绘制到交互处理,是理解 Android View 绘制流程的绝佳实践案例。