【輪子】Android中的圖片縮放

寫在開頭

本文只是介紹,通過安卓原生的方式將一張原始圖片縮放到合適的大小,嚴格來說是縮放圖片,而非壓縮圖片的技術
并且由于縮放后的圖片占空間還是較大,并且算法耗時較長,所以對于我的使用場景(壓縮上傳圖片)不是很好用,但是拿來做圖片墻的話還行
若想壓縮上傳圖片的話,還是推薦使用現成的庫,下面先推薦兩個圖片壓縮庫,傳送門:
AiYaCompressHelper
Luban

圖片壓縮

生產環境用戶反映,上傳身份證經常會失敗,而且很費時間,檢查代碼發現前輩的代碼寫的漿糊一般,凈出現一些w,ww,www的變量名,我知道他們都指代寬width,但原諒后輩腦子笨,實在記不住誰是誰,只能刪掉重來。
壓縮圖片的思路,無非就是以下幾步:

  1. 通過inSampleSize減少取樣點,先將圖片大概壓縮一下
  2. 通過Matrix,對圖片大小進行精確調整
  3. 改變編碼格式,將圖片轉存為PNG或者JPG

減少采樣點

減少采樣點是BitmapFactory中提供的一個方法,主要是用到了inSampleSize參數。若inSampleSize為1時,采樣后的圖片就是原始大小的圖片,若inSampleSize為2,則采樣后的圖片的寬高為原圖的1/2,像素面積為原圖的1/4,占空間也為1/4;若inSampleSize為4,則采樣后的圖片的寬高為原圖的1/4,像素面積為原圖的1/16,占空間也為1/16以此類推。
但是inSampleSize參數不能為浮點數,以及小于1的數,小于1時作為1來處理,即不能將圖片放大,因為原理上不允許。
這里有一個特殊情況,官方文檔中指出,inSampleSize參數取值應該為2的冪,即1、2、4、8等,若給的值不為2的冪,則會取一個比給的值小的最大的2的冪來代替。例如inSampleSize參數取值為10,則會用8來代替。但這個結論并非在所有系統上成立,因此此處應該嚴格控制,否則會得到意想不到的結果。

獲取采樣率的步驟遵循以下流程:

  1. BitmapFactory.Options.inJustDecodeBounds = true,若此時加載圖片,只會加載圖片的寬高信息
  2. 加載圖片,然后從BitmapFactory.Options中取出寬高信息
  3. 根據目標大小計算采樣率
  4. 可選步驟,BitmapFactory.Options.inPreferredConfig = Config.RGB_565,將圖像設為565模式,此時圖像深度為2字節。安卓中默認模式為RGB_8888,圖像深度為4字節,但大部分情況不需要圖像透明屬性
  5. BitmapFactory.Options.inJustDecodeBounds = false,重新加載圖片

需要注意的是,降低采樣點加載圖片耗時較長,一次處理大概需要200-400ms,大量圖片請使用線程池。

Matrix變換

由于修改采樣點無法將圖片縮放到一個準確的大小,所以還需要Matrix做后續處理。因為圖片在內存中存儲就是一個個像素點,Matrix可以對每個像素點進行相應的變換,即可完成對圖像的變換。
需要注意的是,為什么有了Matrix變換,還需要用更改采樣點的方式做一次預處理?因為Matrix變換需要將圖片加載進內存操作,而現在手機相機,拍一張圖像大約16M像素點,若使用RGB_8888模式加載,一張圖片大概需要60M的內存,雖然現在手機內存夠大,但一個應用程序可用內存也就幾百M,加載大量圖片還是會OOM。所以應該先減少采樣點加載圖片,做好預處理之后再用Matrix微調。

順便說一下Matrix,Matrix基本上就是一個用來操作圖片的類,但是不止是縮放,還有其他功能,比如反轉,位移,傾斜等

setTranslate(float dx,float dy):位移操作
setSkew(float kx,float ky):傾斜操作,kx、ky為X、Y方向上的比例
setSkew(float kx,float ky,float px,float py):傾斜操作,以px、py為軸心進行傾斜,kx、ky為X、Y方向上的傾斜比例
setRotate(float degrees):旋轉操作,軸心為(0,0)
setRotate(float degrees,float px,float py):旋轉操作,軸心為(px,py)
setScale(float sx,float sy):縮放操作,sx、sy為X、Y方向上的縮放比例。
setScale(float sx,float sy,float px,float py):縮放操作,以(px,py)為軸心進行縮放,sx、sy為X、Y方向上的縮放比例

