Android開發技巧——大圖裁剪

來源:互聯網
上載者:User

Android開發技巧——大圖裁剪

本篇內容是接上篇《Android開發技巧——定製仿圖片裁剪控制項》 的,先簡單介紹對上篇所封裝的裁剪控制項的使用,再詳細說明如何使用它進行大圖裁剪,包括對旋轉圖片的裁剪。

裁剪控制項的簡單使用XML代碼

使用如普通控制項一樣,首先在布局檔案裡包含該控制項:

 

支援的屬性如下:

civHeight 高度比例,預設為1 civWidth 寬度比例,預設為1 civTipText 裁剪的提示文字 civTipTextSize 裁剪的提示文字的大小 civMaskColor 遮罩層顏色 civClipPadding 裁剪框邊距Java代碼

如果裁剪的圖片不大,可以直接設定,就像使用ImageView一樣,通過如下四種方法設定圖片:

mClipImageView.setImageURI(Uri.fromFile(new File(mInput)));mClipImageView.setImageBitmap(bitmap);mClipImageView.setImageResource(R.drawable.xxxx);mClipImageView.setImageDrawable(drawable);

裁剪的時候調用mClipImageView.clip();就可以返回裁剪之後的Bitmap對象。

大圖裁剪

這裡會把大圖裁剪及圖片檔案可能旋轉的情況一起處理。
注意:由於裁剪圖片最終還是需要把裁剪結果以Bitmap對象載入到記憶體中,所以裁剪之後的圖片也是會有大小限制的,否則會有OOM的情況。所以,下面會設一個裁剪後的最大寬度的值。

讀取圖片旋轉角度

在第一篇《 Android開發技巧——Camera拍照功能 》的時候,有提到過像三星的手機,豎屏拍出來的照片還是橫的,但是有Exif資訊記錄了它的旋轉方向。考慮到我們進行裁剪的時候,也會遇到類似這樣的照片,所以對於這種照片需要旋轉的情況,我選擇了在裁剪的時候才進行處理。所以首先,我們需要讀到圖片的旋轉角度:

    /**     * 讀取圖片屬性:旋轉的角度     *     * @param path 圖片絕對路徑     * @return degree旋轉的角度     */    public static int readPictureDegree(String path) {        int degree = 0;        try {            ExifInterface exifInterface = new ExifInterface(path);            int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);            switch (orientation) {                case ExifInterface.ORIENTATION_ROTATE_90:                    degree = 90;                    break;                case ExifInterface.ORIENTATION_ROTATE_180:                    degree = 180;                    break;                case ExifInterface.ORIENTATION_ROTATE_270:                    degree = 270;                    break;            }        } catch (IOException e) {            e.printStackTrace();        }        return degree;    }

如果你能確保要裁剪的圖片不大不會導致OOM的情況發生的話,是可以直接通過這個角度,建立一個Matrix對象,進行postRotate,然後由原圖建立一個新的Bitmap來得到一個正確朝向的圖片的。但是這裡考慮到我們要裁剪的圖片是從手機裡讀取的,有可能有大圖,而我們的裁剪控制項本身只實現了簡單的手勢縮放和裁剪功能,並沒有實現大圖載入的功能,所以需要在設定圖片進行之前進行一些預先處理。

採樣縮放

由於圖片較大,而我們又需要把整張圖都載入進來而不是只載入局部,所以就需要在載入的時候進行採樣,來載入縮小之後的圖片,這樣載入到的圖片較小,就能有效避免OOM了。
以前文提到的裁剪證件照為例,這裡仍以寬度為參考值來計算採樣值,具體是用寬還是高或者是綜合寬高(這種情況較多,考慮到可能會有很長的圖)來計算採樣值,還得看你具體情況。在計算採樣的時候,我們還需要用到上面讀到的旋轉值,在圖片被旋轉90度或180度時,進行寬和高的置換。所以,除了相關的控制項,我們需要定義如下相關的變數:

    private String mOutput;    private String mInput;    private int mMaxWidth;    // 圖片被旋轉的角度    private int mDegree;    // 大圖被設定之前的採樣比例    private int mSampleSize;    private int mSourceWidth;    private int mSourceHeight;

計算採樣代碼如下:

