文章

Canvas进阶

Canvas进阶

Canvas 进阶

Canvas Layer Canvas 图层

Canvas 画布的操作是不可逆的,而且很多 Canvas 画布操作会影响后续的步骤,所以会对 Canvas 画布的一些状态进行保存和回滚。

画布和图层

画布是由多个图层构成:

image.png

绘制操作和画布操作都是在默认图层上进行的,在通常情况下,使用默认图层就可满足需求,但是如果需要绘制比较复杂的内容,如地图 (地图可以有多个地图层叠加而成,比如:政区层,道路层,兴趣点层) 等,则分图层绘制比较好一些。

你可以把这些图层看做是一层一层的玻璃板,你在每层的玻璃板上绘制内容,然后把这些玻璃板叠在一起看就是最终效果。

Canvas 保存和恢复

状态栈

这个栈可以存储画布状态和图层状态:

image.png

为什么存在快照与回滚

画布的操作是不可逆的,而且很多画布操作会影响后续的步骤,例如第一个例子,两个圆形都是在坐标原点绘制的,而因为坐标系的移动绘制出来的实际位置不同。所以会对画布的一些状态进行保存和回滚。

SaveFlags

名称简介
ALL_SAVE_FLAG默认,保存全部状态
CLIP_SAVE_FLAG保存剪辑区
CLIP_TO_LAYER_SAVE_FLAG剪裁区作为图层保存
FULL_COLOR_LAYER_SAVE_FLAG保存图层的全部色彩通道
HAS_ALPHA_LAYER_SAVE_FLAG保存图层的 alpha (不透明度) 通道
MATRIX_SAVE_FLAG保存 Matrix 信息 ( translate, rotate, scale, skew)

相关的 API

相关 API介绍
save把当前的画布的状态进行保存,然后放入特定的栈中,save() 方法之后的代码,可以调用 Canvas 的平移、放缩、旋转、裁剪等操作;返回值传给 restoreToCount 恢复状态
saveLayerXxx新建一个图层,并放入特定的栈中
restore把栈中最顶层的画布状态取出来,并按照这个状态恢复当前的画布
restoreToCount弹出指定位置及其以上所有的状态,并按照指定位置的状态进行恢复,参数为 save () 的返回值
getSaveCount获取栈中内容的数量 (即保存次数)
save()
1
2
3
4
5
6
7
8
9
// 保存全部状态;保存当前matrix and clip状态到一个私有的stack
public int save() {  
    return nSave(mNativeCanvasWrapper, MATRIX_SAVE_FLAG | CLIP_SAVE_FLAG);  
}

// 根据saveFlags参数保存一部分状态
public int save(@Saveflags int saveFlags) {  
    return nSave(mNativeCanvasWrapper, saveFlags);  
}
  • 第二种方法比第一种多了一个 saveFlags 参数,使用这个参数可以只保存一部分状态,更加灵活,这个 saveFlags 参数具体可参考上面表格中的内容。
  • 每调用一次 save 方法,都会在栈顶添加一条状态信息。
saveLayerXxx
1
2
3
4
5
6
7
8
9
10
11
// 无图层alpha(不透明度)通道
public int saveLayer (RectF bounds, Paint paint)
public int saveLayer (RectF bounds, Paint paint, int saveFlags)
public int saveLayer (float left, float top, float right, float bottom, Paint paint)
public int saveLayer (float left, float top, float right, float bottom, Paint paint, int saveFlags)

// 有图层alpha(不透明度)通道
public int saveLayerAlpha (RectF bounds, int alpha)
public int saveLayerAlpha (RectF bounds, int alpha, int saveFlags)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha)
public int saveLayerAlpha (float left, float top, float right, float bottom, int alpha, int saveFlags)
  • 行为和 save() 一样;saveLayerXxx 方法会让你花费更多的时间去渲染图像 (图层多了相互之间叠加会导致计算量成倍增长),使用前请谨慎,如果可能,尽量避免使用。
  • 避免使用,可用 View.LAYER_TYPE_HARDWARE 替代,对一个 View 做 xfermodecolor filteralphasaveLayerXXX 更好
  • 使用 saveLayerXxx 方法,也会将图层状态也放入状态栈中,同样使用 restore 方法进行恢复。
restore

状态回滚,就是从栈顶取出一个状态然后根据内容进行恢复。