不過這不是本文的重點,不再詳述。

完整代碼貼出:

import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.Base64;

import java.io.ByteArrayOutputStream;

/**
 * Created by ZhangXuan
 * 圖像縮放工具類
 */
public class ImageScalingUtil {
    /**
     * 通過降低取樣點壓縮圖片,不推薦直接使用<br/>
     * 壓縮后圖像使用RGB_565模式,即每個像素占位2字節,限定寬高壓縮<br/>
     * 由于inSampleSize壓縮比這個參數在不同手機表現不同,有的手機可以取任意整數,有的手機只能取2的冪數,則取2的冪數保證所有手機表現一致。<br/>
     * 需注意,由于inSampleSize的特性,若限定寬為1000x1000,實際圖片寬為1010x600,則該圖片會被壓縮為505x300,圖片會較小
     *
     * @param imgPath   原圖片路徑
     * @param reqWidth  最大寬度
     * @param reqHeight 最大高度
     * @return 壓縮后的bitmap
     */
    public static Bitmap reducingBitmapSampleFromPath(String imgPath, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;// 讀取大小不讀取內容
        options.inPreferredConfig = Config.RGB_565;// 設置圖片每個像素占2字節,沒有透明度
        BitmapFactory.decodeFile(imgPath, options);// options讀取圖片

        double outWidth = options.outWidth;
        double outHeight = options.outHeight;// 獲取到當前圖片寬高
        int inSampleSize = 1;

        /*
        先計算原圖片寬高比ratio=width/height,再計算限定的范圍的寬高比比reqRatio,
        若reqRatio > ratio,則說明限定的范圍更加細長,則以高為標準計算inSampleSize
        否則,則說明限定范圍更加粗矮,則以寬為計算標準
         */
        double ratio = outWidth / outHeight;
        double reqRatio = reqWidth / reqHeight;
        if (reqRatio > ratio)
            while (outHeight / inSampleSize > reqHeight) inSampleSize *= 2;
        else
            while (outWidth / inSampleSize > reqWidth) inSampleSize *= 2;

        options.inSampleSize = inSampleSize;
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(imgPath, options);
    }

    /**
     * 通過降低取樣點壓縮圖片,不推薦直接使用<br/>
     * 壓縮后圖像使用RGB_565模式,即每個像素占位2字節,限定大小壓縮<br/>
     * 由于inSampleSize壓縮比這個參數在不同手機表現不同,有的手機可以取任意整數,有的手機只能取2的冪數,則取2的冪數保證所有手機表現一致。<br/>
     * 需注意,由于inSampleSize的特性,若限定大小為500k,而原圖為501k,則壓縮后的圖片為125.25k,圖片會較小
     *
     * @param imgPath 原圖片路徑
     * @param reqSize 目標文件大小,單位為kb
     * @return 壓縮后的bitmap
     */
    public static Bitmap reducingBitmapSampleFromPath(String imgPath, int reqSize) {
        long area = reqSize * 1024 / 2;// 每個像素占2字節,將需求大小轉為像素面積

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;// 讀取大小不讀取內容
        options.inPreferredConfig = Config.RGB_565;// 設置圖片每個像素占2字節,沒有透明度
        BitmapFactory.decodeFile(imgPath, options);// options讀取圖片

        double outWidth = options.outWidth;
        double outHeight = options.outHeight;// 獲取到當前圖片寬高

        int inSampleSize = 1;
        while ((outHeight / inSampleSize) * (outWidth / inSampleSize) > area)
            inSampleSize *= 2;

        options.inSampleSize = inSampleSize;
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(imgPath, options);
    }

