T003 自定义控件 裁剪动画

参考知识点:01.5 精通自定义 View 之绘图基础——Canvas

一、原理

这个动画的原理很简单,就是每次将裁剪区域变大,在裁剪区域内的图像就会显示出来,而裁剪区域之外的图像不会显示。而关键问题在于如何计算裁剪区域。

再来看一下动画截图,如下图所示。

从图示中可以看出,有两个裁剪区域。

裁剪区域一:从左向右,逐渐变大。假设宽度是 clipWidth,高度是 CLIP_HEIGHT,那么裁剪区域一所对应的 Rect 对象如下:

1
Rect(0, 0, clipWidth, CLIP_HEIGHT);

裁剪区域二:从右向左,同样逐渐变大,它的宽度、高度都与裁剪区域一相同。但它是从右向左变化的,假设图片的宽度是 width,那么裁剪区域二所对应的 Rect 对象如下:

1
Rect(width - clipWidth, CLIP_HEIGHT, width, 2* CLIP_HEIGHT);

二、示例代码

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
83
84
85
86
87
88
89
public class ClipRegionView extends View {
private Paint mPaint;
private Bitmap mBitmap;
private int clipWidth = 0;
private int width;
private int height;
private static final int CLIP_HEIGHT = 50;
private Path mPath;
private RectF mRect;
public ClipRegionView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.meinv);
width = mBitmap.getWidth();
height = mBitmap.getHeight();
mPath = new Path();
mRect = new RectF();
}
@Override
protected void onSizeChanged(int w, int h, int oldW, int oldH) {
super.onSizeChanged(w, h, oldW, oldH);
// 原图宽高超过控件自身宽高,进行缩放
if (width > w || height > h) {
float scaleW = w * 1f / width;
float scaleH = h * 1f / height;
float scale = Math.min(scaleW, scaleH);
// 按比例重置参数
mBitmap = scaleBitmap(mBitmap, scale);
width = (int) (width * scale);
height = (int) (height * scale);
}
}
@Override
protected void onDraw(Canvas canvas) {
mPath.reset();
int i = 0;
while (i * CLIP_HEIGHT <= height) {
if (i % 2 == 0) {
mRect.set(0, i * CLIP_HEIGHT, clipWidth, (i+1) * CLIP_HEIGHT);
} else {
mRect.set(width - clipWidth, i * CLIP_HEIGHT, width, (i+1) * CLIP_HEIGHT);
}
// 替换 Region.union 方法
mPath.addRect(mRect, Path.Direction.CCW);
i++;
}
// 因 canvas.clipRegion 方法过时,所以替换成 Path 相关 Api
canvas.clipPath(mPath);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
if (clipWidth > width) {
return;
}
clipWidth += 5;
invalidate();
}
/**
* 按比例缩放图片
*
* @param origin 原图
* @param ratio 比例
* @return 新的bitmap
*/
private Bitmap scaleBitmap(Bitmap origin, float ratio) {
if (origin == null) {
return null;
}
int width = origin.getWidth();
int height = origin.getHeight();
Matrix matrix = new Matrix();
matrix.preScale(ratio, ratio);
Bitmap newBM = Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
if (newBM.equals(origin)) {
return newBM;
}
origin.recycle();
return newBM;
}
}

通过调用 invalidate() 函数的方式来重复触发 onDraw() 函数,然后在 onDraw() 函数中计算需要裁剪的画布。

在上述代码中,首先,由于 mPath 对象是每次都复用的,所以,在每次计算裁剪区域前, 都需要调用 mPath.reset() 函数将区域置空。

其次,根据计算裁剪区域的原理循环计算图片中每条间隔的裁剪区域并添加到 mPath 对象中。

1
2
3
4
5
6
7
8
9
10
while (i * CLIP_HEIGHT <= height) {
if (i % 2 == 0) {
mRect.set(0, i * CLIP_HEIGHT, clipWidth, (i+1) * CLIP_HEIGHT);
} else {
mRect.set(width - clipWidth, i * CLIP_HEIGHT, width, (i+1) * CLIP_HEIGHT);
}
// 替换 Region.union 方法
mPath.addRect(mRect, Path.Direction.CCW);
i++;
}

最后,将图片绘制在裁剪过的画布上,并渐变增大裁剪区域。

1
2
3
canvas.clipPath(mPath);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
clipWidth += 5;

需要注意的是,当裁剪区域超过图像大小时,表示当前图像已经完全被绘制出来,可以暂停当前的绘制,以免浪费 CPU 资源。

当图片宽高超过控件自身大小时,裁剪动画效果很差,因此对原图进行缩放并重置参数。其效果图如下所示:

有瑕疵的裁剪动画