Android-view-绘制流程及自定义例子

Android View 绘制流程及自定义示例

📊 View 绘制流程(核心三步骤)

onMeasure() → onLayout() → onDraw()

↓ ↓ ↓

测量尺寸 确定位置 实际绘制

  1. 测量阶段 (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 系统内部使用 尺寸不受限制

  1. 布局阶段 (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; // 添加间距
}

}

  1. 绘制阶段 (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(); // 触发重绘
}

}

📈 性能优化建议

  1. 避免在 onDraw 中创建对象

// ❌ 错误:每次绘制都创建新对象

@Override

protected void onDraw(Canvas canvas) {

Paint paint = new Paint(); // 不要在这里创建!

// ...

}

// ✅ 正确:在构造函数中初始化

public MyView(Context context) {

super(context);

mPaint = new Paint(); // 只创建一次

mPaint.setAntiAlias(true);

}

  1. 使用局部刷新

// 只刷新需要更新的区域

private void updateProgress() {

// 计算需要重绘的区域

Rect dirtyRect = new Rect(left, top, right, bottom);

复制代码
// 只刷新这个区域
invalidate(dirtyRect);

// 或者使用 postInvalidate 在非UI线程调用
// postInvalidate(left, top, right, bottom);

}

  1. 使用硬件加速

// 代码中设置

setLayerType(View.LAYER_TYPE_HARDWARE, null);

  1. 减少过度绘制

// 使用 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()

🎯 最佳实践

  1. 尽量使用组合控件而不是完全自定义

  2. 属性化配置:通过 XML 属性让 View 可配置

  3. 支持 padding:在 onDraw 中考虑 padding

  4. 处理 wrap_content:在 onMeasure 中提供默认尺寸

  5. 添加预览支持:使用 @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 绘制流程的绝佳实践案例。

相关推荐
常利兵3 小时前
Android “解锁”屏幕方向:APP适配新征程
android·gitee
红藕香残玉簟秋5 小时前
【安卓学习】配置开发环境
android·学习
用户69371750013845 小时前
Android R8 深度解析:为什么 Google 用R8取代 ProGuard?
android·android studio·android jetpack
seabirdssss6 小时前
联想拯救者Y7000P上使用ADB无法监听到通过USB连接的安卓设备
android·adb
2501_916008896 小时前
iPhone 上怎么抓 App 的网络请求,在 iOS 设备上捕获网络请求
android·网络·ios·小程序·uni-app·iphone·webview
工业甲酰苯胺6 小时前
PHP闭包中static关键字的核心作用与底层原理解析
android·开发语言·php
Kapaseker7 小时前
解析 Compose 的核心概念 remember
android·kotlin
秋知叶i8 小时前
【Android Studio】Kotlin 第一个 App Hello World 创建与运行|超详细入门
android·kotlin·android studio