同样以上面状态栈图片为例,调用一次 restore 方法则将状态栈中第 5 次取出,根据里面保存的状态进行状态恢复。

restoreToCount

弹出指定位置以及以上所有状态,并根据指定位置状态进行恢复。

以上面状态栈图片为例,如果调用 restoreToCount (2) 则会弹出 2 3 4 5 的状态,并根据第 2 次保存的状态进行恢复。

getSaveCount

获取保存的次数,即状态栈中保存状态的数量,以上面状态栈图片为例,使用该函数的返回值为 5。

不过请注意,该函数的最小返回值为 1,即使弹出了所有的状态,返回值依旧为 1,代表默认状态。

Canvas 操作 常用格式

虽然关于状态的保存和回滚啰嗦了不少,不过大多数情况下只需要记住下面的步骤就可以了:

1
2
3
save();      //保存状态
// ...          //具体操作
restore();   //回滚到之前的状态

Ref

Canvas 几何变换操作 (二维、三维)

为什么要有画布操作?

画布操作可以帮助我们用更加容易理解的方式制作图形。

例如: 从坐标原点为起点,绘制一个长度为 20 dp,与水平线夹角为 30 度的线段怎么做?

按照我们通常的想法 (被常年训练出来的数学思维),就是先使用三角函数计算出线段结束点的坐标,然后调用 drawLine 即可。

然而这是否是被固有思维禁锢了?

假设我们先绘制一个长度为 20 dp 的水平线,然后将这条水平线旋转 30 度,则最终看起来效果是相同的,而且不用进行三角函数计算,这样是否更加简单了一点呢?

合理的使用画布操作可以帮助你用更容易理解的方式创作你想要的效果,这也是画布操作存在的原因。

PS: 所有的画布操作都只影响后续的绘制,对之前已经绘制过的内容没有影响。

常见的二维变换

几何变换的使用大概分为三类:

  1. 使用 Canvas 来做常见的二维变换;
  2. 使用 Matrix 来做常见和不常见的二维变换;
  3. 使用 Camera 来做三维变换

所有的 Canvas 操作都只影响后续的绘制,对之前已经绘制过的内容没有影响

Canvas 多重变换要倒序?

还没试验

translate 平移

translate 是坐标系的移动,可以为图形绘制选择一个合适的坐标系。 请注意,位移是基于当前位置移动,而不是每次基于屏幕左上角的 (0,0) 点移动,如下:

1
2
3
4
5
6
7
8
9
10
11
// 省略了创建画笔的代码

// 在坐标原点绘制一个黑色圆形
mPaint.setColor(Color.BLACK);
canvas.translate(200,200);
canvas.drawCircle(0,0,100,mPaint);

// 在坐标原点绘制一个蓝色圆形
mPaint.setColor(Color.BLUE);
canvas.translate(200,200);
canvas.drawCircle(0,0,100,mPaint);

image.png

我们首先将坐标系移动一段距离绘制一个圆形,之后再移动一段距离绘制一个圆形,两次移动是可叠加的

rotate 旋转

  • rotate (float degrees) 默认是以原点作为旋转中心旋转
  • rotate (float degrees, float px, float py) 参数里的 degrees 是旋转角度,单位是度(也就是一周有 360° 的那个单位),方向是顺时针为正向; px 和 py 是轴心的位置(以传入的 x, y 作为旋转中心)。

默认的旋转中心依旧是坐标原点:

1
2
3
4
5
6
7
8
9
10
11
12
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400, 400,0);   // 矩形区域

mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
canvas.drawRect(rect, mPaint);

canvas.rotate(180);                     // 旋转180度 <-- 默认旋转中心为原点

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect, mPaint);

image.png

改变旋转中心位置:

1
2
3
4
5
6
7
8
9
10
11
12
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400, 400,0);   // 矩形区域

mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
canvas.drawRect(rect, mPaint);

canvas.rotate(180, 200, 0);               // 旋转180度 <-- 旋转中心向右偏移200个单位

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect, mPaint);

image.png

旋转也是可叠加的:

1
2
canvas.rotate(180); 
canvas.rotate(20);

调用两次旋转,则实际的旋转角度为 180+20=200 度。

示例:

1
2
3
4
5
6
7
8
9
10
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);

canvas.drawCircle(0,0,400,mPaint);          // 绘制两个圆形
canvas.drawCircle(0,0,380,mPaint);

