Android——Luban圖片壓縮算法學習
這個庫單獨使用感覺相當簡單,作者封裝的非常好,使用特方便
本篇使用的代碼是在 RxJava——基礎學習(三),簡單實踐 基礎上,添加了圖片的點擊事件。最近沒有再學習 RxJava ,因為 RxJava 正處于過渡時期, 2.0 版本要發布了,修改還蠻大的,就想等 2.0 發布后,再繼續學習 RxJava
1.簡單使用
使用RecyclerView將圖片展示出來
前兩張圖,是特意添加的兩個比較大的,不同分辨率的圖片,第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<File, File>() {
@Override
public File call(File file) {
return firstCompress(file);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
if (compressListener != null) compressListener.onError(throwable);
}
})
.onErrorResumeNext(Observable.<File>empty())
.filter(new Func1<File, Boolean>() {
@Override
public Boolean call(File file) {
return file != null;
}
})
.subscribe(new Action1<File>() {
@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<File, File>() {
@Override
public File call(File file) {
return thirdCompress(file);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
if (compressListener != null) compressListener.onError(throwable);
}
})
.onErrorResumeNext(Observable.<File>empty())
.filter(new Func1<File, Boolean>() {
@Override
public Boolean call(File file) {
return file != null;
}
})
.subscribe(new Action1<File>() {
@Override
public void call(File file) {
if (compressListener != null) compressListener.onSuccess(file);
}
});
return this;
}
這個方法內使用了 RxJava ,開啟一個獨立的線程來進行壓縮,即使圖片很大,也不會阻塞 UI 線程
方法開始有一個判 null 的方法,這個方法單獨封裝在了一個輔助工具類內
public static <T> 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 ,這些怎么得來的并不曉得。
不過,主要是想學習作者封裝的思路和設計,細節隨著經驗增長,再思考了
來自:http://www.jianshu.com/p/2bc57932468d