这个库单独使用感觉相当简单,作者封装的非常好,使用特方便
- 源码地址以及使用教程:Luban
本篇使用的代码是在RxJava——基础学习(三),简单实践基础上,添加了图片的点击事件。最近没有再学习RxJava
,因为RxJava
正处于过渡时期,2.0
版本要发布了,修改还蛮大的,就想等2.0
发布后,再继续学习RxJava
1.简单使用
前两张图,是特意添加的两个比较大的,不同分辨率的图片,第3个图之后的就是手机截屏后的图,分辨率就是手机屏幕的分辨率
- 第1个图
5120 * 2880
,大小为5.68M
- 第2个图
3840 * 2400
,大小为1.08M
- 第3个图
1080 * 1920
,大小为1.19M
前两个图,不做任何处理,直接使用ImageView
展示,在我的坚果手机百分百OOM
点击每一个图片后,开启一个新的Activity
来展示图片。在新的Activity
中,使用Luban
将图片进行压缩,得到压缩后的图片后,使用ImageView
展示出来
代码:
private void showPicFileByLuban(@NonNull File file) {
Luban.get(ShowPicActivity.this)
.load(file)//目标图片
.putGear(Luban.THIRD_GEAR)//压缩等级
.setCompressListener(new OnCompressListener() {
@Override
public void onStart() {//开始压缩
}
@Override
public void onSuccess(File file) {//压缩成功,拿到压缩的图片,在UI线程
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
mToolBar.setSubtitle(bitmap.getWidth() + "*" + bitmap.getHeight() + "-->" + bitmap.getByteCount());
iv.setImageBitmap(bitmap);
}
@Override
public void onError(Throwable e) {//压缩失败
}
})
.launch();//开启压缩
}
代码很简单,压缩后的是一个File
,根据需求,对这个File
再做处理
注意不同分辨率的图片压缩后的宽高
这个库强大的地方在于针对不同的分辨率图片,压缩比例计算,控制图片文件的大小
整个Demo
的代码上传到了Github
:PicStore
使用很简单,则意味着源码做了大量的优化,设计巧妙,下面学习大神的代码
2. 尝试学习源码
根据使用过程用到的方法来进行学习源码,其中最重要就是关于压缩比例的计算,学习作者的封装的思路和技巧
2.1 get(Context context)方法
public static Luban get(Context context) {
if (INSTANCE == null) INSTANCE = new Luban(Luban.getPhotoCacheDir(context));
return INSTANCE;
}
这个方法用来创建Luban
对象,Luban
的构造方法是私有的并且需要一个File
对象,在get()
内,在new
的时候,就调用了Luban.getPhotoCacheDir(context)
,这个方法是用来指定缓存目录的,缓存目录默认为:系统默认缓存文件夹下的luban_disk_cache
文件夹
在Luban.getPhotoCacheDir(context)
内又调用了getPhotoCacheDir(Context context, String cacheName)
方法
private static File getPhotoCacheDir(Context context, String cacheName) {
File cacheDir = context.getCacheDir();
if (cacheDir != null) {
File result = new File(cacheDir, cacheName);
if (!result.mkdirs() && (!result.exists() || !result.isDirectory())) {//result文件夹不能创建,或者创建了却不是一个文件夹
return null;
}
return result;
}
if (Log.isLoggable(TAG, Log.ERROR)) {
Log.e(TAG, "default disk cache dir is null");
}
return null;
}
设置缓存目录的方法
2.2 load(File file)设置压缩目标图片
public Luban load(File file) {
mFile = file;
return this;
}
这个方法倒是比较容易理解,设置过目标图片文件后,又返回了Luban
对象本身,这样就可以用方法链了
2.3 putGear(int gear)设置压缩等级
public Luban putGear(int gear) {
this.gear = gear;
return this;
}
有两个压缩等级:1档
和3档
,默认为3档
,设置其他的档位是无效的
2.4 setComressListener()设置压缩进度监听
public Luban setCompressListener(OnCompressListener listener) {
compressListener = listener;
return this;
}
设置监听,OnCompressListener
内部有3个方法
public interface OnCompressListener {
//压缩开始前
void onStart();
//压缩成功后
void onSuccess(File file);
//压缩失败
void onError(Throwable e);
}
三个方法都在UI
线程,可以直接用来更新UI
2.5 launch()开启压缩方法
这个方法是Luban
中的核心方法,内部使用了RxJava
,这个方法内的重点是根据压缩档位来进行不同的操作
public Luban launch() {
checkNotNull(mFile, "the image file cannot be null, please call .load() before this method!");//用来判断null
if (compressListener != null) compressListener.onStart();
if (gear == Luban.FIRST_GEAR)//1档
Observable.just(mFile)
.map(new Func1() {
@Override
public File call(File file) {
return firstCompress(file);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(new Action1() {
@Override
public void call(Throwable throwable) {
if (compressListener != null) compressListener.onError(throwable);
}
})
.onErrorResumeNext(Observable.empty())
.filter(new Func1() {
@Override
public Boolean call(File file) {
return file != null;
}
})
.subscribe(new Action1() {
@Override
public void call(File file) {
if (compressListener != null) compressListener.onSuccess(file);
}
});
else if (gear == Luban.THIRD_GEAR)//3档
Observable.just(mFile)
.map(new Func1() {
@Override
public File call(File file) {
return thirdCompress(file);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(new Action1() {
@Override
public void call(Throwable throwable) {
if (compressListener != null) compressListener.onError(throwable);
}
})
.onErrorResumeNext(Observable.empty())
.filter(new Func1() {
@Override
public Boolean call(File file) {
return file != null;
}
})
.subscribe(new Action1() {
@Override
public void call(File file) {
if (compressListener != null) compressListener.onSuccess(file);
}
});
return this;
}
这个方法内使用了RxJava
,开启一个独立的线程来进行压缩,即使图片很大,也不会阻塞UI
线程
方法开始有一个判null
的方法,这个方法单独封装在了一个辅助工具类内
public static T checkNotNull(T reference, @Nullable Object errorMessage) {
if (reference == null) {//若null,就抛异常,并把异常提示信息显示出来
throw new NullPointerException(String.valueOf(errorMessage));
}
return reference;
}
这个方法的重中之重是thirdCompress(file)
和firstCompress(file)
,两个方法看懂一个,另一个就比较容易理解了
2.6 thirdCompress(file)3档压缩
设计思路:
源码:
private File thirdCompress(@NonNull File file) {
String thumb = mCacheDir.getAbsolutePath() + "/" + System.currentTimeMillis();//压缩后图片缓存路径
thumb = filename == null || filename.isEmpty() ? thumb : filename;//判null处理
double size;//文件大小 单位为KB
String filePath = file.getAbsolutePath();//文件的绝对路径
int angle = getImageSpinAngle(filePath);//图片的角度,为了保持所有的图片都能够竖直显示在屏幕
int width = getImageSize(filePath)[0];//图片的宽
int height = getImageSize(filePath)[1];//图片的高
int thumbW = width % 2 == 1 ? width + 1 : width;//临时宽,将宽变作偶数
int thumbH = height % 2 == 1 ? height + 1 : height;//临时高,将高变作偶数
width = thumbW > thumbH ? thumbH : thumbW;//将小的一边给width,最短边
height = thumbW > thumbH ? thumbW : thumbH;//将大的一边给height,最长边
double scale = ((double) width / height);//比例,图片短边除以长边为该图片比例
if (scale <= 1 && scale > 0.5625) {//比例在[1,0.5625)间
//判断最长边是否过界
if (height < 1664) {//最长边小于1664px
if (file.length() / 1024 < 150) return file;//如果文件的大小小于150KB
size = (width * height) / Math.pow(1664, 2) * 150;//计算文件大小
size = size < 60 ? 60 : size;//判断文件大小是否小于60KB
} else if (height >= 1664 && height < 4990) {//最长边大于1664px小于4990px
thumbW = width / 2;//最短边缩小2倍
thumbH = height / 2;//最长边缩小2倍
size = (thumbW * thumbH) / Math.pow(2495, 2) * 300;//计算文件大小
size = size < 60 ? 60 : size;//判断文件大小是否小于60KB
} else if (height >= 4990 && height < 10240) {//如果最长边大于4990px小于10240px
thumbW = width / 4;//最短边缩小2倍
thumbH = height / 4;//最长边缩小2倍
size = (thumbW * thumbH) / Math.pow(2560, 2) * 300;//计算文件大小
size = size < 100 ? 100 : size;判断文件大小是否小于100KB
} else {//最长边大于10240px
int multiple = height / 1280 == 0 ? 1 : height / 1280;//最长边与1280相比的倍数
thumbW = width / multiple;//最短边根据倍数压缩
thumbH = height / multiple;//最长边根据倍数压缩
size = (thumbW * thumbH) / Math.pow(2560, 2) * 300;//计算文件大小
size = size < 100 ? 100 : size;//判断文件大小是否小于100KB
}
} else if (scale <= 0.5625 && scale > 0.5) {//比例在[0.5625,00.5)区间
if (height < 1280 && file.length() / 1024 < 200) return file;//最长边小于1280px并且文件大小在200KB内,就返回
int multiple = height / 1280 == 0 ? 1 : height / 1280;//倍数,最长边与1280相比
thumbW = width / multiple;//最短边根据倍数压缩
thumbH = height / multiple;//最长边根据倍数压缩
size = (thumbW * thumbH) / (1440.0 * 2560.0) * 400;//计算文件大小
size = size < 100 ? 100 : size;//判断文件大小是否小于100KB
} else {//比例小于0.5
int multiple = (int) Math.ceil(height / (1280.0 / scale));//最长边乘以比例后与1280相比的结果向上取整
thumbW = width / multiple;//最短边根据倍数压缩
thumbH = height / multiple;//最长边根据倍数压缩
size = ((thumbW * thumbH) / (1280.0 * (1280 / scale))) * 500;//计算文件大小
size = size < 100 ? 100 : size;//判断文件大小是否小于100KB
}
//根据计算结果来进行压缩图片
return compress(filePath, thumb, thumbW, thumbH, angle, (long) size);
}
thumbW
和width
有区别,width
是最短边,而thumbW
是压缩目标的宽的大小
拿到计算的结果后,调用了compress()
方法
注意:比例是短边除以长边
compress()方法代码:
private File compress(String largeImagePath, String thumbFilePath, int width, int height, int angle, long size) {
//根据最终计算的宽高来压缩图片
Bitmap thbBitmap = compress(largeImagePath, width, height);
//根据拿到的图片角度,使用`Matrix`旋转图片
//有的手机照片会存在旋转90°的情况
thbBitmap = rotatingImage(angle, thbBitmap);
//保存图片在缓存文件中
return saveImage(thumbFilePath, thbBitmap, size);
}
compress(largeImagePath, width, height)
就是Bitmap
的二次采样,将Bitmap
的宽高压缩到目标大小
saveImage()代码:
/**
* 保存图片到指定路径
* Save image with specified size
*
* @param filePath the image file save path 储存路径
* @param bitmap the image what be save 目标图片
* @param size the file size of image 期望大小
*/
private File saveImage(String filePath, Bitmap bitmap, long size) {
checkNotNull(bitmap, TAG + "bitmap cannot be null");//判`null`
File result = new File(filePath.substring(0, filePath.lastIndexOf("/")));
if (!result.exists() && !result.mkdirs()) return null;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
int options = 100;
bitmap.compress(Bitmap.CompressFormat.JPEG, options, stream);//进行质量压缩,是图片文件的大小达到计算目标的大小
while (stream.toByteArray().length / 1024 > size && options > 6) {//若图片文件的大小大于目标大小,并且质量压缩率大于6
stream.reset();
options -= 6;
bitmap.compress(Bitmap.CompressFormat.JPEG, options, stream);
}
try {
FileOutputStream fos = new FileOutputStream(filePath);
fos.write(stream.toByteArray());
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
return new File(filePath);
}
代码是看完了,可有细节并不明白,比如,比例0.5625
,文件大小60KB,100KB
,质量压缩率6
,这些怎么得来的并不晓得。
不过,主要是想学习作者封装的思路和设计,细节随着经验增长,再思考了
3.最后
代码也算是看了一遍,大体是懂了。不晓得作者郑梓斌Curzibn这位大神,看到我这种水平的分析他的代码,会不会觉得把他的一些好的设计给曲解了,我哪里考虑的不对,请留言指出啊 :)
以后要多读别人的代码,多向大神们学习 ,本篇博客完整代码
本人很菜,有错误请指出
共勉 :)