mClipImageView.post(new Runnable() {    @Override    public void run() {        mClipImageView.setMaxOutputWidth(mMaxWidth);        mDegree = readPictureDegree(mInput);        final boolean isRotate = (mDegree == 90 || mDegree == 270);        final BitmapFactory.Options options = new BitmapFactory.Options();        options.inJustDecodeBounds = true;        BitmapFactory.decodeFile(mInput, options);        mSourceWidth = options.outWidth;        mSourceHeight = options.outHeight;        // 如果圖片被旋轉,則寬高度置換        int w = isRotate ? options.outHeight : options.outWidth;        // 裁剪是寬高比例3:2,只考慮寬度情況,這裡按border寬度的兩倍來計算縮放。        mSampleSize = findBestSample(w, mClipImageView.getClipBorder().width());        //代碼未完,將下面的[縮放及設定]裡分段講到。    }});

由於我們是需要裁剪控制項的裁剪框來計算採樣,所以需要擷取裁剪框,因此我們把上面的代碼通過控制項的post方法來調用。
inJustDecodeBounds在許多講大圖縮放的部落格都有講到,相信很多朋友都清楚,本文就不贅述了。
注意:採樣的值是2的冪次方的,如果你傳的值不是2的冪次方,它在計算的時候最終會往下找到最近的2的冪次方的值。所以,如果你後面還需要用這個值來進行計算,就不要使用網上的一些直接用兩個值相除進行計算sampleSize的方法。精確的計算方式應該是直接計算時這個2的冪次方的值,例如下面代碼:

    /**     * 計算最好的採樣大小。     * @param origin 當前寬度     * @param target 限定寬度     * @return sampleSize     */    private static int findBestSample(int origin, int target) {        int sample = 1;        for (int out = origin / 2; out > target; out /= 2) {            sample *= 2;        }        return sample;    }
縮放及設定

接下來就是設定inJustDecodeBoundsinSampleSize,以及把inPreferredConfig設定為RGB_565,然後把圖片給載入進來,如下:

        options.inJustDecodeBounds = false;        options.inSampleSize = mSampleSize;        options.inPreferredConfig = Bitmap.Config.RGB_565;        final Bitmap source = BitmapFactory.decodeFile(mInput, options);

這裡載入的圖片還是沒有旋轉到正確朝向的,所以我們要根據上面所計算的角度,對圖片進行旋轉。我們豎屏拍的圖,在一些手機上是橫著儲存的,但是它會記錄一個旋轉90度的值在Exif中。如中,左邊是儲存的圖,它依然是橫著的,右邊是我們顯示時的圖。所以我們讀取到這個值後,需要對它進行順時針的旋轉。

代碼如下:

        // 解決圖片被旋轉的問題        Bitmap target;        if (mDegree == 0) {            target = source;        } else {            final Matrix matrix = new Matrix();            matrix.postRotate(mDegree);            target = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);            if (target != source && !source.isRecycled()) {                source.recycle();            }        }        mClipImageView.setImageBitmap(target);

這裡需要補充的一個注意點是:Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);這個方法返回的Bitmap不一定是重新建立的,如果matrix相同並且寬高相同,而且你沒有對Bitmap進行其他設定的話,它可能會返回原來的對象。所以在建立新的Bitmap之後,回收原來的Bitmap時要判斷是否可以回收,否則可能導致建立出來的target對象被回收而使ImageView的圖片無法顯示出來。
如上,就是完整的設定大圖時的處理過程的代碼。

裁剪

裁剪時需要建立一個裁剪之後的Bitmap,再把它儲存下來。下面介紹一下這個建立過程。完整代碼如下:

    private Bitmap createClippedBitmap() {        if (mSampleSize <= 1) {            return mClipImageView.clip();        }        // 擷取縮放位移後的矩陣值        final float[] matrixValues = mClipImageView.getClipMatrixValues();        final float scale = matrixValues[Matrix.MSCALE_X];        final float transX = matrixValues[Matrix.MTRANS_X];        final float transY = matrixValues[Matrix.MTRANS_Y];        // 擷取在顯示的圖片中裁剪的位置        final Rect border = mClipImageView.getClipBorder();        final float cropX = ((-transX + border.left) / scale) * mSampleSize;        final float cropY = ((-transY + border.top) / scale) * mSampleSize;        final float cropWidth = (border.width() / scale) * mSampleSize;        final float cropHeight = (border.height() / scale) * mSampleSize;        // 擷取在旋轉之前的裁剪位置        final RectF srcRect = new RectF(cropX, cropY, cropX + cropWidth, cropY + cropHeight);        final Rect clipRect = getRealRect(srcRect);        final BitmapFactory.Options ops = new BitmapFactory.Options();        final Matrix outputMatrix = new Matrix();        outputMatrix.setRotate(mDegree);        // 如果裁剪之後的圖片寬高仍然太大,則進行縮小        if (mMaxWidth > 0 && cropWidth > mMaxWidth) {            ops.inSampleSize = findBestSample((int) cropWidth, mMaxWidth);            final float outputScale = mMaxWidth / (cropWidth / ops.inSampleSize);            outputMatrix.postScale(outputScale, outputScale);        }        // 裁剪        BitmapRegionDecoder decoder = null;        try {            decoder = BitmapRegionDecoder.newInstance(mInput, false);            final Bitmap source = decoder.decodeRegion(clipRect, ops);            recycleImageViewBitmap();            return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), outputMatrix, false);        } catch (Exception e) {            return mClipImageView.clip();        } finally {            if (decoder != null && !decoder.isRecycled()) {                decoder.recycle();            }        }    }

下面分段介紹。

計算在採樣縮小前的裁剪框

首先,如果採樣值不大於1,也就是我們沒有進行圖片縮小的時候,就不需要進行下面的計算了,直接調用我們的裁剪控制項返回裁剪後的圖片即可。否則,就是我們對圖片進行縮放的情況了,所以會需要綜合我們的採樣值mSampleSize,計算我們的裁剪框實際上在原圖上的位置。所以會看到相對於上篇所講的裁剪控制項對裁剪框的計算,這裡多乘了一個mSampleSize的值,如下:

        // 擷取在顯示的圖片中裁剪的位置        final Rect border = mClipImageView.getClipBorder();        final float cropX = ((-transX + border.left) / scale) * mSampleSize;        final float cropY = ((-transY + border.top) / scale) * mSampleSize;        final float cropWidth = (border.width() / scale) * mSampleSize;        final float cropHeight = (border.height() / scale) * mSampleSize;

然後我們建立這個在原圖大小時的裁剪框:

        final RectF srcRect = new RectF(cropX, cropY, cropX + cropWidth, cropY + cropHeight);
計算在圖片旋轉前的裁剪框

對於大圖的裁剪,我們可以使用BitmapRegionDecoder類,來只載入圖片的一部分,也就是用它來載入我們所需要裁剪的那一部分,但是它是從旋轉之前的原圖進行裁剪的,所以還需要對這個裁剪框進行反向的旋轉,來計算它在原圖上的位置。
如所示,ABCD是旋轉90度之後的圖片,EFGH是我們的裁剪框。

但是在原圖中,它們的對應位置如所示:

也就是B點成了A點,A點成了D點,等等。
所以我們擷取EFGH在ABCD中的位置,也不能像裁剪控制項那樣,而需要進行反轉之後的計算。以旋轉90度為例,現在我們的左上方變成了F點,那麼它的left就是原來的top,它的top就是圖片的高度減去原來的right,它的right就是原來的bottom,它的bottom就是圖片的高度減去原來的left,完整代碼如下:

    private Rect getRealRect(RectF srcRect) {        switch (mDegree) {            case 90:                return new Rect((int) srcRect.top, (int) (mSourceHeight - srcRect.right),                        (int) srcRect.bottom, (int) (mSourceHeight - srcRect.left));            case 180:                return new Rect((int) (mSourceHeight - srcRect.right), (int) (mSourceWidth - srcRect.bottom),                        (int) (mSourceHeight - srcRect.left), (int) (mSourceWidth - srcRect.top));            case 270:                return new Rect((int) (mSourceWidth - srcRect.bottom), (int) srcRect.left,                        (int) (mSourceWidth - srcRect.top), (int) srcRect.right);            default:                return new Rect((int) srcRect.left, (int) srcRect.top, (int) srcRect.right, (int) srcRect.bottom);        }    }

所以在原圖上的真正的裁剪框位置是:
final Rect clipRect = getRealRect(srcRect);

局部載入所裁剪的圖片部分

大圖裁剪,我們使用BitmapRegionDecoder類,它可以只載入指定的某一部分的圖片內容,通過它的public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options)方法,我們可以把所裁剪的內容載入出來,得到一個Bitmap,這個Bitmap就是我們要裁剪的內容了。但是,我們載入的這部分內容,同樣可能太寬,所以還可能需要進行採樣縮小。如下:

    final BitmapFactory.Options ops = new BitmapFactory.Options();    final Matrix outputMatrix = new Matrix();//用於最圖圖片的精確縮放    outputMatrix.setRotate(mDegree);    // 如果裁剪之後的圖片寬高仍然太大,則進行縮小    if (mMaxWidth > 0 && cropWidth > mMaxWidth) {        ops.inSampleSize = findBestSample((int) cropWidth, mMaxWidth);        final float outputScale = mMaxWidth / (cropWidth / ops.inSampleSize);        outputMatrix.postScale(outputScale, outputScale);    }

計算出採樣值sampleSize之後,再使用它及我們計算的裁剪框,載入所裁剪的內容:

        // 裁剪        BitmapRegionDecoder decoder = null;        try {            decoder = BitmapRegionDecoder.newInstance(mInput, false);            final Bitmap source = decoder.decodeRegion(clipRect, ops);            recycleImageViewBitmap();            return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), outputMatrix, false);        } catch (Exception e) {            return mClipImageView.clip();        } finally {            if (decoder != null && !decoder.isRecycled()) {                decoder.recycle();            }        }
總結

完整代碼見github上我的clip-image項目的樣本ClipImageActivity.java。
上面例子中,我所用的圖片並不大,下面我打包了一個大圖的apk,它使用了維基百科上的一張世界地圖

上面的例子:

可以看出,在這個例子中,雖然在裁剪過程當中圖片被縮放過所以不太清晰,但是我們真正的裁剪是對原圖進行裁剪再進行適當的縮放的,所以裁剪之後的圖片更清晰。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.