Skip to content
Go back

理解 Android 自定义 View 的测量与绘制

最近在写自定义 View 时,经常会遇到一个问题:明明 Canvas 上的绘制逻辑没有错,控件放进布局以后却大小不对,甚至完全看不见。

后来才慢慢理解,自定义 View 并不是直接从 onDraw() 开始的。系统要先知道它需要多大空间,再确定最终尺寸,最后才轮到我们真正把内容画出来。

三个比较重要的方法

一个简单的自定义 View,最先需要认识下面三个方法:

其中最容易忽略的是 onMeasure()。如果 View 直接继承自 View,又希望支持 wrap_content,就不能完全依赖父类的默认处理,否则它可能无法得到我们期望的大小。

MeasureSpec 是什么

父布局不会简单地告诉子 View “你随便多大都可以”,而是通过 MeasureSpec 把限制传下来。它包含尺寸和模式:

例如,一个只需要画圆点的 View,可以在 wrap_content 时提供一个默认尺寸:

public class DotView extends View {    private static final int DEFAULT_SIZE_DP = 48;    private Paint mPaint;    private float mRadius;    public DotView(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    private void init() {        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);        mPaint.setColor(Color.parseColor("#4CAF50"));    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int desiredSize = dpToPx(DEFAULT_SIZE_DP);        int width = resolveSize(desiredSize, widthMeasureSpec);        int height = resolveSize(desiredSize, heightMeasureSpec);        setMeasuredDimension(width, height);    }    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        mRadius = Math.min(w, h) / 2f;    }    @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, mRadius, mPaint);    }    private int dpToPx(int dp) {        return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);    }}

这里使用 resolveSize() 的好处是:当外部明确指定尺寸时尊重布局约束;当外部使用 wrap_content 时,View 仍然有自己的合理默认大小。

不要在 onDraw 里反复创建对象

刚开始写控件时,很容易在 onDraw() 里创建 PaintRectF 或临时数组。虽然效果能够画出来,但 onDraw() 在动画或滑动时会被频繁调用,反复分配对象会增加不必要的开销。

比较合适的方式是:

这也是为什么圆环、图表或者自定义 loading 控件通常都会把尺寸相关计算和真正绘制拆开。

invalidate 与 requestLayout 的区别

修改 View 状态以后,常常会遇到两个方法:

例如修改圆点颜色,只需要调用 invalidate();如果修改了控件希望展示的直径,则需要重新测量,调用 requestLayout() 更合理。

小结

自定义 View 的关键不只是会使用 Canvas.drawCircle()Canvas.drawArc(),还要理解尺寸是如何从父布局一路传下来的。

先正确处理测量,再在尺寸确定后准备绘制数据,最后让 onDraw() 保持简单,这样写出来的控件才更容易复用,也更不容易在不同布局中出现奇怪的问题。


Share this post on: