虽然我们在开发中基本可以用 Android 自带的各种控件实现绝大多数的功能,但难以避免还是有一些需求是自带的控件无法实现的。这个时候我们通常会想到去 Github 上寻找开源控件,但有的东西是有成熟的实现如:ViewPager 的 Indicator。而有的就没那么容易找到了。
还有就是虽然我们平时的一些需求可以使用图片资源代替,但过多的图片资源不仅会使得应用体积增大,还会使得加载的过程中消耗不少的系统资源(内存以及 CPU)—— 我曾经就这么干过,至少这种方法做东西很快(但也很坑)。
这个时候我们就应该想到自定义 View 了,下面就讲讲我在学习自定义 View 的一些心得体会吧。
View绘制流程
View 的绘制是从 ViewRoot 的performTraversals()
方法开始的,其执行过程可简单概括为根据之前所有设置好的状态,判断是否需要计算视图大小(measure)、是否需要重新安置视图的位置(layout),以及是否需要重绘(draw)视图,其流程图如下所示:

而我们今天讲的自定义 View 的绘制,主要就是在是否需要重新 draw 这一步来实现。
三个绘图工具类简介
要在自定义 View 中进行重新绘制,我们首先需要了解一下 Android 中的三个重要的绘图工具类,它们就是Paint
(画笔)、Canvas
(画布)以及Path
(路径)。当然其实不仅仅只有这三个可以作用于画图和图像处理,但它们是最基础的。
Paint
Paint 就是画笔,在 Android 图形绘制的时候,我们就好像真的有一个人拿着画笔把图像画出来一样,所以画笔这个类也给了我们和现实世界作画的时候一样的一些设定。
我们可以通过 Paint 来设定线宽(就像现实中画笔的粗细)、颜色(颜料)、透明度以及填充风格等。
我们可以通过它的构造函数来新建一个画笔
1
| Paint paint = new Paint();
|
然后对它进行一些设定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| paint.setARGB(255, 255, 0, 0); paint.setAlpha(0); paint.setColor(getResources().getColor(android.R.color.black)); paint.setAntiAlias(true); paint.setDither(true); paint.setFilterBitmap(true); paint.setMaskFilter(maskFilter); paint.setColorFilter(colorFilter); paint.setPathEffect(pathEffect); paint.setShader(shader); paint.setShadowLayer(2, 2, 2, Color.GRAY); paint.setStyle(Paint.Style.FILL_AND_STROKE); paint.setStrokeCap(Paint.Cap.SQUARE); paint.setStrokeJoin(Paint.Join.MITER); paint.setStrokeWidth(2); paint.setXfermode(xfermode); paint.setFakeBoldText(true); paint.setSubpixelText(true); paint.setTextAlign(Paint.Align.CENTER); paint.setTextScaleX(0.5); paint.setTextSize(40); paint.setTextSkewX(30); paint.setTypeface(Typeface.SANS_SERIF); paint.setUnderlineText(true); paint.setStrikeThruText(true); paint.setStrokeJoin(Paint.Join.ROUND); paint.setStrokeMiter(30); paint.setStrokeCap(Paint.Cap.ROUND); paint.ascent(); paint.descent(); paint.clearShadowLayer();
|
但我们光有画笔还是不够的,我们至少还需要画布(Canvas)才可以真正开始作画呢。
Canvas
Canvas 就是画布,我们有了画笔和画布就可以开始作画(图形绘制)了。
我们有两种创建 Canvas 的方法:
1 2
| Canvas canvas = new Canvas(); Canvas canvasByBitmap = new Canvas(bitmap);
|
其中传入 Bitmap 的方法会将 Bitmap 作为画布的背景。
下面是常用的drawXXX()
方法,它们被用于绘制不同的图形
1 2 3 4 5 6 7 8 9 10
| canvas.drawRect(new RectF(0, 0, 100, 100), mPaint); canvas.drawRect(0, 0, 100, 100, mPaint); canvas.drawPath(path, paint); canvas.drawBitmap(bitmap, src, dst, mPaint); canvas.drawLine(0, 0, 100, 100, mPaint); canvas.drawPoint(100, 20, mPaint); canvas.drawText("这是一段文字", 0, 0, mPaint); canvas.drawOval(new RectF(0, 0, 100, 200), mPaint); canvas.drawCircle(300, 300, 100, mPaint); canvas.drawArc(new RectF(0, 0, 100, 100), 0, 30, true, mPaint);
|
还有clipXXX()
方法,它们是裁剪一块新的区域用于绘图,这里就不详细说明了。
save()
和restore()
方法用来保存和恢复 Canvas 的状态,简单而言就是一个存档,一个恢复存档。
还有就是三个变换方法:translate
(平移)、scale
(缩放)以及rotate
(旋转)了,它们可以控制画布的一些动作,就好像我们真实世界中作画的时候对画布的一些动作一样(除了缩放,2333)。
Path
其实在有了上面两个类之后我们就已经可以开始绘制了,但还是先把 Path 也介绍完毕之后再开始真实案例吧。
Path 就是路径,有点像我们在初中数学中学习函数的时候,可以根据几个点确认画出一个函数的图形。
下面是一些常用的方法:
1 2 3 4 5 6 7 8 9 10 11 12
| path.addArc(new RectF(0, 0, 100, 100), 0, 30); path.addCircle(300, 300, 100, Path.Direction.CW); path.addOval(rectF, Path.Direction.CCW); path.addRect(rectF, Path.Direction.CW); path.addRoundRect(rectF, {5, 5, 5, 5}, path.Direction.CW); path.isEmpty(); path.transform(matrix); path.moveTo(100, 100); path.lineTo(300, 300); path.quadTo(x1, y1, x2, y2); path.rCubicTo(x1, y1, x2, y2, x3, y3); path.arcTo(rectF, 0, 50);
|
开始绘制
介绍完了三个绘制 UI 的基础类,那么我们现在来动手试试吧。难度从低到高,循序渐进完成自定义 View 中复杂图形的绘制。
我们自定义一个 View 并且要重新绘制的话,我们只需要新建一个类继承 View 并且实现onDraw(Canvas canvas)
即可,View 会调用子类实现的onDraw
完成绘制。
那么我们接下来的示例就只列出onDraw
方法和对应的效果图了。
简单图形
矩形
1 2 3 4 5 6
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawRect(0, 0, 100, 200, mPaint); }
|

