你有没有试过在纸上画一个仪表盘?先拿圆规画个圆,再标上刻度,最后画根指针。自定义View就像这个过程——系统提供了画布和画笔,但具体画什么、怎么画,全凭你自己。
1. 测量尺寸?先量好画纸有多大
比喻:你拿到一张白纸,准备画仪表盘。但纸可能被折过、剪过,你得先知道有效画布有多大。如果纸太小,圆画不完整;纸太大,画面又太空。
技术点:自定义View的第一步是onMeasure()。系统会问:“你要占多大地方?”你需要测量自己的宽高,然后告诉系统。比如你希望仪表盘是正方形,那就在onMeasure()里根据父布局给的建议尺寸(MeasureSpec),算出最终宽高,调用setMeasuredDimension()定下来。说白了,就是告诉Android:“我就要这么大,多了我不要,少了会变形。”
至于onLayout(),那是ViewGroup安排子View坐标的活儿,咱们画单个仪表盘用不上——就像你只在一张纸上画一个圆,不需要考虑圆内部还有别的小图形怎么摆。
2. 绘制内容?拿起画笔开始涂鸦
比喻:尺寸定好了,现在你真要画了。先画个灰色的圆盘,再画刻度线——短的、长的,还要写上数字。最后用红色画一根指针,指向某个角度。每一步都是一笔一笔画上去的。
技术点:onDraw(Canvas canvas)就是你的手,Paint就是你的笔。
下面是一个极简的指针绘制片段(感受一下旋转画布的用法):
canvas.save(); // 保存当前画布状态
canvas.rotate(angle, centerX, centerY); // 旋转画布到指针角度
canvas.drawLine(centerX, centerY - radius,
centerX, centerY, paint); // 画指针
canvas.restore(); // 恢复画布,不影响画刻度
canvas.drawCircle()画圆盘canvas.drawLine()画刻度线(循环60次,每6°一条短刻;再循环12次,每30°一条长刻)canvas.drawText()写数字
记住:绘制的顺序就是覆盖的顺序——先画的在底层,后画的在上面。
3. 自定义属性?给仪表盘换个颜色
比喻:你画了好几个仪表盘,有的想要蓝色背景,有的想要红色指针。难道每次都改代码?不,你可以提前留好“可调参数”,比如“盘面颜色”、“指针颜色”、“最大刻度值”。使用时像填空一样填进去就行。
技术点:在res/values/attrs.xml里声明自定义属性(如circleColor、pointerColor),然后在View的构造方法里通过obtainStyledAttributes()读取XML里填的值。注意:用完要调用recycle()回收,否则会内存泄漏——就像借了画笔要记得还。这样你在布局文件里写app:circleColor="#FF0000"就能直接换颜色。
4. 触摸交互?让指针跟着手指转
比喻:你画了一个真正的仪表盘,现在用户想用手拨动指针。你得感应手指的位置,算出角度,然后重新画一遍——看起来指针就转过来了。
技术点:重写onTouchEvent(),拿到手指坐标(x, y),用Math.atan2()计算相对于View中心的夹角,转换成0~360度。然后调用invalidate()触发重绘,onDraw里根据新角度画指针。
注意:invalidate()只会重绘View自身区域,但onDraw里的计算逻辑(比如三角函数、Path运算)仍然消耗CPU。如果手指滑动时一帧触发60次重绘,且onDraw里有复杂运算,照样会卡顿。优化建议:用postInvalidateDelayed(16)限制刷新率,或者把刻度盘等静态元素提前画成Bitmap复用。说白了,不要以为重绘就是免费的。
总结表格
| 环节 | 生活比喻 | 技术方法 | 核心作用 |
|---|---|---|---|
| 测量尺寸 | 量画纸大小 | onMeasure() | 确定View宽高 |
| 绘制内容 | 拿画笔涂鸦 | onDraw(Canvas) | 画出形状、文字、图片 |
| 自定义属性 | 预留可调参数 | attrs.xml + 构造方法读取 | 布局时动态配置样式 |
| 触摸交互 | 手指拨指针 | onTouchEvent() + invalidate() | 响应手势,更新界面 |
面试官爱怎么问?
问:自定义View有三个关键方法,分别是什么?
答:onMeasure()(测量尺寸)、onLayout()(确定位置,ViewGroup才需要)、onDraw()(绘制内容)。就像画画的三步:先量纸有多大,再摆放在哪,最后动笔画。
问:为什么自定义View的onDraw里不要用new对象?
答:因为onDraw会频繁调用(比如手指滑动时一帧60次),每次new对象就像画画前重新磨墨——不仅慢,还会触发大量GC导致掉帧。应该把Paint、Path这些对象提前创建好,在onDraw里复用。
问(扩展):onMeasure()里的MeasureSpec三种模式怎么理解?
答:EXACTLY是“纸就这么大,必须画满”;AT_MOST是“纸最大这么大,你可以画小点”;UNSPECIFIED是“你想画多大就多大,不限制”。就像老师给画纸:第一种指定A4纸,第二种说“不能超过A4”,第三种说“给你一卷无限长的纸”。
人话总结
自定义View就是把Canvas当画板,Paint当画笔,onDraw当你的右手——你画什么,用户就看到什么。但记住,画得别太费劲,否则会手抖(掉帧)。
汇总导航