Alec's blog

唯纯粹与热爱不可辜负

0%

缩放导致的图片内存问题

情景

需求:实现一个简单的图片编辑功能,图片在编辑时可缩放、滑动和设置圆角弧度等操作。

我通过继承ImageView重写onDraw()给图片添加一个半透明灰色罩层,通过Matrix进行图像的缩放和滑动,使用ScaleGestureDetector监听双指的滑动,这都是一些比较常规的操作。但是在保存编辑后的图片时出现了问题:

很显然圆角裁剪弧度不对。通俗来说就是四个圆角[不够圆]。下面是裁剪图片的逻辑:

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
//方形裁剪:
//originBitmapu由ImageView负责绘制
//transMatrix是经过用户滑动,缩放后的Matrix
//clipRectF是用户选中的裁剪区域
val clipWidth = clipRectF!!.right.toInt() - clipRectF!!.left.toInt()
val clipHeight = clipRectF!!.bottom.toInt() - clipRectF!!.top.toInt()
val clipBitmap = Bitmap.createBitmap(
originBitmap,
clipRectF!!.left.toInt(),
clipRectF!!.top.toInt(),
clipWidth,
clipHeight,
transMatrix, true
)

//裁剪圆角:
//corner是用户选中的弧度系数
val bitmap = Bitmap.createBitmap(
clipBitmap.width,
clipBitmap.height,
Bitmap.Config.ARGB_8888
)

val paint = Paint()
paint.shader = BitmapShader(clipBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
paint.isAntiAlias = true

val canvas = Canvas(bitmap)
canvas.drawRoundRect(rectF, corner, corner, paint)

圆角弧度系数corner是用户设定的,理论上来用户所见即所得。

问题分析

圆角问题

canvas.drawRoundRect(rectF, corner, corner, paint)这个方法的定义是:

drawRoundRect(rectF: RectF, rx: Float, ry: Float, paint: Paint)

首先我们要知道 rx 和 ry的意思:

从图中可以看出来当rx和ry的值保持不变的情况下:

  • 矩形的尺寸越小,则看起来[越圆]

  • 矩形尺寸越大,则看起来[没那么圆]

而我们的问题就是实际裁剪到的圆角图片[不够圆],也就是说可能是图片尺寸变大了。

打印一下裁剪前后bitmap的尺寸:

可以看到裁剪后bitmap的宽高更大了。再看看我们保存图片:

8.22MB的图片!!原图1.15MB。那么为什么裁剪后圆角与预定不符的原因找到了:裁剪后图片尺寸变大了。

到这里我们遇到了一个很重要的问题:图片内存问题。

图片内存问题

理论

几百KB的图片经过编辑变成了8.22MB?Why?

  • 首先我们知道一张图片是由一个个像素点组成的。举例:一张尺寸1080x2028的图片,它就是由1080x2028个像素点组成的。

  • 而一个像素点可以表示三种颜色,也就是三原色(红、绿、蓝),另外加上一个透明度。三原色+透明度,我们称为四条通道。

  • 而一条通道的大小也是不一样的,可以是8bit,表示有0-255个值;可以是4bit,表示有0-16个值。一条通道越大那么该图片的分辨率就越大。举例红色通道,如果是8bit,那么它可以表示255种红色。使用4bit就只能表示16种红色。

  • Android开发中遇到的 ARGB_8888 就表示了这张图片的信息:四通道,每条通道8bit。RGB_565 表示三通道(不支持透明度),每条通道大小分别为5bit,6bit和5bit。

  • ARGB_8888 图片一个像素点占用内存是:4 * 8bit / 8 = 4 个字节。如果该图尺寸为1080x2028,那么该图占用内存1080 * 2028 * 4 = 8760960 Byte。

那么影响一张图片的大小的因素就显而易见了:尺寸和质量。质量包含通道数量和每条通道大小。

实际代码

从上面那个例子来看,原图1.15MB裁剪后变成8.22MB的原因是尺寸变大了。那么尺寸变大的位置是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//方形裁剪:
//originBitmapu由ImageView负责绘制
//transMatrix是经过用户滑动,缩放后的Matrix
//clipRectF是用户选中的裁剪区域
val clipWidth = clipRectF!!.right.toInt() - clipRectF!!.left.toInt()
val clipHeight = clipRectF!!.bottom.toInt() - clipRectF!!.top.toInt()
val clipBitmap = Bitmap.createBitmap(
originBitmap,
clipRectF!!.left.toInt(),
clipRectF!!.top.toInt(),
clipWidth,
clipHeight,
transMatrix, true
)

查看源码Bitmap.createBitmap()源码(下面给出伪代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height,Matrix m, boolean filter) {
int neww = width;
int newh = height;
RectF deviceR = new RectF();
if (m == null || m.isIdentity()) {
bitmap = createBitmap(null, neww, newh, newConfig, source.hasAlpha(), cs);
paint = null; // not needed
} else {
m.mapRect(deviceR, dstR); //这行代码将Matrix的宽高信息设置到deviceR
neww = Math.round(deviceR.width());
newh = Math.round(deviceR.height());
bitmap = createBitmap(null, neww, newh, transformedConfig,transformed || source.hasAlpha(), cs);
}

return bitmap;
}

可以发现当传入Matrix的时候,会直接从Matrix获取width和height作为新bitmap的宽和高。也就是说我们实际使用createBitmap()创建的bitmap的尺寸是传入Matrix的尺寸,如果Matrix进行了缩放操作,那么创建的bitmap的尺寸也会发生变化。

问题解决

既然知道是因为使用Matrix进行放大导致的裁剪后图片尺寸变大,那么我们可以使用scale记录一下放大的倍数,然后在createBitmap()之前对Matrix进行一个等倍数缩小操作就行了。

1
2
3
4
5
6
7
8
9
10
11
12
//方形裁剪:
if(scale > 1) transMatrix.postScale(1/scale, 1/scale)
val clipWidth = clipRectF!!.right.toInt() - clipRectF!!.left.toInt()
val clipHeight = clipRectF!!.bottom.toInt() - clipRectF!!.top.toInt()
val clipBitmap = Bitmap.createBitmap(
originBitmap,
clipRectF!!.left.toInt(),
clipRectF!!.top.toInt(),
clipWidth,
clipHeight,
transMatrix, true
)

等倍数缩小尺寸,那么裁剪出来的图片和原来的尺寸一致,圆角的问题自然就解决了。对用户来说,所见即所得。