本篇文章為利用Matrix自定義View三部曲的第一部曲。
雖然Android內建了許多View供開發者組合和使用,但其多樣性還是不足,在很多場景或功能需求下,Android原生自帶的控制元件並不足以實現需求,這時我們就需要自定義滿足我們需求的View。
本文會講解一個自定義View的設計和開發過程,在閱讀之前希望大家有最基礎的自定義View的知識,以及Matrix
類的基本使用。
起步
在很多圖片社交的應用,例如Lofter、Play、In等應用中,都會有新增各種可愛的貼圖到圖片上的功能,然後我們可以對圖片進行移動、旋轉、縮放、翻轉之類的操作,本文製作的View正是為了實現這個功能。最終我們將要實現的效果如下圖:
簡單思考(確定大致思路)
要實現這樣的效果,我們肯定需要對圖片進行操作,在自定義的View中,我們可以在onDraw()
方法將我們的圖片(通常為Bitmap
)畫到View
上。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmap,matrix,paint);
}複製程式碼
drawBitmap()
方法有許多過載方法,但是利用Matrix來控制畫在View上的圖片是最靈活最簡單的。(不熟悉Matrix類可以先去了解下,這裡就不介紹基礎的知識了)
利用Matrix
可以方便的控制圖片的位置,旋轉角度,縮放比。
再看我們的功能,用不同的手勢來操作圖片,既然利用Matrix
可以操作圖片,那麼我們只需要在View的onTouchEvent()
方法中監聽不同的手勢操作,再對其Matrix進行變換,重繪View即可。整個思路流程就很清楚了。
仔細思考(決定結構)
有了思路,那麼我們就要來考慮我們應該怎麼樣組織程式碼,怎麼樣設計程式碼的結構。當然這個View並不複雜,設計起來也不復雜。
首先,對於貼紙功能,在沒有一張貼紙時就只顯示一張圖片,而這個功能ImageView已經為我們實現了,於是StickerView應該繼承自ImageView,並且重寫onDraw()
和onTouchEvent()
方法。
其次,因為一張圖片上可以新增多張貼紙,而每一張貼紙都需要一個Matrix來控制其相關變換,所以我們可以設計一個類封裝一下,方便對貼紙的操作。
public abstract class Sticker {
protected Matrix mMatrix;
public abstract void draw(Canvas canvas);
……
}複製程式碼
因為貼紙可能是Bitmap,也就是普通的圖片,但是我們也可以新增氣泡啊,標籤啊之類的自定義的Drawable,
當然也可能是各種圖形,為了其擴充套件性,這裡將Sticker類抽象。
擴充套件的DrawableSticker
public class DrawableSticker extends Sticker {
private Drawable mDrawable;
private Rect mRealBounds;
……
@Override
public void draw(Canvas canvas) {
canvas.save();
canvas.concat(mMatrix);
mDrawable.setBounds(mRealBounds);
mDrawable.draw(canvas);
canvas.restore();
}
……
}複製程式碼
那麼大致的結構就確定了,在View的onTouchEvent()
中,我們根據手勢改變Sticker的Matrix,並在onDraw()
方法中將Sticker畫出。
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
……
sticker.draw(canvas);
……
}複製程式碼
實現
在有了思路和一個結構後,大致已經成功了一半,接下來就是一個個功能的實現,和一遍遍的除錯了。
由於我們可以新增不止一個Sticker,所以我們的StickerView需要保有對所有新增的Sticker應用,這裡可以用一個List集合來儲存。而對於當前正在操作的Sticker引用需要額外儲存。
因為對於不同的手勢,我們所做出的操作不同,那麼我們需要在內部宣告所有存在的狀態和一個當前狀態
public class StickerView extends ImageView {
private enum ActionMode {
NONE, //nothing
DRAG, //drag the sticker with your finger
ZOOM_WITH_TWO_FINGER, //zoom in or zoom out the sticker and rotate the sticker with two finger
ZOOM_WITH_ICON, //zoom in or zoom out the sticker and rotate the sticker with icon
DELETE, //delete the handling sticker
FLIP_HORIZONTAL //horizontal flip the sticker
}
private ActionMode mCurrentMode = ActionMode.NONE;
private List<Sticker> mStickers = new ArrayList<>();
private Sticker mHandlingSticker;
……
}複製程式碼
接下來就是一個一個功能實現,但肯定的是,最先需要實現的就是將貼紙新增進來的方法。
新增貼紙
實現起來也很簡單,這裡就是new一個Sticker物件,並把它加入到我們的List中並重繪,注意,我們預設將Sticker縮放至原來的一半,並放在StickerView中央。
public void addSticker(Drawable stickerDrawable) {
Sticker drawableSticker = new DrawableSticker(stickerDrawable);
float offsetX = (getWidth() - drawableSticker.getWidth()) / 2;
float offsetY = (getHeight() - drawableSticker.getHeight()) / 2;
drawableSticker.getMatrix().postTranslate(offsetX, offsetY);
float scaleFactor;
if (getWidth() < getHeight()) {
scaleFactor = (float) getWidth() / stickerDrawable.getIntrinsicWidth();
} else {
scaleFactor = (float) getHeight() / stickerDrawable.getIntrinsicWidth();
}
drawableSticker.getMatrix().postScale(scaleFactor / 2, scaleFactor / 2, getWidth() / 2, getHeight() / 2);
mHandlingSticker = drawableSticker;
mStickers.add(drawableSticker);
invalidate();
}複製程式碼
找到貼紙
在我們的貼紙物件被新增進來後我們才可以繼續接下來的操作,在我們觸控螢幕時,要判斷是否按在貼紙區域,按在哪個貼紙上。實現比較簡單,我們的每個Sticker都有一個矩形範圍,在經過移動縮放之類的操作後也可以通過Matrix來輕鬆得到那個矩形區域(Rect
類),只需要判斷這個範圍是否包含我們按下的點,而這一步應該在Touch事件的ACTION_DOWN
事件中進行。
switch (action) {
case MotionEvent.ACTION_DOWN:
mCurrentMode = ActionMode.DRAG;
mDownX = event.getX();
mDownY = event.getY();
mHandlingSticker = findHandlingSticker();
……
}複製程式碼
其中findHandlingSticker()
正是做了這樣一些事情
private Sticker findHandlingSticker() {
for (int i = mStickers.size() - 1; i >= 0; i--) {
if (isInStickerArea(mStickers.get(i), mDownX, mDownY)) {
return mStickers.get(i);
}
}
return null;
}複製程式碼
移動貼紙
找到了我們要操作的Sticker後,我們就可以對其進行操作了,移動操作最為簡單,只涉及一根手指,在ACTION_DOWN
事件中我們記錄下當前Sticker的狀態和事件起始座標,在ACTION_MOVE
事件中,我們利用當前點的座標計算出實際偏移量,利用Matrix的postTransition()
方法讓Sticker做出隨手指的移動。
mMoveMatrix.set(mDownMatrix);
mMoveMatrix.postTranslate(event.getX() - mDownX, event.getY() - mDownY);
mHandlingSticker.getMatrix().set(mMoveMatrix);複製程式碼
縮放與旋轉貼紙
一般的縮放與旋轉操作都是需要兩根手指,所以我們需要在ACTION_POINT_DOWN
事件中監聽第二根手指按下。這時我們還需要計算出兩根手指之間的距離以及中心點還有角度,因為我們要讓Sticker以這個中心點為中心縮放旋轉,在ACTION_MOVE
事件中以新的兩指尖距離/起始兩指尖距離作為縮放比縮放。以新的角度-起始角度作為旋轉角。
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN:
mOldDistance = calculateDistance(event);
mOldRotation = calculateRotation(event);
mMidPoint = calculateMidPoint(event);
……
}複製程式碼
相應的縮放與旋轉,利用Matrix的postScale
和postRotate
方法實現
float newDistance = calculateDistance(event);
float newRotation = calculateRotation(event);
mMoveMatrix.set(mDownMatrix);
mMoveMatrix.postScale(newDistance / mOldDistance, newDistance / mOldDistance, mMidPoint.x, mMidPoint.y);
mMoveMatrix.postRotate(newRotation - mOldRotation, mMidPoint.x, mMidPoint.y);
mHandlingSticker.getMatrix().set(mMoveMatrix);複製程式碼
新增選中效果
在經過上面的步驟後,我們的StickerView已經可以新增貼紙,用手勢操縱貼紙移動,縮放,旋轉了,但是我們並沒有對選中的貼紙進行特殊處理,因為一般的應用對於選中的貼紙,都會用一個邊框圍住,並在相應的邊框邊角顯示一些操作按鈕。因為這個按鈕有圖示,所以我們也可以把其作為一個Sticker,只是還需要一個位置的x,y值。
public class BitmapStickerIcon extends DrawableSticker {
private float x;
private float y;
……
}複製程式碼
因為對於每個Sticker的邊框及其座標是很容易獲得的,所以我們只需要在onDraw
方法中在正在處理的Sticker周圍畫上邊框和按鈕就可以了。下面的程式碼獲得了選中Sticker的邊角座標,並將操作按鈕畫在相應位置。
if (mHandlingSticker != null && !mLooked) {
float[] bitmapPoints = getStickerPoints(mHandlingSticker);
float x1 = bitmapPoints[0];
float y1 = bitmapPoints[1];
float x2 = bitmapPoints[2];
float y2 = bitmapPoints[3];
float x3 = bitmapPoints[4];
float y3 = bitmapPoints[5];
float x4 = bitmapPoints[6];
float y4 = bitmapPoints[7];
canvas.drawLine(x1, y1, x2, y2, mBorderPaint);
canvas.drawLine(x1, y1, x3, y3, mBorderPaint);
canvas.drawLine(x2, y2, x4, y4, mBorderPaint);
canvas.drawLine(x4, y4, x3, y3, mBorderPaint);
float rotation = calculateRotation(x3, y3, x4, y4);
//draw delete icon
canvas.drawCircle(x1, y1, mIconRadius, mBorderPaint);
mDeleteIcon.setX(x1);
mDeleteIcon.setY(y1);
mDeleteIcon.getMatrix().reset();
mDeleteIcon.getMatrix().postRotate(
rotation, mDeleteIcon.getWidth() / 2, mDeleteIcon.getHeight() / 2);
mDeleteIcon.getMatrix().postTranslate(
x1 - mDeleteIcon.getWidth() / 2, y1 - mDeleteIcon.getHeight() / 2);
mDeleteIcon.draw(canvas);
//draw zoom icon
canvas.drawCircle(x4, y4, mIconRadius, mBorderPaint);
mZoomIcon.setX(x4);
mZoomIcon.setY(y4);
mZoomIcon.getMatrix().reset();
mZoomIcon.getMatrix().postRotate(
45f + rotation, mZoomIcon.getWidth() / 2, mZoomIcon.getHeight() / 2);
mZoomIcon.getMatrix().postTranslate(
x4 - mZoomIcon.getWidth() / 2, y4 - mZoomIcon.getHeight() / 2);
mZoomIcon.draw(canvas);
//draw flip icon
canvas.drawCircle(x2, y2, mIconRadius, mBorderPaint);
mFlipIcon.setX(x2);
mFlipIcon.setY(y2);
mFlipIcon.getMatrix().reset();
mFlipIcon.getMatrix().postRotate(
rotation, mDeleteIcon.getWidth() / 2, mDeleteIcon.getHeight() / 2);
mFlipIcon.getMatrix().postTranslate(
x2 - mFlipIcon.getWidth() / 2, y2 - mFlipIcon.getHeight() / 2);
mFlipIcon.draw(canvas);
}複製程式碼
總結
這樣,我們大致完成了StickerView的所有功能,當然上面並沒有太完整的程式碼,只是一些程式碼片段,但是已經說明了大致的思路及操作,想了解更多細節可以去檢視原始碼。我們在自定義View時,首先最需要的是一個思路,有了思路之後要想其程式碼結構,在這兩塊都想好了以後再開發其功能,會事半功倍。
希望可以對你有幫助。如果有什麼疑問,可以隨時聯絡我,歡迎提issue和pr。