for (int i=0; i<=360; i+=10) {               // 绘制圆形之间的连接线
   canvas.drawLine(0,380,0,400,mPaint);
   canvas.rotate(10);
}

image.png

scale 缩放

  • scale (float sx, float sy) 缩放控制中心 (0, 0) x 和 y 轴缩放
  • scale (float sx, float sy, float px, float py)  缩放控制中心 (px, py) x 和 y 轴缩放

这两个方法中前两个参数是相同的分别为 x 轴和 y 轴的缩放比例。而第二种方法比前一种多了两个参数,用来控制缩放中心位置的。

缩放比例 (sx, sy) 取值范围详解:

取值范围 (n)说明
(-∞, -1)先根据缩放中心放大 n 倍,再根据中心轴进行翻转(n<=-1,根据缩放中心放大 n 倍,再根据中心轴 x 或 y 进行翻转)
-1根据缩放中心轴进行翻转
(-1, 0)先根据缩放中心缩小到 n,再根据中心轴进行翻转(-1<n<0,根据缩放中心缩小 n 倍,再根据中心轴 x 或 y 进行翻转)
0不会显示,若 sx 为 0,则宽度为 0,不会显示,sy 同理(n=0,不会显示)
(0, 1)根据缩放中心缩小到 n(0<n<1,根据缩放中心缩放 n 倍)
1没有变化
(1, +∞)根据缩放中心放大 n 倍(n>=1,根据缩放中心放大 n 倍)

中心轴,指的是以缩放中心为原点的坐标轴,水平 x 轴,竖直 y 轴;缩放的中心默认为坐标原点

scale 案例

  • 案例 1:x 和 y 都缩放 2 倍
1
2
3
4
5
6
7
8
9
10
11
12
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // 矩形区域

mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
canvas.drawRect(rect,mPaint);

canvas.scale(0.5f.5f);                // 画布缩放

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

(为了更加直观,我添加了一个坐标系,可以比较明显的看出,缩放中心就是坐标原点)

image.png

  • 案例 2:让缩放中心位置稍微改变一下
1
2
3
4
5
6
7
8
9
10
11
12
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // 矩形区域

mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
canvas.drawRect(rect,mPaint);

canvas.scale(0.5f,0.5f,200,0);          // 画布缩放  <-- 缩放中心向右偏移了200个单位

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

image.png

(图中用箭头指示的就是缩放中心。)

  • 示例 3:前面两个示例缩放的数值都是正数,按照表格中的说明,当缩放比例为负数的时候会根据缩放中心轴进行翻转,下面我们就来实验一下:
1
2
3
4
5
6
7
8
9
10
11
12
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // 矩形区域

mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
canvas.drawRect(rect,mPaint);

canvas.scale(-0.5f,-0.5f);          // 画布缩放

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

image.png

为了效果明显,这次我不仅添加了坐标系而且对矩形中几个重要的点进行了标注,具有相同字母标注的点是一一对应的。

由于本次未对缩放中心进行偏移,所有默认的缩放中心就是坐标原点,中心轴就是 x 轴和 y 轴。

本次缩放可以看做是先根据缩放中心 (坐标原点) 缩放到原来的 0.5 倍,然后分别按照 x 轴和 y 轴进行翻转。

  • 示例 4:坐标点非默认点 (原点 0,0),缩放再翻转
1
2
3
4
5
6
7
8
9
10
11
12
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,-400,400,0);   // 矩形区域

mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
canvas.drawRect(rect,mPaint);

canvas.scale(-0.5f,-0.5f,200,0);          // 画布缩放  <-- 缩放中心向右偏移了200个单位

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

image.png

本次对缩放中心点 y 轴坐标进行了偏移,故中心轴也向右偏移了。

和位移 (translate) 一样,缩放也是可以叠加的。

1
2
canvas.scale(0.5f.5f);
canvas.scale(0.5f.1f);

调用两次缩放则 x 轴实际缩放为 0.5x0.5=0.25 y 轴实际缩放为 0.5x0.1=0.05

下面我们利用这一特性制作一个有趣的图形。

1
2
3
4
5
6
7
8
9
10
11
// 注意设置画笔模式为描边(STROKE)

// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(-400,-400,400,400);   // 矩形区域