    /**
     * 壓縮圖片<br/>
     * 通過設定壓縮后的寬高的最大像素,將圖片等比例縮小<br/>
     * 先通過降低取樣點,將圖片壓縮到比目標寬高稍大一點,然后再通過Matrix將圖片精確調整到目標大小<br/>
     * 壓縮后圖像使用RGB_565模式,即每個像素占位2字節,限定大小壓縮<br/>
     * 若被壓縮圖片本身就小于限定大小,則不改變其大小,只更改圖像顏色模式為RGB_565<br/>
     * 由于inSampleSize壓縮比這個參數在不同手機表現不同,有的手機可以取任意整數,有的手機只能取2的冪數,則通過混合壓縮的方式保證壓縮的結果一致<br/>
     *
     * @param imgPath   原圖片路徑
     * @param reqWidth  最大寬度
     * @param reqHeight 最大高度
     * @return 壓縮后的bitmap
     */
    public static Bitmap compressBitmapFromPath(String imgPath, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;// 讀取大小不讀取內容
        options.inPreferredConfig = Config.RGB_565;// 設置圖片每個像素占2字節,沒有透明度
        BitmapFactory.decodeFile(imgPath, options);// options讀取圖片

        double outWidth = options.outWidth;
        double outHeight = options.outHeight;// 獲取到當前圖片寬高
        int inSampleSize = 1;

        /*
        先計算原圖片寬高比ratio=width/height,再計算限定的范圍的寬高比比reqRatio,
        若reqRatio > ratio,則說明限定的范圍更加細長,則以高為標準計算inSampleSize
        否則,則說明限定范圍更加粗矮,則以寬為計算標準
         */
        double ratio = outWidth / outHeight;
        double reqRatio = reqWidth / reqHeight;
        if (reqRatio > ratio)
            while (outHeight / inSampleSize > reqHeight) inSampleSize *= 2;
        else
            while (outWidth / inSampleSize > reqWidth) inSampleSize *= 2;

        options.inSampleSize = inSampleSize;
        options.inJustDecodeBounds = false;

        if (1 == inSampleSize) {
            // inSampleSize == 1,就說明原圖比要求的尺寸小或者相等,那么不用繼續壓縮,直接返回。
            return BitmapFactory.decodeFile(imgPath, options);
        }
        /*
        否則的話,先將圖片通過減少采樣點的方式,以一個比限定范圍稍大的尺寸讀入內存,
        防止因為圖片太大而OOM,以及太大的圖片加載時間過長
        然后繼續進行壓縮的步驟
        */
        options.inSampleSize = inSampleSize / 2;
        Bitmap baseBitmap = BitmapFactory.decodeFile(imgPath, options);

        /*
        使用之前計算過的寬高比,
        若reqRatio > ratio,則說明限定的范圍更加細長,則以高為標準計算壓縮比
        否則,則說明限定范圍更加粗矮,則以寬為計算標準
        */
        float compressRatio = 1;
        if (reqRatio > ratio)
            compressRatio = reqHeight * 1.0f / baseBitmap.getHeight();
        else
            compressRatio = reqWidth * 1.0f / baseBitmap.getWidth();

        Bitmap afterBitmap = Bitmap.createBitmap(
                (int) (baseBitmap.getWidth() * compressRatio),
                (int) (baseBitmap.getHeight() * compressRatio),
                baseBitmap.getConfig());
        Canvas canvas = new Canvas(afterBitmap);
        // 初始化Matrix對象
        Matrix matrix = new Matrix();
        // 根據傳入的參數設置縮放比例
        matrix.setScale(compressRatio, compressRatio);
        Paint paint = new Paint();
        // 消除鋸齒
        paint.setAntiAlias(true);
        // 根據縮放比例,把圖片draw到Canvas上
        canvas.drawBitmap(baseBitmap, matrix, paint);
        return afterBitmap;
    }

