編輯:關於android開發
本篇內容是接上篇《Android開發技巧——定制仿微信圖片裁剪控件》 的,先簡單介紹對上篇所封裝的裁剪控件的使用,再詳細說明如何使用它進行大圖裁剪,包括對旋轉圖片的裁剪。
使用如普通控件一樣,首先在布局文件裡包含該控件:
支持的屬性如下:
civHeight 高度比例,默認為1 civWidth 寬度比例,默認為1 civTipText 裁剪的提示文字 civTipTextSize 裁剪的提示文字的大小 civMaskColor 遮罩層顏色 civClipPadding 裁剪框邊距如果裁剪的圖片不大,可以直接設置,就像使用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;
}
接下來就是設置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 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,它使用了維基百科上的一張世界地圖
上面的例子截圖:
可以看出,在這個例子中,雖然在裁剪過程當中圖片被縮放過所以不太清晰,但是我們真正的裁剪是對原圖進行裁剪再進行適當的縮放的,所以裁剪之後的圖片更清晰。
CAS實現SSO單點登錄原理1.CAS簡介1.1.What is CAS?CAS(Central Authentication Service) 是Yale大學發起的一個
開發首屏廣告(Android)簡述 作為一個成熟的應用, 必須要有廣告. 那麼, 如何優雅地開發廣告呢? 需要注意一些細節. 本文提供一個簡單的示例, 代碼僅供參考.
安卓第十五天筆記-圖形圖像一些簡單處理,安卓第圖形圖像安卓第十五天筆記-圖形圖像一些簡單處理 多媒體編程簡介--圖形的一般處理 1.計算機中圖形計算的大小 一般白色就是0
本文使用SAX來解析XML,在Android裡面可以使用SAX和DOM,DOM需要把整個XML文件