for (int i=0; i<=20; i++) {
    canvas.scale(0.9f.9f);
    canvas.drawRect(rect,mPaint);
}

image.png

skew 错切

skew (float sx, float sy) skew 这里翻译为错切,错切是特殊类型的线性变换。

参数里的 sx 和 sy 是 x 方向和 y 方向的错切系数。

  • float sx: 将画布在 x 方向上倾斜相应的角度,sx 为倾斜角度的 tan 值;
  • float sy: 将画布在 y 轴方向上倾斜相应的角度,sy 为倾斜角度的 tan 值;
    变换后:
1
2
X = x + sx * y
Y = sy * x + y

示例:

1
2
3
4
5
6
7
8
9
10
11
12
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,0,200,200);   // 矩形区域

mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
canvas.drawRect(rect,mPaint);

canvas.skew(1,0);                       // 水平错切 <45度

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

image.png

注意,这里全是倾斜角度的 tan 值,比如我们打算在 X 轴方向上倾斜 45 度,tan 45=1;

错切也是可叠加的,不过请注意,调用次序不同绘制结果也会不同

1
2
3
4
5
6
7
8
9
10
11
12
13
// 将坐标系原点移动到画布正中心
canvas.translate(mWidth / 2, mHeight / 2);

RectF rect = new RectF(0,0,200,200);   // 矩形区域

mPaint.setColor(Color.BLACK);           // 绘制黑色矩形
canvas.drawRect(rect,mPaint);

canvas.skew(1,0);                       // 水平错切
canvas.skew(0,1);                       // 垂直错切

mPaint.setColor(Color.BLUE);            // 绘制蓝色矩形
canvas.drawRect(rect,mPaint);

image.png

使用 Matrix 来做变换

Matrix 做常见变换的方式:

  1. 创建 Matrix 对象;
  2. 调用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法来设置几何变换;
  3. 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 来把几何变换应用到 Canvas
1
2
3
4
5
6
7
8
9
10
11
12
Matrix matrix = new Matrix();

// …

matrix.reset();
matrix.postTranslate();
matrix.postRotate();

canvas.save();
canvas.concat(matrix);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();

把 Matrix 应用到 Canvas 有两个方法: Canvas.setMatrix(matrix)Canvas.concat(matrix)

  1. Canvas.setMatrix(matrix):用 Matrix 直接替换 Canvas 当前的变换矩阵,即抛弃 Canvas 当前的变换,改用 Matrix 的变换(注:不同的系统中 setMatrix(matrix) 的行为可能不一致,所以还是尽量用 concat(matrix) 吧);
  2. Canvas.concat(matrix):用 Canvas 当前的变换矩阵和 Matrix 相乘,即基于 Canvas 当前的变换,叠加上 Matrix 中的变换。

Matrix 常见变换

postTranslate/postRotate/postScale/postSkew

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private fun drawMatrix_postSkew(canvas: Canvas) {
    val dx = dx / width
    val dy = dy / height
    myMatrix.reset()
    myMatrix.postSkew(dx, dy)
    canvas.save()
    canvas.concat(myMatrix)
    val x = width / 2F - bitmap.width / 2F
    val y = height / 2F - bitmap.height / 2F
    canvas.drawBitmap(bitmap, x, y, paint)
    canvas.restore()
}

private fun drawMatrix_postScale(canvas: Canvas) {
    val pivotX = width / 2F
    val pivotY = height / 2F
    myMatrix.reset()
    myMatrix.postScale(dx / width.toFloat(), dy / height.toFloat(), pivotX, pivotY)
    canvas.save()
    canvas.concat(myMatrix)
    val x = width / 2F - bitmap.width / 2F
    val y = height / 2F - bitmap.height / 2F
    canvas.drawBitmap(bitmap, x, y, paint)
    canvas.restore()
}

private fun drawMatrix_postTranslate(canvas: Canvas) {
    myMatrix.reset()
    myMatrix.postTranslate(dx, dy)
    canvas.save()
    canvas.concat(myMatrix)
    val x = width / 2F - bitmap.width / 2F
    val y = height / 2F - bitmap.height / 2F
    canvas.drawBitmap(bitmap, x, y, paint)
    canvas.restore()
}

