自定义 View 的三种方式
系统控件不够用时才自定义 View。三种做法:继承 View 重写 onDraw 画内容;继承已有控件(如 TextView)在它的基础上叠加绘制;组合多个控件包成一个自定义组件。这篇讲第一种,也是最自由的一种。
自定义 View 的舞台是 onDraw,画笔是 Paint,画布是 Canvas。
新建类继承 View,重写 onDraw,用 Canvas 画圆、画线、画文字,再在布局里用包名引用。绘制是每帧全量重画的,性能瓶颈往往在 onDraw 里干了太多活,所以绘制代码要轻、要能复用。
onMeasure 与测量
View 被放进去之前先要量尺寸。onMeasure 收到父布局的约束(宽高模式),自己决定最终尺寸。不重写的话用默认的测量,wrap_content 会变成撑满,这是新手最常见的坑。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w = MeasureSpec.getSize(widthMeasureSpec);
int h = MeasureSpec.getSize(heightMeasureSpec);
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
w = Math.min(w, dp(160)); // wrap_content 时给个合理值
}
setMeasuredDimension(w, h);
}
- EXACTLY父布局定死了。
- AT_MOST上限约束,wrap_content。
- UNSPECIFIED没有约束,少见。
Canvas 基础绘制
onDraw 里 Canvas 提供全套绘制命令:画圆、画弧、画线、画路径、画文字。坐标系原点在左上角,单位是像素,注意适配屏幕密度。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(0xFF3B82F6);
canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, dp(60), paint);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(dp(6));
canvas.drawArc(box, 0, 270, false, paint);
}
绘制顺序决定遮挡关系,后画的盖住先画的。把 Paint 对象建一次放在字段里,别在 onDraw 里 new,否则每帧都在创建对象,卡顿随之而来。
Paint 高级用法
Paint 不只是颜色和粗细:线性渐变 Shader、阴影 setShadowLayer、圆角 Path、抗锯齿都能让绘制细腻起来。进度条、仪表盘这类控件全靠这些组合。
paint.setShader(new LinearGradient(0, 0, getWidth(), 0,
new int[]{0xFF3B82F6, 0xFF06B6D4}, null, Shader.TileMode.CLAMP));
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setShadowLayer(dp(8), 0, 0, 0x553B82F6);
渐变、阴影用多了会掉帧,尤其阴影在低端机上很贵。能用实色表达就别开阴影,开了也控制范围。复杂图形拆成 Path 一次绘制,比几十条 drawLine 高效得多。
触摸事件与动画
自定义 View 的交互靠 onTouchEvent:ACTION_DOWN 记录起点,ACTION_MOVE 更新状态并 invalidate 重绘,ACTION_UP 结束手势。配合属性动画可以让 View 平滑变化。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: press = true; break;
case MotionEvent.ACTION_UP: press = false; break;
}
invalidate(); // 请求重绘
return true;
}
连续触摸时只 invalidate 局部区域(invalidate 传矩形),比整块重绘省很多。动画用 ValueAnimator 驱动进度字段,invalidate 逐帧刷新,别在 onDraw 里写循环。
实战:画一个进度圆环
把前面学的合起来做一个环形进度条:底环灰色,进度弧蓝色渐变,中间文字显示百分比,触摸可以调节进度。
@Override
protected void onDraw(Canvas canvas) {
canvas.drawArc(box, -90, 360, false, trackPaint);
canvas.drawArc(box, -90, progress * 3.6f, false, progressPaint);
canvas.drawText(Math.round(progress) + "%", cx, cy + dp(12), textPaint);
}
onTouchEvent 里根据触摸点算角度更新 progress,再 invalidate。跑起来看效果:渐变进度、圆角端点、触摸跟手,一个生产级的环形进度条就完成了。
小结
自定义 View 四步走:onMeasure 定尺寸、onDraw 画内容、onTouchEvent 接交互、动画驱动刷新。Paint 常驻字段、绘制保持轻量、渐变阴影克制使用,再复杂的控件也能稳稳跑起来。画得出来,就是你的独家控件。