在上一篇文章中介绍了TakePhoto
的常见api
,这一篇打算介绍下TakePhoto
的图片压缩解决方案,TakePhoto
的图片压缩有两种方案,一种是第三方压缩工具LuBan,另一种是自带的图片压缩方案,自身的图片压缩方案分为尺寸压缩
和质量压缩
,关于图片压缩的更详细的文章可以看这里。接下来将结合源码介绍下TakePhoto
中这两种图片压缩方案的实现过程。
入口调用
图片无论是拍照或者选取成功,都会调用takeResult
方法,下面是其部分实现代码
CompressImageImpl.of(contextWrap.getActivity(), compressConfig, result.getImages(), new CompressImage.CompressListener() {
@Override
public void onCompressSuccess(ArrayList<TImage> images) {
if(!compressConfig.isEnableReserveRaw()) {
deleteRawFile(images);
}
handleTakeCallBack(result);
....
}
@Override
public void onCompressFailed(ArrayList<TImage> images, String msg) {
if(!compressConfig.isEnableReserveRaw()) {
deleteRawFile(images);
}
handleTakeCallBack(TResult.of(images), String.format(contextWrap.getActivity().getResources().getString(R.string.tip_compress_failed), message.length > 0 ? message[0] : "", msg, result.getImage().getCompressPath()));
....
}
}).compress();
这个只有配置了compressConfig
,这段代码才会执行。在这段代码中,首先会调用CompressImageImpl.of
初始化图片压缩方式,跳进去查看其实现代码。
public static CompressImage of(Context context,CompressConfig config, ArrayList<TImage> images, CompressImage.CompressListener listener) {
if(config.getLubanOptions()!=null){
return new CompressWithLuBan(context,config,images,listener);
}else {
return new CompressImageImpl(context,config,images,listener);
}
}
可以看见根据是否有config.getLubanOptions()
配置来选择CompressWithLuBan
和CompressImageImpl
两种实例化方式,根据对应的实例化方式,来调用compress()
压缩方法。详细介绍下两种压缩方式的实现过程。
CompressWithLuBan
鲁班压缩,内部封装了鲁班压缩的使用,查看其构造函数:
public CompressWithLuBan(Context context, CompressConfig config, ArrayList<TImage> images,
CompressListener listener) {
options = config.getLubanOptions();
this.images = images;
this.listener = listener;
this.context = context;
}
第一个参数CompressConfig
压缩配置,通过这个可以配置LuBan
和自带的压缩
,自带的压缩
可以配置的参数有长宽最大像素
,压缩的大小
,是否采用像素压缩
,是否采用质量压缩
,是否保留源文件
这几种配置,LuBan
压缩可以配置的有最大长度
,最大高度
,和压缩的大小
这几种。启用压缩时候会调用compress
,里面通过images
的数量来判断是否压缩单张和多张。压缩的方法是调用LuBan
的api,压缩成功回调
listener.onCompressSuccess(images);
压缩的图片有为null
的或为空的回调
listener.onCompressFailed();
Luban压缩的实现还是很简单的,内部只是封装了Luban的api,简化了我们的调用过程。
CompressImageImpl
这是TakePhoto
自己实现的一套压缩算法,也可以实现对多张图片的压缩
compressImageUtil.compress(image.getOriginalPath(), new CompressImageUtil.CompressListener() {
@Override
public void onCompressSuccess(String imgPath) {
image.setCompressPath(imgPath);
continueCompress(image,true);
}
@Override
public void onCompressFailed(String imgPath, String msg) {
continueCompress(image,false,msg);
}
});
private void continueCompress(TImage image,boolean preSuccess,String...message){
image.setCompressed(preSuccess);
int index=images.indexOf(image);
boolean isLast=index== images.size() - 1;
if (isLast) {
handleCompressCallBack(message);
}else {
compress(images.get(index+1));
}
}
private void handleCompressCallBack(String...message){
if(message.length>0){
listener.onCompressFailed(images,message[0]);
return;
}
for(TImage image:images){
if(!image.isCompressed()){
listener.onCompressFailed(images,image.getCompressPath()+" is compress failures");
return;
}
}
listener.onCompressSuccess(images);
}
这是实现的最重要的部分,其中compressImageUtil
中封装了像素压缩
和质量压缩
两种方法。而continueCompress
当一张图片压缩完成之后,判断是否还有继续要压缩的图片,有就通过compress
继续压缩下一张,直到全部压缩完毕,每压缩一次压缩的图片就会通过setCompressPath
放到compressPath
中,最后通过handleCompressCallBack
回调给最先给到的takeResult
的onCompressSuccess
回调中,在该回调中就可以获取压缩到的图片和源图片。
继续来看compressImageUtil
的实现,首先看它的compress
方法
public void compress(String imagePath, CompressListener listener) {
if (config.isEnablePixelCompress()){
try {
compressImageByPixel(imagePath,listener);
} catch (FileNotFoundException e) {
listener.onCompressFailed(imagePath,String.format("图片压缩失败,%s",e.toString()));
e.printStackTrace();
}
}else {
compressImageByQuality(BitmapFactory.decodeFile(imagePath),imagePath,listener);
}
}
takephoto的压缩是回调这个compress
方法,在这个方法中首先根据isEnablePixelCompress
来判断是否开启了尺寸压缩,是就开启尺寸压缩
,否则进行质量压缩
,首先看质量压缩
/**
* 按比例缩小图片的像素以达到压缩的目的
* */
private void compressImageByPixel(String imgPath,CompressListener listener) throws FileNotFoundException {
BitmapFactory.Options newOpts = new BitmapFactory.Options();
newOpts.inJustDecodeBounds = true;//只读边,不读内容
BitmapFactory.decodeFile(imgPath, newOpts);
newOpts.inJustDecodeBounds = false;
int width = newOpts.outWidth;
int height = newOpts.outHeight;
float maxSize =config.getMaxPixel();
int be = 1;
if (width >= height && width > maxSize) {//缩放比,用高或者宽其中较大的一个数据进行计算
be = (int) (newOpts.outWidth / maxSize);
be++;
} else if (width < height && height > maxSize) {
be = (int) (newOpts.outHeight / maxSize);
be++;
}
newOpts.inSampleSize =be;//设置采样率
newOpts.inPreferredConfig = Config.ARGB_8888;//该模式是默认的,可不设
newOpts.inPurgeable = true;// 同时设置才会有效
newOpts.inInputShareable = true;//。当系统内存不够时候图片自动被回收
Bitmap bitmap = BitmapFactory.decodeFile(imgPath, newOpts);
if (config.isEnableQualityCompress()){
compressImageByQuality(bitmap,imgPath,listener);//压缩好比例大小后再进行质量压缩
}else {
File thumbnailFile=getThumbnailFile(new File(imgPath));
bitmap.compress(Bitmap.CompressFormat.JPEG,100,new FileOutputStream(thumbnailFile));
listener.onCompressSuccess(thumbnailFile.getPath());
}
}
在该方法中可以看见主要是通过动态计算图片的采样率
,来达到压缩图片尺寸的目的。在方法开始首先通过将inJustDecodeBounds
设置为true
,来读图片的宽高,而不去加载图片,接着计算要压缩的be
,如果宽高有一个大于maxSize
,就用宽高中较大的去除以maxSize
,然后自身加一,来保证压缩的尺寸不能超过设置的做大值,通过getThumbnailFile
来保证存放的位置在cache
的takephoto_cache
目录下面,最后通过bitmap.compress
来将图片存放到刚才指定目录下面。通过该方法可以直到这是常见的图片无损压缩方法,我们通过这种方法来让显示的图片不会因为过大而OOM
。
如果我们配置了质量压缩,就会继续调用compressImageByQuality
来进行有损压缩图片
,继续看下有损压缩
/**
* 多线程压缩图片的质量
* */
private void compressImageByQuality(final Bitmap bitmap, final String imgPath, final CompressListener listener){
if(bitmap==null){
sendMsg(false,imgPath,"像素压缩失败,bitmap is null",listener);
return;
}
new Thread(new Runnable() {//开启多线程进行压缩处理
@Override
public void run() {
// TODO Auto-generated method stub
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int options = 100;
bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);//质量压缩方法,把压缩后的数据存放到baos中 (100表示不压缩,0表示压缩到最小)
while (baos.toByteArray().length >config.getMaxSize()) {//循环判断如果压缩后图片是否大于指定大小,大于继续压缩
baos.reset();//重置baos即让下一次的写入覆盖之前的内容
options -= 5;//图片质量每次减少5
if(options<=5)options=5;//如果图片质量小于5,为保证压缩后的图片质量,图片最底压缩质量为5
bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);//将压缩后的图片保存到baos中
if(options==5)break;//如果图片的质量已降到最低则,不再进行压缩
}
// if(bitmap!=null&&!bitmap.isRecycled()){
// bitmap.recycle();//回收内存中的图片
// }
try {
File thumbnailFile=getThumbnailFile(new File(imgPath));
FileOutputStream fos = new FileOutputStream(thumbnailFile);//将压缩后的图片保存的本地上指定路径中
fos.write(baos.toByteArray());
fos.flush();
fos.close();
sendMsg(true, thumbnailFile.getPath(),null,listener);
} catch (Exception e) {
sendMsg(false,imgPath,"质量压缩失败",listener);
e.printStackTrace();
}
}
}).start();
}
图片的质量压缩是内部开启一个子线程,然后不断减少bitmap.compress
第二个参数的值,当减少到一定阈值时候,就停止压缩,通过sendMsg
将压缩的图片发送给主线程,然后借助listener.onCompressSuccess(imagePath)
将图片路径回调出去,最后通过onCompressSuccess
可以取出图片的压缩路径和源路径,将结果交给handleTakeCallBack
处理,最后会交给takeSuccess
,用户可以通过takeSuccess
的参数拿到最终的图片,如果我们不想保存源图,可以配置isEnableReserveRaw
来将拍照后的源图片删除,用户此时就只会获取到压缩的图片。这是TakePhoto
自带的图片压缩的大致实现流程。
ImageRotateUtil
图片角度矫正类,如果我们配置了isCorrectImage
,内部会矫正图片的角度。这是一个附加的功能。内部大致原理是通过getBitmapDegree
获取图片倾斜的度数,然后通过rotateBitmapByDegree
旋转图片得到新的图片,将新的图片覆盖以前老的图片地址,得到矫正后的图片地址。
总结
TakePhoto
的图片压缩分为Luban压缩
和自带的尺寸和质量压缩
两种算法,我自己一般在项目中使用Luban
压缩,Luban
压缩可以单独使用,而且效果也很好,但是内部的压缩算法我们还是有必要了解的,这对我们自己写图片的ImageLoader
有很大帮助,如果我们需要自己定制自己的图片框架,可以参考TakePhoto
的内部压缩算法,这是很有用的。