private fun drawMatrix_postRotate(canvas: Canvas, pivotX: Float = 0F, pivotY: Float = 0F) {
    myMatrix.reset()
    val degrees = dx
    myMatrix.postRotate(degrees, pivotX, pivotY)
    canvas.save()
    canvas.concat(myMatrix)
    val x = width / 2F - bitmap.width / 2F
    val y = height / 2F - bitmap.height / 2F
    canvas.drawBitmap(bitmap, x, y, paint)
    canvas.restore()

    canvas.drawDot(pivotX, pivotY)
}

使用 Matrix 来做自定义变换

Matrix 的自定义变换使用的是 setPolyToPoly() 方法。

poly 就是「多」的意思。setPolyToPoly() 的作用是通过多点的映射的方式来直接设置变换。「多点映射」的意思就是把指定的点移动到给出的位置,从而发生形变。例如:(0, 0) -> (100, 100) 表示把 (0, 0) 位置的像素移动到 (100, 100) 的位置,这个是单点的映射,单点映射可以实现平移。而多点的映射,就可以让绘制内容任意地扭曲。

setPolyToPoly 参数: Matrix.setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount) 用点对点映射的方式设置变换;setPolyToPoly 各个点的变化,最多 4 个

  • src 代表变换前的坐标
  • dst 代表变换后的坐标
  • srcIndex/dstIndexsrcdst 的变换,可以通过 srcIndex 和 dstIndex 来制定第一个变换的点,一般可能都设置为 0
  • pointCount 代表支持的转换坐标的点数,最多支持 4 个(取值范围是: 0 到 4)。其实也就是你定义的 float[] src 这个数组除以 2 的数字。也可以这么理解:
    • 0 相当于 reset
    • 1 相当于 translate
    • 2 可以进行缩放、旋转、平移变换
    • 3 可以进行缩放、旋转、平移、错切变换
    • 4 可以进行缩放、旋转、平移、错切以及任何形变

案例 1:

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
private fun drawMatrix_setPolyToPoly(canvas: Canvas) {
    val bitmap = BitmapFactory.decodeResource(resources, R.drawable.iv_0)
    val left = width / 2F - bitmap.width / 2F
    val top = height / 2F - bitmap.height / 2F
    val right = left + bitmap.width
    val bottom = top + bitmap.height
    val pointsSrc = floatArrayOf(
            left, top,
            right, top,
            left, bottom,
            right, bottom)
    val pointsDst = floatArrayOf(
            left - 10F.dp(), top + 50F.dp(),
            right + 120F.dp(), top - 90F.dp(),
            left + 20F.dp(), bottom + 30F.dp(),
            right + 20F.dp(), bottom + 60F.dp())

    myMatrix.reset()
    myMatrix.setPolyToPoly(pointsSrc, 0, pointsDst, 0, pointsSrc.size shr 1) // 一般是除以2
    canvas.save()
    canvas.concat(myMatrix)
    val x = width / 2F - bitmap.width / 2F
    val y = height / 2F - bitmap.height / 2F
    canvas.drawBitmap(bitmap, x, y, paint)
    canvas.restore()
}

案例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Matrix matrix = new Matrix();
float pointsSrc = {left, top, right, top, left, bottom, right, bottom};
float pointsDst = {left - 10, top + 50, right + 120, top - 90, left + 20, bottom + 30, right + 20, bottom + 60};

// …

matrix.reset();
matrix.setPolyToPoly(pointsSrc, 0, pointsDst, 0, 4);

canvas.save();
canvas.concat(matrix);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();

// setPolyToPoly参数里,`src` 和 `dst` 是源点集合目标点集;`srcIndex` 和 `dstIndex` 是第一个点的偏移;`pointCount` 是采集的点的个数(个数不能大于 4,因为大于 4 个点就无法计算变换了)。

image.png

setPolyToPoly 实现折叠效果

使用 Camera 来做三维变换

Camera 的三维变换有三类:旋转、平移、移动相机。

Camera.rotate*() 三维旋转

Camera.rotate*() 一共有四个方法: rotateX(deg) rotateY(deg) rotateZ(deg) rotate(x, y, z)

示例:

1
2
3
4
5
6
7
canvas.save();

camera.rotateX(30); // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas

canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();

image.png

另外,Camera 和 Canvas 一样也需要保存和恢复状态才能正常绘制,不然在界面刷新之后绘制就会出现问题。所以上面这张图完整的代码应该是这样的:

1
2
3
4
5
6
7
8
9
canvas.save();

