项目地址
https://github.com/ListerChen/BitmapProject
一、Bitmap基本介绍
Bitmap也称为位图,是图片在内存中的表现形式,任何图片(JPEG, PNG, WEBP...)加载到内存后都是一个Bitmap对象。Bitmap实际是像素点的集合,假设它的宽高为width和height,那么该Bitmap就包含width*height个像素,它在内存中占用的内存就是(width*height*单个像素内存)。
为了减小图片在磁盘上所占的空间,将Bitmap保存到磁盘上时会进行压缩,图片的文件格式实际代表的是不同的压缩方式与压缩率,而将磁盘上的文件加载到内存中时就要进行解压缩。
1.1 图片格式介绍
常见的静态图片格式为JPEG、PNG和WEBP,它们有着不同的压缩方式,保存到本地后所占用的空间大小也不一样。
- JPEG是一种有损压缩格式,以24位颜色压缩存储单个位图,但是不支持透明度。使用JPEG进行压缩时需要选择适当的压缩率,避免图片质量太差。
- PNG是一种无损压缩格式,支持所有的颜色,由于是无损压缩,PNG一般用于APP图标这类对线条或者清晰度有要求的图片。由于PNG所占空间较大,目前一般将PNG转为WEBP使用。
- WEBP支持无损压缩和有损压缩,并且他的无损压缩率优于PNG,有损压缩率优于JPEG,同时它支持所有颜色,并支持多帧动画,唯一的缺点是压缩效率不如JPEG和PNG。
1.2 Bitmap色深
Bitmap的本质就是像素点的集合,它通过描述每个像素的ARGB信息来形成整张图片,其中A表示透明度通道,RGB表示红绿蓝3种颜色通道,每个通道的值都在0-255之间,因此8bit可以完整地表示1个通道,那么4x8=32bit可以表示一个完整的像素。但如果每个Bitmap都使用32bit来表示一个像素的话,对内存来说是一个较大的负担,因此对于质量要求不高的Bitmap来说,可以采用较少的内存去表示一个像素。
色深指的是每一个像素点用多少bit来存储ARGB值,色深越大,图片的色彩越丰富,一般来说色深有8bit、16bit、32bit等,Bitmap.Config中的色深配置如下。
ALPHA_8: 这种方案只存储透明度通道,色深为8bit,使用场景特殊,比如设置遮盖效果等。
ARGB_8888: ARGB每个通道值采8bit来表示,色深为32bit,每个像素点需要4byte的内存来存储数据,图片质量高,所占内存大。
ARGB_4444: ARGB每个通道都是4位,色深为16bit,由于这种配置下的图片质量较差,Android官方已经将其标为弃用。
RGB_565: 色深为16bit,RGB通道值分别占5、6、5位,但是没有存储A通道值,所以不支持透明度,可用于对清晰度要求不高的照片。
RGBA_F16: 色深为64bit,该配置主要用于广色域与HDR内容。
HARDWARE: 这是一种特殊配置,该配置下,Bitmap会被存储在显存中,并且Bitmap是不可变的。这种配置仅适用于,当Bitmap唯一的操作就是将自己绘制在屏幕上时。
我们可以根据对图片质量的要求创建不同色深的Bitmap,如果对显示质量有较高的要求可以使用ARGB_8888,一般来说使用RGB_565即可,这也可以减少OOM的概率。
Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1.2 通过采样加载大图
ImageView是显示Bitmap的载体,一般情况下ImageView的宽高会小于Bitmap,如果将一个完整的Bitmap加载到偏小的ImageView中会浪费内存。关于这个问题,Android官方提供了一个优化方法。
该方法通过比较ImageView与Bitmap的大小并计算采样率,最终将采样后的小图加载到内存中。流程如下:在调用BitmapFactory.decodeXXX(res, resId, BitmapFactory.Options)
解析图片时,先将BitmapFactory.Options
的inJustDecodeBounds
设为true,此时不会将图片加载到内存中,而是只得到Bitmap的宽高,随后通过图片宽高计算采样率。得到采样率后再将inJustDecodeBounds
设为false,再加载Bitmap时可以得到大图的采样图。
public static Bitmap decodeSampledBitmapFromResource(
Resources res, int resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
// 该属性默认为false, 为true时不会将图片加载到内存中, 而是只计算宽高
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算采样率
options.inSampleSize = calculateInSampleSize(
options, reqWidth, reqHeight);
// 设置inJustDecodeBounds为false, 将图片加载到内存
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 获得Bitmap的宽高
final int height = options.outHeight;
final int width = options.outWidth;
// 计算采样率
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
如果使用Glide这类图片加载框架将Bitmap加载到ImageView中,框架会自动帮我们完成采样的工作。不过如果你需要获取某个Bitmap的缩略图,上述方法还是有用武之地的。
二、Bitmap相关应用
2.1 图片裁剪
图片裁剪是图片处理的基本功能,APP中的设置头像功能就需要用到图片裁剪,用户可以通过一个预览框在原图上裁剪出特定区域的图案。
系统本身提供了裁剪的功能,我们可以在A页面通过以下代码启动一个裁剪图片的Activity,裁剪结束在A页面的onActivityResult(...)
中得到裁剪后的图片。
// uri为图片的地址
public void startCropPicture(Uri uri) {
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1); // 裁剪框比例
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 300); // 输出图片大小
intent.putExtra("outputY", 300);
intent.putExtra("scale", true);
intent.putExtra("return-data", true);
startActivityForResult(intent, REQUEST_CODE);
}
图片裁剪的原理很简单,通过Bitmap.createBitmap(originBitmap, left, top, width, height)
即可在originBitmap上裁出指定区域的Bitmap,因此我们可以自己实现一个裁剪头像的功能。需要注意的是,Bitmap.createBitmap(...)
方法参数中传入的坐标和宽高都是基于原始Bitmap的,如果传入的参数超出了原始Bitmap的宽高就会抛出异常。
本文Demo实现了裁剪图案的基础功能,拖动裁剪框右下角可以修改框大小,拖动其他区域可以移动裁剪框,可以通过本文开头的链接下载代码。
除了Bitmap.createBitmap(originBitmap, left, top, width, height)
裁剪出指定的Bitmap,也可以在绘制时直接指定Bitmap的绘制区域,通过Canvas.drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint)
即可实现,其中src表示对原图片的裁剪区域,dst表示对裁剪后的图片绘制到View上的区域。
2.2 图片拼接
图片拼接常见于分享时拼接所有图片,达到"一图看尽所有"的效果。代码示例如下,为了方便演示,示例中将4张一样的正方形图片splitBitmap拼接到了一起。
private Bitmap getJointBitmap(Bitmap splitBitmap) {
int width = splitBitmap.getWidth();
int height = splitBitmap.getHeight();
Bitmap bitmap = Bitmap.createBitmap(
width * 2, height * 2, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
canvas.drawBitmap(splitBitmap, 0, 0, null);
canvas.drawBitmap(splitBitmap, width, 0, null);
canvas.drawBitmap(splitBitmap, 0, height, null);
canvas.drawBitmap(splitBitmap, width, height, null);
return bitmap;
}
拼接图片前先创建了一个宽高都为splitBitmap宽高2倍的Bitmap作为容器,然后通过Canvas在容器的四个位置绘制4张一样的图片,最终效果如下。
2.3 矩阵变换
Bitmap是像素点的集合,我们可以通过矩阵运算改变每个像素点的位置,达到图形变换的效果。Android中可以通过Matrix类来进行变换,Matrix本身是一个3x3的矩阵,可以通过Matrix m = new Matrix()
新建一个单位矩阵,原始矩阵的值如下所示。
[1 0 0]
[0 1 0]
[0 0 1]
Matrix中各个位置的变换信息如下所示,scale表示缩放,skew表示错切,trans表示平移,persp等表示透视参数。Bitmap中的每个像素点可以使用一个3x1的矩阵表示,其中x表示当前像素点的横坐标,y表示纵坐标。用该矩阵左乘Bitmap中的所有像素后,就能得到变换后的图像。
[scaleX skewX transX] [x] [scaleX * x + skewX * y + transX]
[skewY scaleY transY] x [y] = [skewY * x + scaleY * y + transY]
[persp0 persp1 persp2] [1] [persp0 * x + persp1 * y + persp2]
简单来说,Matrix是一个容器,保存了用户期望的矩阵变换信息。在将Matrix应用于Bitmap之前,我们可以对其进行各种操作,将变换信息保存进去。矩阵运算可以实现平移、旋转、缩放、错切,因此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方向上的缩放比例
很多时候矩阵变换并不是单一的平移、旋转或缩放,这些变换经常结合在一起使用,此时setXXX()
方法无法满足要求。因此Matrix提供了preXXX()
和postXXX()
方法来组合多个矩阵操作,多个矩阵操作之间通过乘法运算。由于矩阵的乘法是不满足交换律的,因此进行矩阵运算时需要注意乘法的顺序。
在使用preXXX()
和postXXX()
时,我们可以将矩阵变换的所有计算看成一个乘法列表,调用preXXX()
方法时就是向列表头部添加操作,调用postXXX()
时就是向列表尾部添加操作。例如以下代码中矩阵乘法的执行顺序就是2->1->3->4。需要注意的是,setXXX()
方法会重置Matrix的变换,如果对下方执行完4个运算的Matrix调用setTranslate()
方法,那么该Matrix就只有平移效果了。
Matrix matrix = new Matrix();
matrix.preScale(...); // 1
matrix.preTranslate(...); // 2
matrix.postTranslate(...); // 3
matrix.postRotate(...); // 4
2.4 颜色变换
颜色变换主要通过ColorFilter进行,通过Paint.setColorFilter(ColorFilter filter)
可以设置颜色过滤器,该过滤器会对每一个像素的颜色进行过滤,得到最终的图像。ColorFilter有3个子类,这里主要介绍ColorMatrixColorFilter。
2.4.1 ColorMatrixColorFilter
该颜色过滤器通过矩阵进行色彩变换,先来介绍一下色彩矩阵,Android中的色彩是以ARGB的形式存储的,我们可以通过ColorMatrix修改颜色的值,ColorMatrix定义了一个4x5的float矩阵,矩阵的4行分别表示在RGBA上的向量,其范围值在0f-2f之间,如果为1就是原效果。每一行的第5列表示偏移量,就是指在当前通道上增大或减小多少。
ColorMatrix colorMatrix = new ColorMatrix(new float[]{
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, 1, 0,
});
ColorMatrix与颜色之间的运算如下所示,其实就是矩阵运算,与上一节的Matrix类似。
[a, b, c, d, e] [R] [a*R + b*G + c*B + d*A + e]
[f, g, h, i, j] [G] [f*R + g*G + h*B + i*A + j]
[k, l, m, n, o] x [B] = [k*R + l*G + m*B + n*A + o]
[p, q, r, s, t] [A] [p*R + q*G + r*B + s*A + t]
[1]
有了ColorMatrix,就可以对Bitmap上所有的颜色进行修改,例如调整每个通道的值,将初始值1改为0.5,就可以将Bitmap变暗。不过我不太了解色彩,也没在项目中实际使用过,感兴趣的朋友可以看参考1。
2.5 图像混合
图像混合是指对两张原始图像(我们称为DST和SRC)的内容按某种规则合成,从而形成一张包含DST和SRC特点的新图像。例如DST为圆形图像,SRC为照片,可以将它们合成为圆形照片。
Android通过PorterDuffXfermode实现图像混合,它实际上是通过公式对两张图像在Canvas上的所有像素进行ARGB运算,最终在每个像素点得到新的ARGB值。需要注意的是,在onDraw(Canvas)
方法中进行图像混合时,先绘制的图像为DST,后绘制的图像为SRC,因此需要注意图像的绘制顺序。
PorterDuffXfermode一共提供了18种混合模式,它们的计算公式如下,Sa表示SRC的ALPHA通道,Sc表示SRC的颜色;Da表示DST的ALPHA通道,Dc表示DST的颜色。以CLEAR为例,该模式会清除SRC区域的所有内容。
合成模式 | 公式 |
---|---|
CLEAR | [0, 0] |
SRC | [Sa, Sc] |
DST | [Da, Dc] |
SRC_OVER | [Sa + (1 - Sa)Da, Rc = Sc + (1 - Sa)Dc] |
DST_OVER | [Sa + (1 - Sa)Da, Rc = Dc + (1 - Da)Sc] |
SRC_IN | [Sa * Da, Sc * Da] |
DST_IN | [Sa * Da, Sa * Dc] |
SRC_OUT | [Sa * (1 - Da), Sc * (1 - Da)] |
DST_OUT | [Da * (1 - Sa), Dc * (1 - Sa)] |
SRC_ATOP | [Da, Sc * Da + (1 - Sa) * Dc] |
DST_ATOP | [Sa, Sa * Dc + Sc * (1 - Da)] |
XOR | [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] |
DARKEN | [Sa + Da - Sa * Da, Sc * (1 - Da) + Dc * (1 - Sa) + min(Sc, Dc)] |
LIGHTEN | [Sa + Da - Sa * Da, Sc * (1 - Da) + Dc * (1 - Sa) + max(Sc, Dc)] |
MULTIPLY | [Sa * Da, Sc * Dc] |
SCREEN | [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] |
ADD | Saturate(S + D) |
如果你使用过PorterDuffXfermode,你可能见过下面这张图,Android官方的样例就是这个效果,不过官方只提供了16种混合模式的样例,我在Demo中把ADD和OVERLAY也添加了进去。
当然你也可能见过这张图。
乍一看,这两张图中的DST和SRC原始图像都是一样的,但是为什么使用了同样的混合模式后,显示的结果不同呢?
关键就在于DST和SRC的大小,第一张图中的DST和SRC都是Bitmap,它们的大小与Canvas相等,只是在Bitmap的某个区域绘制了圆和矩形。Demo代码如下,可以看到makeDst()
和makeSrc()
中创建的Bitmap与整个View(或者说Canvas)是相等的。这也解释了为什么第一张图的CLEAR模式下的结果是空白的,因为SRC的大小是整个View的大小,CLEAR模式表示清除SRC区域的内容,最终将整个View的内容清除了。
public class XFerModeView extends View {
private Paint mPaint;
private PorterDuffXfermode mPorterDuffXfermode;
private int mWidth;
private int mHeight;
// 省略构造方法......
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPorterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mWidth != w || mHeight != h) {
mWidth = w;
mHeight = h;
invalidate();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawBackground(canvas);
int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
drawCompositionInFullSize(canvas);
canvas.restoreToCount(sc);
}
private void drawBackground(Canvas canvas) {
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
mPaint.setColor(Color.BLACK);
canvas.drawRect(0, 0, mWidth, mHeight, mPaint);
}
private void drawCompositionInFullSize(Canvas canvas) {
mPaint.setStyle(Paint.Style.FILL);
Bitmap dst = makeDst();
Bitmap src = makeSrc();
// 绘制DST
canvas.drawBitmap(dst, 0, 0, mPaint);
// 设置图像混合模式
mPaint.setXfermode(mPorterDuffXfermode);
// 绘制SRC
canvas.drawBitmap(src, 0, 0, mPaint);
// 清除图像混合模式
mPaint.setXfermode(null);
}
private Bitmap makeDst() {
Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
mPaint.setColor(0xFFFFCC44);
c.drawOval(10, 10, mWidth * 3f / 4, mHeight * 3f / 4, mPaint);
return bm;
}
private Bitmap makeSrc() {
Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bm);
mPaint.setColor(0xFF66AAFF);
c.drawRect(mWidth * 1f / 3, mHeight * 1f / 3,
mWidth * 19f / 20, mHeight * 19f / 20, mPaint);
return bm;
}
在第二张图中,绘制SRC和DST时创建的图像大小就是圆或矩形的大小,最终的结果也与第一张图有所不同,修改后的代码如下。还是以CLEAR模式为例,此时清除的就只是矩形区域SRC的图像,可以看到DST中与SRC相交的部分被清除了。
public class XFerModeView extends View {
// 省略重复代码......
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawBackground(canvas);
int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
drawCompositionInSelfSize(canvas);
canvas.restoreToCount(sc);
}
/**
* 混合图像的大小只有可见区域大小
*/
private void drawCompositionInSelfSize(Canvas canvas) {
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(0xFFFFCC44);
canvas.drawOval(10, 10, mWidth * 3f / 4, mHeight * 3f / 4, mPaint);
mPaint.setXfermode(mPorterDuffXfermode);
mPaint.setColor(0xFF66AAFF);
canvas.drawRect(mWidth * 1f / 3, mHeight * 1f / 3,
mWidth * 19f / 20, mHeight * 19f / 20, mPaint);
mPaint.setXfermode(null);
}
}
使用图像混合时还有一个要注意的地方:上述代码在onDraw(Canvas)
方法中绘制混合图像时会先调用int sc = canvas.saveLayer(...)
生成一个新的图层(Layer),sc表示图层的编号,随后在新Layer上绘制DST和SRC,绘制完后将该Layer添加到Canvas上。那么这里为什么需要新的Layer来绘制DST和SRC,而不是直接在Canvas上绘制呢?
Layer可以理解为画布Canvas的一个层级,默认情况下Canvas只有一个Layer,所有的绘制都在同一图层上。当需要绘制多层图像时,可以通过canvas.saveLayer(...)
生成新的Layer,在新Layer上绘制的内容是独立的,不会影响到其他Layer的内容,调用canvas. restoreToCount(int sc)
时将该Layer覆盖到Canvas现有的图像上。Canvas通过栈的形式管理Layer,示意图如下。
之前提到进行图像混合时,先绘制的内容是DST,后绘制的是SRC。如果不新建Layer的话,在绘制SRC时,Canvas上的所有内容都会被当作DST,所以背景等内容也会参与图像混合,很容易得到错误的效果。
以上就是图像混合的基本介绍,图像混合的应用场景比较广泛,这里介绍几种常见的场景。
2.5.1 图像切割
图像切割用于将图像切割成特定的形状。可以是常见形状如圆形或圆角矩形,也可以切割为五角星这样的非常规形状,使用这一类非常规形状时需要该形状的底图。
将图像裁剪为圆角矩形时比较简单,在onDraw(Canvas)
中新建图层,绘制圆角矩形作为DST,再绘制原图作为SRC即可,此时图像混合模式应设置为SRC_IN,代码如下,decodeSampledBitmapFromResource(...)
就是1.2节的大图采样。
public class RoundCornerView extends View {
private Paint mPaint;
private PorterDuffXfermode mFerMode;
private Bitmap mBitmap;
private Rect mBitmapRect;
private int mWidth;
private int mHeight;
// 省略构造函数...
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mFerMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mWidth != w || mHeight != h) {
mWidth = w;
mHeight = h;
mBitmap = BitmapUtils.decodeSampledBitmapFromResource(
getContext().getResources(), R.drawable.compress_test, mWidth, mHeight);
mBitmapRect = new Rect(0, 0, mWidth, mHeight);
invalidate();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
canvas.drawRoundRect(0, 0, mWidth, mHeight, 50, 50, mPaint);
mPaint.setXfermode(mFerMode);
canvas.drawBitmap(mBitmap, null, mBitmapRect, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(sc);
}
......
}
最终效果如下,同理可以将图像切割为圆形等基础形状。
如果要将图像切割为五角星这样的图案,就需要使用一张五角星的底图,需要注意的是,底图上五角星以外的部分应该是透明的,否则切割出来还是原来的形状。其代码与切割为圆角矩形大同小异,只需要将绘制圆角矩形的部分换成绘制五角星即可。
public class StarPicView extends View {
private Paint mPaint;
private PorterDuffXfermode mMode;
private Bitmap mBgBitmap;
private Bitmap mBitmap;
private int mWidth, mHeight;
private Rect mDrawRect;
// 省略构造函数......
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mWidth != w || mHeight != h) {
mWidth = w;
mHeight = h;
mBgBitmap = BitmapUtils.decodeSampledBitmapFromResource(
getContext().getResources(), R.drawable.star4, mWidth, mHeight);
mBitmap = BitmapUtils.decodeSampledBitmapFromResource(
getContext().getResources(), R.drawable.icon3, mWidth, mHeight);
mDrawRect = new Rect(0, 0, mWidth, mHeight);
invalidate();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
canvas.drawBitmap(mBgBitmap, null, mDrawRect, mPaint);
mPaint.setXfermode(mMode);
canvas.drawBitmap(mBitmap, null, mDrawRect, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(sc);
}
}
最终的效果如下。
2.5.2 色彩合成
色彩合成可以为图片添加新的效果,当使用纯色与照片混合时,可以改变图片整体的色调。例如黄色可以让图片具有泛黄的怀旧效果,红色可以让图片更温暖。以下代码通过SCREEN混合模式将半透明的红色与图片混合。
public class ColorComposeView extends View {
......
private void init() {
setLayerType(LAYER_TYPE_SOFTWARE, null);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);
}
......
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
int sc = canvas.saveLayer(0, 0, mWidth, mHeight, null);
canvas.drawColor(0x44FF0000);
mPaint.setXfermode(mMode);
canvas.drawBitmap(mBitmap, null, mRect, mPaint);
mPaint.setXfermode(null);
canvas.restoreToCount(sc);
}
}
怎么样,是不是觉得小姐姐看上去都温柔了一些?
出了纯色混合,也可以将两张图片进行合成,例如通过一张毛玻璃的底图,可以为照片添加一定的模糊效果,底图如下所示。
绘制时将底图作为DST,将照片作为SRC,混合模式使用OVERLAY,代码与绘制切割五角星的大同小异,不再赘述。最终得到如下的效果。
2.5.3 图像alpha渐变
之前遇到一个很有意思的UI需求,给定一张底图,要求绘制时图像的透明度从上到下是渐变的1-0,效果如下所示。因为background是白色,所以透明部分透出来的是白色。
这个其实也很简单,在本地新建一个渐变的drawable,然后通过XOR模式进行混合,具体代码可参考文章开头代码。
三、图片压缩
3.1 质量压缩
质量压缩减小的是图片在磁盘上的体积大小,通过Bitmap.compress(CompressFormat, quality, outputStream)
将Bitmap保存到本地时,可以选择对应的文件格式以及质量标准,文件格式包含JPEG、PNG和WEBP三种,质量的取值为0-100,0代表最差质量,100代表最高质量。WEBP格式将会在API30被弃用,取而代之的是WEBP_LOSSLESS和WEBP_LOSSY,用于更清晰地描述是无损压缩还是有损压缩。
将Bitmap保存为60质量标准的jpeg的代码如下。
private void qualityCompressJPG() {
OutputStream os = getOutputStreamByName("jpgFile60.jpeg");
if (os != null) {
mOriginBitmap.compress(Bitmap.CompressFormat.JPEG, 60, os);
}
}
private OutputStream getOutputStreamByName(String fileName) {
BufferedOutputStream bos = null;
File dir = new File(FILE_DIR);
boolean dirExist = true;
if (!dir.exists()) {
dirExist = dir.mkdirs();
}
if (dirExist) {
File file = new File(dir, fileName);
if (file.exists()) {
file.delete();
}
try {
boolean fileExist;
fileExist = file.createNewFile();
if (fileExist) {
bos = new BufferedOutputStream(new FileOutputStream(file));
}
} catch (IOException e) {
e.printStackTrace();
}
}
return bos;
}
三种图片格式中,PNG为无损压缩,因此当文件格式选择PNG时Bitmap.compress(CompressFormat, quality, outputStream)
方法会无视quality参数。下图为保存同一张图片时,选择不同的格式与不同质量的对比。可以发现WEBP格式所占用的空间是比较理想的,如果APP的体积比较大,可以考虑把资源文件转化为WEBP来节省空间。
3.2 尺寸压缩
尺寸压缩就是指压缩原始Bitmap的宽高,通过减小像素个数来减小Bitmap占用的空间,这种压缩方式下,不管Bitmap所占的内存,还是图片保存到磁盘上所占的空间都会有显著的减小。
尺寸压缩可以通过Bitmap.createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
来创建压缩后的Bitmap,filter参数可以简单地理解为,如果为true的话就会消耗更长的时间获得更好的图片质量,false则相反。
private void sizeCompress1(int scale) {
int width = mOriginBitmap.getWidth() / scale;
int height = mOriginBitmap.getHeight() / scale;
Bitmap b = Bitmap.createScaledBitmap(
mOriginBitmap, width, height, false);
OutputStream os = getOutputStreamByName("sizeCompress1.webp");
if (os != null) {
b.compress(Bitmap.CompressFormat.WEBP, 100, os);
}
}
通过Bitmap b = Bitmap.createScaledBitmap(src, w, h, filter)
来创建Bitmap时,它的Config是基于原始Bitmap的,如果原始Bitmap的Config是ARGB_8888而压缩后又不需要这么高的清晰度,那么可以选择新建RGB_565的Bitmap,并通过Canvas将压缩后的图片绘制上去。
private void sizeCompress2(int scale) {
int width = mOriginBitmap.getWidth() / scale;
int height = mOriginBitmap.getHeight() / scale;
Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(b);
Rect rect = new Rect(0, 0, width, height);
canvas.drawBitmap(mOriginBitmap, null, rect, null);
OutputStream os = getOutputStreamByName("sizeCompress2_565.webp");
if (os != null) {
b.compress(Bitmap.CompressFormat.WEBP, 100, os);
}
}
如果第2种方式选择ARGB_8888,创建出来的Bitmap其所占的内存(通过Bitmap.getByteCount()
计算)以及保存到磁盘上的大小都与第1种方式相同。如果选择RGB_565,相比于第1种方式,Bitmap所占的内存会缩小一半,保存到磁盘上后的体积也会适当减小。
参考&推荐阅读
- Android自定义控件其实很简单
- Android动态模糊实现的研究