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

 

 本文由用戶 as11051105 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!