camera.save(); // 保存 Camera 的状态
camera.rotateX(30); // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
camera.restore(); // 恢复 Camera 的状态

canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();

如果你需要图形左右对称,需要配合上 Canvas.translate(),在三维旋转之前把绘制内容的中心点移动到原点,即旋转的轴心,然后在三维旋转后再把投影移动回来:

1
2
3
4
5
6
7
8
9
10
11
canvas.save();

camera.save(); // 保存 Camera 的状态
camera.rotateX(30); // 旋转 Camera 的三维空间
canvas.translate(centerX, centerY); // 旋转之后把投影移动回来
camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
canvas.translate(-centerX, -centerY); // 旋转之前把绘制内容移动到轴心(原点)
camera.restore(); // 恢复 Camera 的状态

canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();

Canvas 的几何变换顺序是反的,所以要把移动到中心的代码写在下面,把从中心移动回来的代码写在上面。

image.png

Camera.translate (float x, float y, float z) 移动

1
2
3
4
5
6
7
8
9
10
11
canvas.save()

camera.save()
camera.translate(0F.dp(), dy, z) // 旋转 Camera 的三维空间
camera.applyToCanvas(canvas) // 把旋转投影到 Canvas
camera.restore()

val xx = width / 2F - bitmap.width / 2F
val yy = height / 2F - bitmap.height / 2F
canvas.drawBitmap(bitmap, xx, yy, paint)
canvas.restore()

Camera.setLocation(x, y, z) 设置虚拟相机的位置

注意!这个方法有点奇葩,它的参数的单位不是像素,而是 inch,英寸。

这种设计源自 Android 底层的图像引擎 Skia 。在 Skia 中,Camera 的位置单位是英寸,英寸和像素的换算单位在 Skia 中被写死为了 72 像素,而 Android 中把这个换算单位照搬了过来。

在 Camera 中,相机的默认位置是 (0, 0, -8)(英寸)。8 x 72 = 576,所以它的默认位置是 (0, 0, -576)(像素)。

如果绘制的内容过大,当它翻转起来的时候,就有可能出现图像投影过大的「糊脸」效果。而且由于换算单位被写死成了 72 像素,而不是和设备 dpi 相关的,所以在像素越大的手机上,这种「糊脸」效果会越明显。

而使用 setLocation () 方法来把相机往后移动,就可以修复这种问题。

1
camera.setLocation(0, 0, newZ);

Camera.setLocation (x, y, z) 的 x 和 y 参数一般不会改变,直接填 0 就好。

Canvas Camera 实现翻页效果

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class Sample14FlipboardView extends View {
    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    Bitmap bitmap;
    Camera camera = new Camera();
    int degree;
    ObjectAnimator animator = ObjectAnimator.ofInt(this, "degree", 0, 180);

    public Sample14FlipboardView(Context context) {
        super(context);
    }

    public Sample14FlipboardView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public Sample14FlipboardView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    {
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.maps);

        animator.setDuration(2500);
        animator.setInterpolator(new LinearInterpolator());
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setRepeatMode(ValueAnimator.REVERSE);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        animator.start();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        animator.end();
    }

    @SuppressWarnings("unused")
    public void setDegree(int degree) {
        this.degree = degree;
        invalidate();
    }

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

        int bitmapWidth = bitmap.getWidth();
        int bitmapHeight = bitmap.getHeight();
        int centerX = getWidth() / 2;
        int centerY = getHeight() / 2;
        int x = centerX - bitmapWidth / 2;
        int y = centerY - bitmapHeight / 2;

        // 第一遍绘制:上半部分
        canvas.save();
        canvas.clipRect(0, 0, getWidth(), centerY);
        canvas.drawBitmap(bitmap, x, y, paint);
        canvas.restore();

        // 第二遍绘制:下半部分
        canvas.save();

        if (degree < 90) {
            canvas.clipRect(0, centerY, getWidth(), getHeight());
        } else {
            canvas.clipRect(0, 0, getWidth(), centerY);
        }
        camera.save();
        camera.rotateX(degree);
        canvas.translate(centerX, centerY);
        camera.applyToCanvas(canvas);
        canvas.translate(-centerX, -centerY);
        camera.restore();

        canvas.drawBitmap(bitmap, x, y, paint);
        canvas.restore();
    }
}

Ref

本文由作者按照 CC BY 4.0 进行授权