本篇文章爲利用Matrix自定義View三部曲的第一部曲。java
雖然Android內置了許多View供開發者組合和使用,但其多樣性仍是不足,在不少場景或功能需求下,Android原生自帶的控件並不足以實現需求,這時咱們就須要自定義知足咱們需求的View。git
本文會講解一個自定義View的設計和開發過程,在閱讀以前但願你們有最基礎的自定義View的知識,以及Matrix
類的基本使用。github
在不少圖片社交的應用,例如Lofter、Play、In等應用中,都會有添加各類可愛的貼圖到圖片上的功能,而後咱們能夠對圖片進行移動、旋轉、縮放、翻轉之類的操做,本文製做的View正是爲了實現這個功能。最終咱們將要實現的效果以下圖:canvas
項目地址:github.com/wuapnjie/St…ide
要實現這樣的效果,咱們確定須要對圖片進行操做,在自定義的View中,咱們能夠在onDraw()
方法將咱們的圖片(一般爲Bitmap
)畫到View
上。post
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmap,matrix,paint);
}複製代碼
drawBitmap()
方法有許多重載方法,可是利用Matrix來控制畫在View上的圖片是最靈活最簡單的。(不熟悉Matrix類能夠先去了解下,這裏就不介紹基礎的知識了)spa
利用Matrix
能夠方便的控制圖片的位置,旋轉角度,縮放比。設計
再看咱們的功能,用不一樣的手勢來操做圖片,既然利用Matrix
能夠操做圖片,那麼咱們只須要在View的onTouchEvent()
方法中監聽不一樣的手勢操做,再對其Matrix進行變換,重繪View便可。整個思路流程就很清楚了。調試
有了思路,那麼咱們就要來考慮咱們應該怎麼樣組織代碼,怎麼樣設計代碼的結構。固然這個View並不複雜,設計起來也不復雜。rest
首先,對於貼紙功能,在沒有一張貼紙時就只顯示一張圖片,而這個功能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。