线段
1 2 3 4 5
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawLine(0, 0, 100, 200, mPaint); }
|

圆形
1 2 3 4 5
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawCircle(100, 100, 100, mPaint); }
|

画布底色
1 2 3 4 5
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(getResources().getColor(android.R.color.darker_gray)); }
|

复杂图形
刻度尺
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.translate(0, 50);
for (int i = 0; i <= 100; i++) { if (i % 10 == 0) { canvas.drawLine(0, 0, 70, 0, mPaint); canvas.drawText(String.format(Locale.CHINESE, "%d", i / 10), 100, 10, mPaint); } else if (i % 5 == 0) { canvas.drawLine(0, 0, 40, 0, mPaint); } else { canvas.drawLine(0, 0, 30, 0, mPaint); } canvas.translate(0, 15); } }
|

手表表盘
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawCircle(400, 400, 400, mPaint);
canvas.drawLine(400, 400, 400, 200, mPaint); canvas.drawLine(400, 400, 550, 400, mPaint);
for (int i = 0; i < 12; i++) { canvas.drawLine(400, 0, 400, 10, mPaint); canvas.drawText(String.format(Locale.CHINESE, "%d", i == 0 ? 12 : i), 400, 100, mTextPaint); canvas.rotate(30, 400, 400); } }
|

总结
其实 Android 中的图形绘制基本就是靠这三个类扩展变化而来,掌握了它们的使用方式我们也就可以定义各种各样的好看的自定义控件了。
那么我们掌握了绘制之后,我们还要考虑的就是自定义 View 的测量了,我会在之后再写一篇博文来总结我学习自定义 View 的测量的一些经验,感谢观看(虽然并不会有多少人看……)。