編輯:關於Android編程
拍照——裁剪,或者是選擇圖片——裁剪,是我們設置頭像或上傳圖片時經常需要的一組操作。上篇講了Camera的使用,這篇講一下我對圖片裁剪的實現。
在Android中,裁剪圖片的控件庫還是挺多的,特別是github上比較流行的幾個,都已經進化到比較穩定的階段,但比較遺憾的是它們的裁剪過程是拖動或縮放裁剪框,於是只好自己再找,看有沒有現成或半成品的輪子,可以不必從零開始。
首先先了解一下上面的高仿微信裁剪控件的實現過程。說起來也不難,主要是下面幾點:
1,重寫ImageView
,並監聽手勢事件,包括雙點,兩點縮放,拖動,使它成為一個實現縮放拖動圖片功能的控件。
2,定義一個Matrix
成員變量,對於維護該圖片的縮放、平移等矩陣數據。
3,拖動或縮放時,圖片與裁剪框的相交面積一定與裁剪框相等。即圖片不能拖離裁剪框。
3,在設置圖片時,先根據圖片的大小進行初始化的縮放平移操作,使得上面第三條的條件下圖片盡可能的小。
4,每次接收到相對應的手勢事件,都進行對應的矩陣計算,並將計算結果通過ImageView
的setImageMatrix
方法應用到圖片上。
5,裁剪框是一個單獨的控件,與ImageView
同樣大,疊加到它上面顯示出來。
6,用一個XXXLayout
把裁剪框和縮放封裝起來。
7,裁剪時,先創建一個空的Bitmap並用其創建一個Canvas
,把縮放平移後的圖片畫到這個Bitmap上,並創建在裁剪框內的Bitmap
(通過調用Bitmap.createBitmap
方法)。
我拿到的代碼是鴻洋大神版本之後再被改動的,代碼上有點亂(雖然功能上是實現的裁剪)。在原有的功能上,我希望進行的改動有:
合並裁剪框的內容到ImageView中 裁剪框可以是任意長寬比的矩形 裁剪框的左右外邊距可以設置 遮罩層顏色可以設置 裁剪框下有提示文字(自己的產品需求) 後面產品又加入了一條裁剪圖片的最大大小在上面的功能需求中,我定義了以下屬性:
<code class="language-xml hljs "><declare-styleable name="ClipImageView"> <attr name="civHeight" format="integer"> <attr name="civWidth" format="integer"> <attr name="civTipText" format="string"> <attr name="civTipTextSize" format="dimension"> <attr name="civMaskColor" format="color"> <attr name="civClipPadding" format="dimension"> </attr></attr></attr></attr></attr></attr></declare-styleable></code>
其中:
civHeight
和civWidth
是裁剪框的寬高比例。 civTipText
提示文字的內容 civTipTextSize
提示文字的大小 civMaskColor
遮罩層的顏色值 civClipPadding
裁剪內邊距。由於裁剪框是在控件內部的,最終我選擇使用padding來說明裁剪框與我們控件邊緣的距離。
成員變量我進行了一些改動,把原本用於定義裁剪框的水平邊距變量及其他沒什麼用的變量等給去掉了,並加入了自己的一些成員變量,最終如下:
private final int mMaskColor;//遮罩層顏色
private final Paint mPaint;//畫筆
private final int mWidth;//裁剪框寬的大小(從屬性上讀到的整型值)
private final int mHeight;//裁剪框高的大小(同上)
private final String mTipText;//提示文字
private final int mClipPadding;//裁剪框相對於控件的內邊距
private float mScaleMax = 4.0f;//圖片最大縮放大小
private float mScaleMin = 2.0f;//圖片最小縮放大小
/**
* 初始化時的縮放比例
*/
private float mInitScale = 1.0f;
/**
* 用於存放矩陣
*/
private final float[] mMatrixValues = new float[9];
/**
* 縮放的手勢檢查
*/
private ScaleGestureDetector mScaleGestureDetector = null;
private final Matrix mScaleMatrix = new Matrix();
/**
* 用於雙擊
*/
private GestureDetector mGestureDetector;
private boolean isAutoScale;
private float mLastX;
private float mLastY;
private boolean isCanDrag;
private int lastPointerCount;
private Rect mClipBorder = new Rect();//裁剪框
private int mMaxOutputWidth = 0;//裁剪後的圖片的最大輸出寬度
構造方法裡主要是多了一些我們自定義屬性的讀取:
public ClipImageView(Context context) {
this(context, null);
}
public ClipImageView(Context context, AttributeSet attrs) {
super(context, attrs);
setScaleType(ScaleType.MATRIX);
mGestureDetector = new GestureDetector(context,
new SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
if (isAutoScale)
return true;
float x = e.getX();
float y = e.getY();
if (getScale() < mScaleMin) {
ClipImageView.this.postDelayed(new AutoScaleRunnable(mScaleMin, x, y), 16);
} else {
ClipImageView.this.postDelayed(new AutoScaleRunnable(mInitScale, x, y), 16);
}
isAutoScale = true;
return true;
}
});
mScaleGestureDetector = new ScaleGestureDetector(context, this);
this.setOnTouchListener(this);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.WHITE);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClipImageView);
mWidth = ta.getInteger(R.styleable.ClipImageView_civWidth, 1);
mHeight = ta.getInteger(R.styleable.ClipImageView_civHeight, 1);
mClipPadding = ta.getDimensionPixelSize(R.styleable.ClipImageView_civClipPadding, 0);
mTipText = ta.getString(R.styleable.ClipImageView_civTipText);
mMaskColor = ta.getColor(R.styleable.ClipImageView_civMaskColor, 0xB2000000);
final int textSize = ta.getDimensionPixelSize(R.styleable.ClipImageView_civTipTextSize, 24);
mPaint.setTextSize(textSize);
ta.recycle();
mPaint.setDither(true);
}
裁剪框是在控件正中間的,首先我們從屬性中讀取到的是寬高的比例,以及左右邊距,但是在構造方法中,由於控件還沒有繪制出來,無法獲取到控件的寬高,所以並不能計算裁剪框的大小和位置。所以我重寫了onLayout方法,在這裡計算裁剪框的位置:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
final int width = getWidth();
final int height = getHeight();
mClipBorder.left = mClipPadding;
mClipBorder.right = width - mClipPadding;
final int borderHeight = mClipBorder.width() * mHeight / mWidth;
mClipBorder.top = (height - borderHeight) / 2;
mClipBorder.bottom = mClipBorder.top + borderHeight;
}
這裡我順便把繪制提示文字的代碼也一並給出,都是在同一個方法裡的。很簡單,重寫onDraw
方法即可。繪制裁剪框有兩種方法,一是繪制一個滿屏的遮罩層,然後從中間摳出一個長方形出來,但是我用的時候發現摳不出來,所以我采用的是下面這一種:
先畫上下兩個矩形,再畫左右兩個矩形,中間所圍起來的沒有畫的部分就是我們的裁剪框。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int width = getWidth();
final int height = getHeight();
mPaint.setColor(mMaskColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(0, 0, width, mClipBorder.top, mPaint);
canvas.drawRect(0, mClipBorder.bottom, width, height, mPaint);
canvas.drawRect(0, mClipBorder.top, mClipBorder.left, mClipBorder.bottom, mPaint);
canvas.drawRect(mClipBorder.right, mClipBorder.top, width, mClipBorder.bottom, mPaint);
mPaint.setColor(Color.WHITE);
mPaint.setStrokeWidth(1);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawRect(mClipBorder.left, mClipBorder.top, mClipBorder.right, mClipBorder.bottom, mPaint);
if (mTipText != null) {
final float textWidth = mPaint.measureText(mTipText);
final float startX = (width - textWidth) / 2;
final Paint.FontMetrics fm = mPaint.getFontMetrics();
final float startY = mClipBorder.bottom + mClipBorder.top / 2 - (fm.descent - fm.ascent) / 2;
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText(mTipText, startX, startY, mPaint);
}
}
這裡我不使用全局布局的監聽(通過getViewTreeObserver
加入回調),而是直接重寫幾個設置圖片的方法,在設置圖片後進行初始顯示的設置:
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
postResetImageMatrix();
}
@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
postResetImageMatrix();
}
@Override
public void setImageURI(Uri uri) {
super.setImageURI(uri);
postResetImageMatrix();
}
private void postResetImageMatrix() {
post(new Runnable() {
@Override
public void run() {
resetImageMatrix();
}
});
}
resetImageMatrix()
方法設置圖片的初始縮放及平移,參考圖片大小,控件本身大小,以及裁剪框的大小進行計算:
/**
* 垂直方向與View的邊矩
*/
public void resetImageMatrix() {
final Drawable d = getDrawable();
if (d == null) {
return;
}
final int dWidth = d.getIntrinsicWidth();
final int dHeight = d.getIntrinsicHeight();
final int cWidth = mClipBorder.width();
final int cHeight = mClipBorder.height();
final int vWidth = getWidth();
final int vHeight = getHeight();
final float scale;
final float dx;
final float dy;
if (dWidth * cHeight > cWidth * dHeight) {
scale = cHeight / (float) dHeight;
} else {
scale = cWidth / (float) dWidth;
}
dx = (vWidth - dWidth * scale) * 0.5f;
dy = (vHeight - dHeight * scale) * 0.5f;
mScaleMatrix.setScale(scale, scale);
mScaleMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
setImageMatrix(mScaleMatrix);
mInitScale = scale;
mScaleMin = mInitScale * 2;
mScaleMax = mInitScale * 4;
}
注意:這裡有一個坑。把一個Bitmap
設置到ImageView
中,顯示時要計算的是ImageView
獲取的Drawable
對象以及這個對象的寬高,而不是Bitmap
對象。Drawable
對象可能由於對Bitmap
的放大或縮小顯示,導致它的寬或高與Bitmap
的寬高不同。
還有一點小注意:獲取控件寬高是要在控件被繪制出來之後才能獲取得到的,所以上面我通過post
一個Runnable
對象到主線程的Looper
中,保證它是在界面繪制完成之後被調用。
縮放及拖動時都需求判斷是否超出邊界,如果超出,則取允許的最終值。這裡的代碼我沒怎麼動,稍後可直接參考源碼,暫不贅述。
這裡是另外一個改造的重點了。
首先,鴻洋大神是通過創建一個空的Bitmap
,並根據它創建出一個Canvas
對象,然後通過draw
方法把縮放後的圖片給繪制到這個Bitmap中,再調用Bitmap.createBitmap
得到屬於裁剪框的內容。但是我們已經重寫了onDraw
方法畫出裁剪框,所以這裡就不考慮了。
另外,這種方法還有一個問題:它繪制的是Drawable對象。如果我們設置進去的是一個比較大的Bitmap,那麼就可能被縮放了,這裡裁剪的是縮放後的Bitmap,也就是它不是對原圖進行裁剪的。
這裡我參考了其他裁剪圖片庫,通過保存了縮放平移的Matrix
成員變量進行計算,獲取出裁剪框在其的對應范圍,並根據最終所需(我們產品要限制一個最大大小),得到最終的圖片,代碼如下:
public Bitmap clip() {
final Drawable drawable = getDrawable();
final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();
final float[] matrixValues = new float[9];
mScaleMatrix.getValues(matrixValues);
final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth();
final float transX = matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y];
final float cropX = (-transX + mClipBorder.left) / scale;
final float cropY = (-transY + mClipBorder.top) / scale;
final float cropWidth = mClipBorder.width() / scale;
final float cropHeight = mClipBorder.height() / scale;
Matrix outputMatrix = null;
if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) {
final float outputScale = mMaxOutputWidth / cropWidth;
outputMatrix = new Matrix();
outputMatrix.setScale(outputScale, outputScale);
}
return Bitmap.createBitmap(originalBitmap,
(int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight,
outputMatrix, false);
}
由於我們是對Bitmap
進行裁剪,所以首先獲取這個Bitmap
:
final Drawable drawable = getDrawable();
final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();
然後,我們的矩陣值可以通過一個包含9個元素的float
數組讀出:
final float[] matrixValues = new float[9];
mScaleMatrix.getValues(matrixValues);
比如,讀X上的縮放值,代碼為matrixValues[Matrix.MSCALE_X]
。
要特別注意一點,在前文也有提到,這裡縮放的是Drawable
對象,但是我們裁剪時用的Bitmap
,如果圖片太大的話是可能在Drawable
上進行縮放的,所以縮放大小的計算應該為:
final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth();
然後獲取圖片平移量:
final float transX = matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y];
計算裁剪框對應在圖片上的起點及寬高:
final float cropX = (-transX + mClipBorder.left) / scale;
final float cropY = (-transY + mClipBorder.top) / scale;
final float cropWidth = mClipBorder.width() / scale;
final float cropHeight = mClipBorder.height() / scale;
上面就是我們所要裁剪出來的最終結果。
但是,我前面也說的,應產品需求,要限制最大輸出大小。由於我們裁剪出來的圖片寬高比是3:2,我這裡只取寬度(你要取高度也可以)進行限制,所以又加上了如下代碼,當裁剪出來的寬度超出我們最大寬度時,進行縮放。
Matrix outputMatrix = null;
if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) {
final float outputScale = mMaxOutputWidth / cropWidth;
outputMatrix = new Matrix();
outputMatrix.setScale(outputScale, outputScale);
}
最終根據上面計算出來的值,創建裁剪出來的Bitmap:
Bitmap.createBitmap(originalBitmap,
(int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight,
outputMatrix, false);
這樣,圖片裁剪控件就算全部完成。
getClipMatrixValues
,獲取裁剪時圖片的矩陣值,它可用於做大圖的裁剪。 有關大圖的裁剪,我後續會再寫一篇。 大圖裁剪的代碼,也在上面的demo裡。 使用時可以設置裁剪框的寬高比來決定是正方形的裁剪框還是有其他比例要求的裁剪框。
今天開始陸續整理一下一些常規的Android常用開發實用程序。 第一季:Android播放動畫的方法示例 1. 通常動畫都是gif圖像,推薦使用easygifanimat
本文要演示的Android開發實例是如何完成一個Android中的miniTwitter登錄界面,下面將分步驟講解怎樣實現圖中的界面效果,讓大家都能輕松的做出美觀的登錄界
在Android實現沒有標題欄的方法有兩種:在代碼中添加requestWindowFeature(Window.FEATURE_NO_TITLE); 在清單文件Andro
關於滑動沖突在Android開發中,如果是一些簡單的布局,都很容易搞定,但是一旦涉及到復雜的頁面,特別是為了兼容小屏手機而使用了ScrollView以後,就會出現很多點擊