Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 關於android開發 >> Android開發技巧——大圖裁剪

Android開發技巧——大圖裁剪

編輯:關於android開發

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,它使用了維基百科上的一張世界地圖

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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved