Android 自定義 View 實戰之 StickerView

本篇文章爲利用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的postScalepostRotate方法實現

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。

下一篇:Android自定義View實戰之拼圖PuzzleView

相關文章
相關標籤/搜索