Android开发技巧——大图裁剪
本篇內容是接上篇《Android開發技巧——定制仿微信圖片裁剪控件》 的,先簡單介紹對上篇所封裝的裁剪控件的使用,再詳細說明如何使用它進行大圖裁剪,包括對旋轉圖片的裁剪。
裁剪控件的簡單使用
XML代碼
使用如普通控件一樣,首先在布局文件里包含該控件:
<com.githang.clipimage.ClipImageView xmlns:app="http://schemas.android.com/apk/res-auto"android:id="@+id/clip_image_view"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_above="@+id/bottom"app:civClipPadding="@dimen/padding_common"app:civHeight="2"app:civMaskColor="@color/viewfinder_mask"app:civWidth="3"/>支持的屬性如下:
- 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度時,進行寬和高的置換。所以,除了相關的控件,我們需要定義如下相關的變量:
計算采樣代碼如下:
mClipImageView.post(new Runnable() {@Overridepublic 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的冪次方的值,例如下面代碼:
縮放及設置
接下來就是設置inJustDecodeBounds,inSampleSize,以及把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.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,完整代碼如下:
所以在原圖上的真正的裁剪框位置是:
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,它使用了維基百科上的一張世界地圖,下載地址如下:http://download.csdn.net/detail/maosidiaoxian/9464200
上面的例子截圖:
可以看出,在這個例子中,雖然在裁剪過程當中圖片被縮放過所以不太清晰,但是我們真正的裁剪是對原圖進行裁剪再進行適當的縮放的,所以裁剪之后的圖片更清晰。
本文原創,轉載請注明CSDN博客出處。
http://blog.csdn.net/maosidiaoxian/article/details/50912577
總結
以上是生活随笔為你收集整理的Android开发技巧——大图裁剪的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JS Event事件
- 下一篇: 快嘉开发框架1.0和示例介绍及使用说明