Android绘图那些事——Canvas,Matrix,Shader

简介

Android自己的SDK提供了2D图形处理相关的API,大部分都在android.graphics和android.graphics.drawable包中。
绘制2D图形,有两种方式:

  1. 在layout中定义图形和动画,主要用于绘制静态图形和预先定义好的动画。
  2. 直接在Canvas上绘制,一般用于需要重绘实现效果的View。

根据官方文档,绘画需要4个元素协同完成

To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect, Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).

  1. 位图:Bitmap来存储像素
  2. 画布:Canvas来响应画画(draw)的行为并将其写入Bitmap
  3. “颜料”:drawing primitive,比如矩形、路径、文字、位图等元素
  4. 画笔:Paint描述画画(draw)的颜色和样式等

画笔Paint

Canvas.drawXXX()方法里面都有一个Paint参数,这个就是常说的画笔。通过这个画笔,就可以在画布上画画了。
Paint类(android.graphics.Paint)主要用于设置绘图风格,包括画笔颜色、粗细、填充风格等。
下面列举了Paint常见的设置绘图风格的方法:

  • setARGB(int a,int r,int g,int b):设置ARGB颜色。
  • setColor(int color):设置颜色。
  • setAlpha(int a):设置透明度。
  • setPathEffect(PathEffect effect):设置绘制路径时的路径效果。
  • setShader(Shader shader):设置Paint的填充效果。
  • setAntiAlias(boolean aa):设置是否抗锯齿。
  • setStrokeWidth(float width):设置Paint的笔触宽度。
  • setStyle(Paint.Style style):设置Paint的填充风格。
  • setTextSize(float textSize):设置绘制文本时的文字大小。

画布Canvas

我们要实现自定义的图形绘制或者动画效果,就必须通过Canvas。通过Canvas,我们所有draw的效果都会被执行到一个Bitmap上面,而这个Bitmap就会显示到手机上,被我们看到。
所以说,Canvas只是提供绘图的API,真正的内存是下面的Bitmap。
android.graphics.Canvas 类提供了很多“画“的方法,让这块画布具有了丰富多彩的画画能力。比如:画点、线、矩形、椭圆、圆、文字等等。
相应的方法就是各种drawXXX(),常见的有以下几种:

  • void drawBitmap(Bitmap bitmap,float left,float top,Paint paint):在指定坐标绘制位图。
  • void drawLine(float startX,float startY,float stopX,float stopY,Paint paint):根据给定的起始点和结束点之间绘制连线。
  • void drawPath(Path path,Paint paint):根据给定的path,绘制连线。
  • void drawPoint(float x,float y,Paint paint):根据给定的坐标,绘制点。
  • void drawText(String text,int start,int end,Paint paint):根据给定的坐标,绘制文字。

其他一些我们在绘制时需要使用到的类一般都有draw方法。
比如我们想把一个Drawable放到Canvas里面时,Drawable类就有一个Canvas为参数的draw()方法。

获取Canvas

Canvas常用的获取方式有两种:

  1. 重写View.onDraw()方法,View中的Canvas对象会被当做参数传递过来。
  2. 自己新建Canvas。
重写View.onDraw()

操作onDraw中传入的Canvas对象,View就会表现出相应效果。
这种方式常用在自定义组件中。
新建一个View的子类,然后重写onDraw()方法,让绘制的步骤在onDraw()中完成:

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
public class PaintBoard extends View {
public PaintBoard(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

// paint a circle
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
canvas.drawCircle(120, 80, 60, paint);

// paint string
paint = new Paint();
paint.setColor(Color.YELLOW);
paint.setTextSize(20);
canvas.drawText("My name is Linc!",245,140,paint);

// draw line
paint = new Paint();
paint.setColor(Color.BLACK);
canvas.drawLine(245,145,500,145,paint);
}
}

新建Canvas

官方文档指出,新建Canvas的时候,一定要定义相应的Bitmap。

However, if you need to create a new Canvas, then you must define the Bitmap upon which drawing will actually be performed. The Bitmap is always required for a Canvas.

Canvas类提供了两个构造函数:

  1. Canvas():创建一个空的Canvas对象。
  2. Canvas(Bitmap bitmap):创建一个以bitmap位图为背景的Canvas。