    /**
     * 壓縮圖片<br/>
     * 通過設定壓縮后的大小,將圖片等比例縮小<br/>
     * 先通過降低取樣點,將圖片壓縮到比目標寬高稍大一點,然后再通過Matrix將圖片精確調整到目標大小<br/>
     * 壓縮后圖像使用RGB_565模式,即每個像素占位2字節<br/>
     * 若被壓縮圖片本身就小于限定大小,則不改變其大小,只更改圖像顏色模式為RGB_565<br/>
     * 由于inSampleSize壓縮比這個參數在不同手機表現不同,有的手機可以取任意整數,有的手機只能取2的冪數,則通過混合壓縮的方式保證壓縮的結果一致<br/>
     *
     * @param imgPath 原圖片路徑
     * @param reqSize 壓縮后文件大小,單位為kb
     * @return 壓縮后的bitmap
     */
    public static Bitmap compressBitmapFromPath(String imgPath, int reqSize) {
        long area = reqSize * 1024 / 2;// 每個像素占2字節,將需求大小轉為像素面積

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;// 讀取大小不讀取內容
        options.inPreferredConfig = Config.RGB_565;// 設置圖片每個像素占2字節,沒有透明度
        BitmapFactory.decodeFile(imgPath, options);// options讀取圖片

        double outWidth = options.outWidth;
        double outHeight = options.outHeight;// 獲取到當前圖片寬高

        int inSampleSize = 1;
        while ((outHeight / inSampleSize) * (outWidth / inSampleSize) > area)
            inSampleSize *= 2;

        options.inSampleSize = inSampleSize;
        options.inJustDecodeBounds = false;

        if (1 == inSampleSize) {
            // inSampleSize == 1,就說明原圖比要求的尺寸小或者相等,那么不用繼續壓縮,直接返回。
            return BitmapFactory.decodeFile(imgPath, options);
        }

        /*
        否則的話,先將圖片通過減少采樣點的方式,以一個比限定范圍稍大的尺寸讀入內存,
        防止因為圖片太大而OOM,以及太大的圖片加載時間過長
        然后繼續進行壓縮的步驟
        */
        options.inSampleSize = inSampleSize / 2;
        Bitmap baseBitmap = BitmapFactory.decodeFile(imgPath, options);

        /*
        目標大小的面積與現在圖片大小的面積的比的平方根,就是縮放比
        java Math.sqrt() 函數不能開小數,而且先計算除法,再計算開放,再對結果求反誤差很大,所以做兩次開方計算
         */
        float compressRatio = 1;
        compressRatio = (float) (Math.sqrt(area) / Math.sqrt(baseBitmap.getWidth() * baseBitmap.getHeight()));

        Bitmap afterBitmap = Bitmap.createBitmap(
                (int) (baseBitmap.getWidth() * compressRatio),
                (int) (baseBitmap.getHeight() * compressRatio),
                baseBitmap.getConfig());
        Canvas canvas = new Canvas(afterBitmap);
        // 初始化Matrix對象
        Matrix matrix = new Matrix();
        // 根據傳入的參數設置縮放比例
        matrix.setScale(compressRatio, compressRatio);
        Paint paint = new Paint();
        // 消除鋸齒
        paint.setAntiAlias(true);
        // 根據縮放比例,把圖片draw到Canvas上
        canvas.drawBitmap(baseBitmap, matrix, paint);
        return afterBitmap;
    }

    /**
     * 將一張圖片 以PNG的格式 轉換成 base64 編碼
     *
     * @param bitmap
     * @return
     */
    public static String savePNGAndToBase64(Bitmap bitmap) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();// outputstream
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
        byte[] pngByte = baos.toByteArray();// 轉為byte數組
        return Base64.encodeToString(pngByte, Base64.DEFAULT);
    }

    /**
     * 將一張圖片 以JPEG的格式 轉換成 base64 編碼
     *
     * @param bitmap
     * @return
     */
    public static String saveJPEGAndToBase64(Bitmap bitmap) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();// outputstream
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        byte[] pngByte = baos.toByteArray();// 轉為byte數組
        return Base64.encodeToString(pngByte, Base64.DEFAULT);
    }
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 摘要:對android 上圖片壓縮,其實總結起來基本可以分為兩類壓縮:尺寸壓縮和質量壓縮, 尺寸壓縮其實也可以理解...
    男爵是只貓丶閱讀 8,828評論 2 14
  • 目錄介紹 01.如何計算Bitmap占用內存1.1 如何計算占用內存1.2 上面方法計算內存對嗎1.3 一個像素占...
    楊充211閱讀 4,335評論 1 9
  • 2021期待與你一起共事,點擊查看崗位[http://www.lxweimin.com/p/6f4d67fa406...
    閑庭閱讀 16,712評論 0 75
  • 圖片壓縮就是為了避免我們內存溢出,所有要對一系列進行壓縮二次采樣等 1.什么是OOM?為什么會引起OOM? out...
    lay_wn閱讀 985評論 0 1
  • 7.1 壓縮圖片 一、基礎知識 1、圖片的格式 jpg:最常見的圖片格式。色彩還原度比較好,可以支持適當壓縮后保持...
    AndroidMaster閱讀 2,554評論 0 13