1
2
Bitmap b = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);

这段代码新建了一个100 * 100像素的Bitmap,并将它作为Canvas的操作对象。
当使用新建的Canvas在Bitmap上执行绘制方法之后,还可以通过Canvas.drawBitmap(Bitmap,…)方法将绘制的结果提交给另一个Canvas,这样就可以达到两个Canvas协作完成的效果。
常见的我们可以在自定义的Canvas中对一个Bitmap实现绘制后,在View.onDraw()方法中,调用canvas.drawBitmap(Bitmap,…)方法,将绘制结果显示到View上。
这种先将图形全加载到内存进行绘制,然后再一次性展示到屏幕上,避免直接在屏幕上绘图导致出现明显闪烁的技术叫做双缓冲

变换处理Matrix

对绘图进行变换处理,需要借助Matrix类(android.graphics.Matrix)。
该类是一个矩阵工具类,本身不能对图像或View进行变换,但是可以结合Canvas的API来控制图形、View的变换。
Matrix提供的控制图片变换的方法主要包括:

  • setTranslate(float dx,float dy):控制Matrix进行位移。
  • setSkew(float kx,float ky):控制Matrix进行倾斜,kx、ky为X、Y方向上的比例。
  • setSkew(float kx,float ky,float px,float py):控制Matrix以px、py为轴心进行倾斜,kx、ky为X、Y方向上的倾斜比例。
  • setRotate(float degrees):控制Matrix进行depress角度的旋转,轴心为(0,0)。
  • setRotate(float degrees,float px,float py):控制Matrix进行depress角度的旋转,轴心为(px,py)。
  • setScale(float sx,float sy):设置Matrix进行缩放,sx、sy为X、Y方向上的缩放比例。
  • setScale(float sx,float sy,float px,float py):设置Matrix以(px,py)为轴心进行缩放,sx、sy为X、Y方向上的缩放比例。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 缩放图片
*/

protected void bitmapScale(float x, float y) {
// 因为要将图片放大,所以要根据放大的尺寸重新创建Bitmap
Bitmap afterBitmap = Bitmap.createBitmap((int) (baseBitmap.getWidth() * x), (int) (baseBitmap.getHeight() * y),
baseBitmap.getConfig());
Canvas canvas = new Canvas(afterBitmap);
// 初始化Matrix对象
Matrix matrix = new Matrix();
// 根据传入的参数设置缩放比例
matrix.setScale(x, y);
// 根据缩放比例,把图片draw到Canvas上
canvas.drawBitmap(baseBitmap, matrix, paint);
iv_after.setImageBitmap(afterBitmap);
}

几个注意点:

  • 对于一个从BitmapFactory.decodeXxx()方法加载的Bitmap对象而言,它是一个只读的,无法对其进行处理,必须使用Bitmap.createBitmap()方法重新创建一个Bitmap对象的拷贝,才可以对拷贝的Bitmap进行处理。
  • 因为图像的变换是针对每一个像素点的,所以有些变换可能发生像素点的丢失,这里需要使用Paint.setAnitiAlias(boolean)设置来消除锯齿,这样图片变换后的效果会好很多。
  • 在重新创建一个Bitmap对象的拷贝的时候,需要注意它的宽高,如果设置不妥,很可能变换后的像素点已经移动到”图片之外”去了。

关于Canvas和Matrix的一些理解

参考:Android Canvas Matrix —— Bitmap是如何画出来的
可以将Canvas理解为坐标系。我们在View或者Bitmap上进行绘制时,都是绘制到屏幕或者Bitmap上,都是有限的大小,而Canvas可以理解为是无限大的。
如果将Canvas理解为坐标系,那么Canvas就成了我们绘制内容的参考基准。
比如,默认Canvas的圆点在绘制区域的左上角,如果对Canvas进行位移,效果如下:
canvas_translate
上图中,右侧红色区域就是偏移后的坐标系,使用偏移后的坐标系进行绘制,效果就是平移了。
通过Matrix进行变换,那么Matrix就会保留变换之后的状态,如果多次变换,就会造成变换的累加。
为了不造成累加后的坐标混乱,Canvas提供了save()和restore()方法对Canvas的状态进行暂存和恢复。
在Canvas变换前,先使用save()方法,保存Canvas的状态,绘制完成之后,再使用restore()方法,将Canvas的状态恢复到save()的时候的状态,然后再进行下一次变换和绘制。
结合坐标系的理解,save()和restore()只是针对坐标系的变换而言,之前绘制的内容不会出现任何改动。
canvas_save_restore

Shader

Shader类是专门用来渲染图像及一些几何图形的。
使用Shader时,先构建Shader对象,然后通过Paint的setShader()方法来设置渲染对象,最后将这个Paint对象绘制到屏幕上。
Shader有5个直接子类,分别是BitmapShader、ComposeShader、LinearGradient、RadialGradient以及SweepGradient。

BitmapShader(图像渲染):

BitmapShader的作用是使用一张位图作为纹理来对某一区域进行填充。可以想象成在一块区域内铺瓷砖,只是这里的瓷砖是一张张位图而已。
BitmapShader函数原型为:

1
public BitmapShader (Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY);

其中,参数bitmap表示用来作为纹理填充的位图;参数tileX表示在位图X方向上位图衔接形式;参数tileY表示在位图Y方向上位图衔接形式。
Shader.TileMode有3种参数可供选择,分别为CLAMP、REPEAT和MIRROR。

  • CLAMP的作用是如果渲染器超出原始边界范围,则会复制边缘颜色对超出范围的区域进行着色。
  • REPEAT的作用是在横向和纵向上以平铺的形式重复渲染位图。
  • MIRROR的作用是在横向和纵向上以镜像的方式重复渲染位图。

LinearGradient(线性渲染)

LinearGradient的作用是实现某一区域内颜色的线性渐变效果。
LinearGradient的函数原型为:

1
public LinearGradient (float x0, float y0, float x1, float y1, int[] colors, float[] positions, Shader.TileMode tile);

其中,参数x0表示渐变的起始点x坐标;参数y0表示渐变的起始点y坐标;参数x1表示渐变的终点x坐标;参数y1表示渐变的终点y坐标;参数colors表示渐变的颜色数组;参数positions用来指定颜色数组的相对位置;参数tile表示平铺方式。
通常,参数positions设为null,表示颜色数组以斜坡线的形式均匀分布。

ComposeShader(混合渲染)

ComposeShader的作用是实现渲染效果的叠加,如BitmapShader与LinearGradient的混合渲染效果等。
ComposeShader的函数原型为:

1
public ComposeShader (Shader shaderA, Shader shaderB, PorterDuff.Mode mode);

其中,参数shaderA表示某一种渲染效果;参数shaderB也表示某一种渲染效果;参数mode表示两种渲染效果的叠加模式。
PorterDuff.Mode有16种参数可供选择,分别为:CLEAR、SRC、DST、SRC_OVER、DST_OVER、SRC_IN、DST_IN、SRC_OUT、DST_OUT、SRC_ATOP、DST_ATOP、XOR、DARKEN、LIGHTEN、MULTIPLY、SCREEN。
这16种叠加模式的具体叠加效果如图1所示。
compose_shader

RadialGradient(环形渲染)

RadialGradient的作用是在某一区域内实现环形的渐变效果。
RadialGradient的函数原型为:

1
public RadialGradient (float x, float y, float radius, int[] colors, float[] positions, Shader.TileMode tile);

其中,参数x表示环形的圆心x坐标;参数y表示环形的圆心y坐标;参数radius表示环形的半径;参数colors表示环形渐变的颜色数组;参数positions用来指定颜色数组的相对位置;参数tile表示平铺的方式。

SweepGradient(梯度渲染)

SweepGradient也称为扫描渲染,是指在某一中心以x轴正方向逆时针旋转一周而形成的扫描效果的渲染形式。
SweepGradient的函数原型为:

1
public SweepGradient (float cx, float cy, int[] colors, float[] positions);

其中,参数cx表示扫描的中心x坐标;参数cy表示扫描的中心y坐标;参数colors表示梯度渐变的颜色数组;参数positions用来指定颜色数组的相对位置。

效果

shader