本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

本篇文章我們來實現一個帶有彈性滑動效果的自定義View。當然,文章的側重點是自定義View但也會涉及到View的事件分發以及一些其他方面的知識,例如使用Scroller實現帶有阻尼效果的彈性滑動。因此,我相信看完這篇文章你不僅能學到自定義View的相關知識,還會了解到View的事件分發!還是老規矩,看下最終實現效果。
這裡寫圖片描述
分析圖中效果會發現其核心功能類似於一個簡單的下拉重新整理、上拉載入的框架,但又有區別。開始前還是先來羅列一下幾個核心步驟,如下:
一. 明確需求,確定對外開放的介面
二. 分析滑動效果,初步實現控制元件佈局
三. 關於滑動,不得不說的事件分發
四. 實現自定義CircleWaveView

一. 明確需求,確定對外開放介面

首先應該明確控制元件的需求,確定有哪些功能,然後做針對性開發。這裡先貼出該控制元件的使用方法,也是為了更好地認識控制元件的需求。
1.佈局檔案新增

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.zhpan.lockview.view.LockView
        android:id="@+id/lock_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

</RelativeLayout>

2.設定操作的監聽事件。程式碼如下:

    mLockView.setOnLockOperateListener(new OnLockOperateListener() {
         @Override
         public void onLockPrepared() {//  上鎖就緒

         }

         @Override
         public void onUnLockPrepared() {//  開鎖就緒

         }

         @Override
         public void onLockStart() {// 開始上鎖

         }

         @Override
         public void onUnlockStart() {// 開始開鎖

         }

         @Override
         public void onNotPrepared() {// 上下滑動距離未達到就緒狀態

         }
     });

3.對外開放介面

// 設定藍芽是否連線
mLockView.setBluetoothConnect(false);
// 設定上鎖狀態
mLockView.setLockState(isLock);
// 設定View是否可以滑動
mLockView.setCanSlide(true)
// 設定滑動阻尼大小
mLockView.setDamping(1.7)
// 設定View中心文字
mLockView.setText("已上鎖");
// 設定中心大圓的顏色
mLockView.setCircleColor
// 開啟心跳動畫
mLockView.startWave();
// 停止心跳動畫
mLockView.stopWave();
// 是否正在搜尋/連線藍芽
mLockView.connecting(true);

// 點選事件監聽(只有在未連線藍芽時有效)
mLockView.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {

         }
     });

我們來總結下控制元件中需要實現的功能:

  1. 控制元件佈局的實現。
  2. 藍芽未連線時,只能點選而不能滑動。
  3. 點選事件以及連線中動畫。
  4. 更改連線狀態。
  5. 實現上下彈性滑動,且需要控制滑動邊界。
  6. 滑動事件回掉。
  7. 心跳動畫實現。

以上幾點就是我們要完成的核心功能,有了需求之後就直接進入主題來實現我們想要的效果吧。

二、分析控制元件,初步實現控制元件佈局

分析上圖的效果發現,中間的View是可滑動的,且覆蓋在上下小圓點的上面。對於這種效果直接繼承View實現起來會不太方便。因此我們可以想到利用自定義ViewGroup來佈局頁面。這麼一來使開發簡單了許多。那麼接下來先新建一個layout_oval_lock.xml的佈局為檔案,並採用FrameLayout來佈局控制元件,這樣就實現了層次疊加效果,FrameLayout內部是兩個自定義View,我們可以暫且擱置不管,後面會講到如何實現。佈局檔案如下:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical">

    <com.zhpan.lockview.view.CircleView
        android:id="@+id/green_cv"
        android:layout_width="15dp"
        android:layout_height="15dp"
        android:layout_marginTop="110dp"
        app:circle_color="@color/green"
        android:layout_gravity="center"/>

    <com.zhpan.lockview.view.CircleView
        android:id="@+id/red_cv"
        android:layout_width="15dp"
        android:layout_height="15dp"
        app:circle_color="@color/red"
        android:layout_marginTop="-110dp"
        android:layout_gravity="center"/>

    <com.zhpan.lockview.view.CircleWaveView
        android:id="@+id/circle_wave_view"
        android:layout_width="220dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        android:padding="20dp"/>

    <ProgressBar
        android:id="@+id/progress"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:visibility="gone"
        android:indeterminateTint="@color/white"
        android:layout_gravity="center"/>
</FrameLayout>

接下來新建一個LockView類並繼承FrameLayout。LockView與 上邊layout_oval_lock的佈局檔案關聯,並重寫相應的方法。程式碼如下:

public LockView(Context context) {
        this(context, null);
    }

    public LockView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LockView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        View view = View.inflate(context, R.layout.layout_oval_lock, this);
        mCircleWaveView = (CircleWaveView) view.findViewById(R.id.circle_wave_view);
        mCircleView = (CircleView) view.findViewById(R.id.green_cv);
        distance = ((LayoutParams) mCircleView.getLayoutParams()).topMargin;
        mProgressBar = (ProgressBar) view.findViewById(R.id.progress);
        mScroller = mCircleWaveView.getScroller();
        mContext = context;
        mCircleWaveView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
            }
        });
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        View view = getChildAt(0);
        view.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

三.關於滑動,不得不說的事件分發

接下來就要來處理中心View的滑動了!說到滑動,避免不了的就應該想到Android中View的事件分發,那麼對於滑動事件的處理我們需要重寫三個方法。我想很多小夥伴肯定已經想到了!沒錯,就是事件分發的三個核心方法:dispatchTouchEvent、onInterceptTouchEvent、以及onTouchEvent。我覺得還是先簡單來了解一下這三個方法吧,因為它確實挺重要的。

  • dispatchTouchEvent 顧名思義,這個方法就是用來對事件進行分發的。如果事件傳遞到了當前View,那麼這個方法一定會被呼叫。它的返回結果受當前View的onTouchEvent或下級View的dispatchTouchEvent方法的影響,表示是否消費當前事件。
  • onInterceptTouchEvent 這個方法在dispatchTouchEvent方法的內部被呼叫,用來表示是否攔截某個事件。返回結果表示是否攔截當前事件。需要注意的是View並沒有該方法,這個方法僅僅存在於ViewGroup中!如果事件傳遞到View中,那麼會直接呼叫該View的onTouchEvent方法。
  • onTouchEvent 這個方法在dispatchTouchEvent方法中呼叫。用來處理點選事件。返回結果表示是否消費當前事件。View中的onTouchEvent方法預設會消費事件,只有當設定clickable和longClickable為false時則不會消費該事件。

首先來看LockView中重寫的dispatchTouchEvent方法中的程式碼

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!canSlide)
            switch (ev.getAction()) {
                case ACTION_DOWN:
                    timestamp = System.currentTimeMillis();
                    break;
                case ACTION_UP:
                    if (System.currentTimeMillis() - timestamp < 500) {
                        performClick();
                        return true;
                    }
                    break;
            }
        return super.dispatchTouchEvent(ev);
    }

上面提到,只要有事件傳遞到當前的ViewGroup那麼dispatchTouchEvent就會首先被呼叫!因此在這個方法裡先來判斷當前是否是可以滑動狀態(藍芽未連線時不可滑動)。如果不可以滑動,那麼就去處理點選事件,我們認為ACTION_DOWN和ACTION_UP之間間隔小於500毫秒就是一次點選事件,那麼就在此處呼叫performClick方法並消費掉當前事件,如果間隔大於500毫秒,不認為是點選事件,那麼緊接著就去呼叫父類的dispatchTouchEvent方法。如果當前可以滑動,那麼同樣呼叫父類的dispatchTouchEvent方法來處理。

接下來我們看重寫的onInterceptTouchEvent方法


@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = true;
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case ACTION_DOWN:
                intercepted = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(y - mLastY) > mTouchSlop) {
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        mLastY = y;
        return intercepted;
    }

在這個方法中我們重點來看ACTION_MOVE的時候,在這裡先判斷了滑動的距離是否大於mTouchSlop,這個值是認為滑動的最小距離,當大於這個值的時候就認為是滑動了。那麼看此時intercepted返回了true,表示要攔截這個事件!此處攔截了這個滑動事件會怎麼樣呢?答案是當前View中的onTouchEvent方法被呼叫了!現在請將我們的目光聚焦到onTouchEvent方法中,注意前方高能!

核心中最核心的onTouchEvent方法


    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int y = (int) event.getY();
        int scrollY = mCircleWaveView.getScrollY();
        switch (event.getAction()) {
            case ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (!canSlide) {
                    return super.onTouchEvent(event);
                }
                int deltaY = (int) ((mLastY - y) / damping);
                if (mCircleWaveView.getScrollY() > mTouchSlop) {
                    mOption = Option.LOCK;
                } else if (mCircleWaveView.getScrollY() < -mTouchSlop) {
                    mOption = Option.UNLOCK;
                }
                if (Math.abs(scrollY) > (distance - mCircleWaveView.getRadius() + mCircleView.getRadius())) {
                    if (mOption != null) {
                        switch (mOption) {
                            case LOCK:
                                if (mOnLockOperateListener != null)
                                    mOnLockOperateListener.onLockPrepared();
                                mCircleWaveView.setLockPrepared(true);
                                break;
                            case UNLOCK:
                                if (mOnLockOperateListener != null)
                                    mOnLockOperateListener.onUnLockPrepared();
                                mCircleWaveView.setUnLockPrePared(true);
                                break;
                        }
                    }
                } else {
                    mCircleWaveView.setUnLockPrePared(false);
                    mCircleWaveView.setLockPrepared(false);
                    mOnLockOperateListener.onNotPrepared();
                   /* if (isLock()) {
                        mCircleWaveView.setText(mContext.getResources().getString(R.string.device_control_unlock));
                    } else {
                        mCircleWaveView.setText(mContext.getResources().getString(R.string.device_control_lock));
                    }*/
//                    isOperating = false;
                }

                /**
                 * 控制滑動邊界
                 */
                int border = (distance - mCircleWaveView.getRadius() + mCircleView.getRadius()) +
                        DensityUtils.dp2px(mContext, 25);//  可上下滑動的最大距離
                //  當前上下滑動的距離
                int slideHeight = deltaY + mCircleWaveView.getScrollY();
                if (slideHeight > border) {
                    mCircleWaveView.scrollTo(0, border);
                    return true;
                } else if (slideHeight + border < 0) {
                    mCircleWaveView.scrollTo(0, -border);
                    return true;
                }
                mCircleWaveView.scrollBy(0, deltaY);
                break;
            case MotionEvent.ACTION_UP:
                mCircleWaveView.setUnLockPrePared(false);
                mCircleWaveView.setLockPrepared(false);
                scrollY = mCircleWaveView.getScrollY();
                if (Math.abs(scrollY) > (distance - mCircleWaveView.getRadius() + mCircleView.getRadius()) && mOption != null) {
                    switch (mOption) {
                        case LOCK:
                            if (mOnLockOperateListener != null)
                                mOnLockOperateListener.onLockStart();
                            break;
                        case UNLOCK:
                            if (mOnLockOperateListener != null)
                                mOnLockOperateListener.onUnlockStart();
                            break;
                    }
                }
                mCircleWaveView.smoothScroll(0, 0);
                break;
        }
        mLastY = y;
        return super.onTouchEvent(event);
    }

看到這個方法中這麼多程式碼不知道各位是否已經懵逼?(好吧,我承認,這地方程式碼寫的確實比較亂)不過沒關係,其實細細分析來還是不難理解的!同樣,我們選擇比較重要的點來看。首先來看ACTION_MOVE的時候,在這裡先判斷了是否可以滑動(其實不可以滑動的情況下應該不會走到這個方法,但是為了嚴謹還是加了判斷),如果不能滑動則下邊的邏輯全都不會再走了。接下來
通過判斷滑動的方向來確定是要開鎖還是上鎖,並根據滑動距離來給出回撥處理。即當中心圓CircleWaveView向上或向下滑動並完全覆蓋到上/下的小圓點時則會回掉上鎖就緒或者開鎖就緒(onLockPrepared、onUnLockPrepared)的方法。此時釋放CircleWaveView,則會回撥開鎖或者上鎖(onLockStart、onUnlockStart)的方法。如果CircleWaveView在完全覆蓋到上/下的小圓點的狀態下,再向反方向滑動至未完全覆蓋小圓點,此時則會回掉未就緒(onNotPrepared)的方法。下邊貼一下回調介面,一共五個方法,如下:

public interface  OnLockOperateListener {
   // 上鎖就緒
   void onLockPrepared();
   // 開鎖就緒
   void onUnLockPrepared();
   // 開始上鎖
   void onLockStart();
   // 開始開鎖
   void onUnlockStart();
   // 未就緒
   void onNotPrepared();
}

接下來是通過一系列計算來控制CircleWaveView的滑動邊界。思路大致如此:首先根據CircleWaveView和上下小圓的位置來計算出可上下滑動的最大距離border。然後計算當CircleWaveView滑動的距離超過border時就強制將其滾動到border位置,已達到固定的效果。程式碼如下:

    /**
      * 控制滑動邊界
      */
    int border = (distance - mCircleWaveView.getRadius() + mCircleView.getRadius()) +
                        DensityUtils.dp2px(mContext, 25);//  可上下滑動的最大距離
    int deltaY = (int) ((mLastY - y) / damping);
    //  當前上下滑動的距離
    int slideHeight = deltaY + mCircleWaveView.getScrollY();
    if (slideHeight > border) {
         mCircleWaveView.scrollTo(0, border);
         return true;
        } else if (slideHeight + border < 0) {
          mCircleWaveView.scrollTo(0, -border);
          return true;
        }
    mCircleWaveView.scrollBy(0, deltaY);

然後是實現CircleWaveView的彈性滑動,這裡我們給CircleWaveView加了一個彈性滑動和阻尼效果。彈性滑動是在CircleWaveView中通過Scroller來實現的,CircleWaveView暴漏出來smoothScroll的彈性滑動介面供在LockView中呼叫。這點我們在後面講解CircleWaveView時再說。而阻尼滑動則是將原滑動距離除以阻尼係數以減小滑動距離從而產生阻尼效果。
最後來看ACTION_UP,同樣是根據Y軸滑動距離與滑動方向回掉對應的方法,並將CircleWaveView恢復到原位。程式碼如下:

 scrollY = mCircleWaveView.getScrollY();
 if (Math.abs(scrollY) > (distance - mCircleWaveView.getRadius() + mCircleView.getRadius()) && mOption != null) {
    switch (mOption) {
          case LOCK:
               if (mOnLockOperateListener != null)
                    mOnLockOperateListener.onLockStart();
           break;
           case UNLOCK:
               if (mOnLockOperateListener != null)
                    mOnLockOperateListener.onUnlockStart();
           break;
     }
 mCircleWaveView.smoothScroll(0, 0);

四. 實現自定義CircleWaveView

關於自定義CircleWaveView就不具體來講了,因為關於自定義View都是一樣的步驟。這裡我們只選取幾個重要的地方來說。1.CircleWaveView中內容的繪製。2.關於彈性滑動的實現。3.心跳動畫的實現以及狀態改變的擴散動畫。

1.CircleWaveView中內容的繪製。

繪製主體圓。主要分為幾種情況:
a.藍芽未連線,且未能獲取到網路資料,背景色為灰色。
b.藍芽未連線,且能獲取到網路資料,背景色為淡綠色或淡紅色。
c.藍芽已連線,開鎖狀態為綠色,未開鎖狀態為紅色。
d.上拉上鎖就緒狀態為深紅色,下拉開鎖就緒狀態為深綠色。

結合以上需求有如下程式碼:

private void drawCircle(Canvas canvas) {
        mPaint.setColor(circleColor);
        int verticalCenter = getHeight() / 2;
        int horizontalCenter = getWidth() / 2;
        int mRadius = Math.min(verticalCenter, horizontalCenter) - Math.min(verticalCenter, horizontalCenter) / 5;
        radius = Math.min(verticalCenter, horizontalCenter) - Math.min(verticalCenter, horizontalCenter) / 5;
        if (transforming) {
            mPaint.setColor(getResources().getColor(R.color.green));
            canvas.drawCircle(mPieCenterX, mPieCenterY, mRadius, mPaint);
            mRadius = isLock ? transformDelta : mRadius - transformDelta;
            mPaint.setColor(getResources().getColor(R.color.red));
            canvas.drawCircle(mPieCenterX, mPieCenterY, mRadius, mPaint);
        } else {
            mRadius = mRadius - waveDelta;
            if (!isBluetoothConnect) {
                if (isNoNetData) {
                    mPaint.setColor(getColor(R.color.gray));
                } else
                    mPaint.setColor(isLock ? getColor(R.color.redLight) : getColor(R.color.greenLight));
            } else {
                if (isLockPrepared) {
                    mPaint.setColor(getColor(R.color.redDark));
                } else if (isUnLockPrePared) {
                    mPaint.setColor(getColor(R.color.greenDark));
                } else {
                    mPaint.setColor(isLock ? getColor(R.color.red) : getColor(R.color.green));
                }
            }
            canvas.drawCircle(mPieCenterX, mPieCenterY, mRadius, mPaint);
        }
    }

繪製CircleWaveView中上下箭頭。 關於箭頭繪製,註釋部分是通過Path來繪製的,但是發現效果不太好,繪製三角形圓角比較麻煩,所以後臺改為了直接在canvas上繪製Bitmap來實現。程式碼如下:

//  繪製圓中兩個三角
    private void drawTriangle(Canvas canvas) {
        int left = (mWidth - arrowUp.getWidth()) / 2;
        canvas.drawBitmap(arrowUp, left, mHeight / 2 - radius + dp13, mPaint);
        canvas.drawBitmap(arrowDown, left, mHeight / 2 + radius - dp13 - arrowDown.getHeight(), mPaint);
        /*int radius = Math.min(mHeight, mWidth) / 2 - Math.min(mHeight, mWidth) / 8;
        mPaintTrangel.setStyle(Paint.Style.FILL);
        mPaintTrangel.setShadowLayer(4, 0, 3, Color.GRAY);
        //  三角形頂點到圓邊的距離
        int h0 = DensityUtils.dp2px(mContext, 10);
        //  三角形高
        int h1 = DensityUtils.dp2px(mContext, 12);
        //  三角形底邊長
        int w = DensityUtils.dp2px(mContext, 14);
        mPaintTrangel.setColor(getResources().getColor(R.color.transparent_33));
        mPath.moveTo(mWidth / 2, mHeight / 2 - (radius - h0));
        mPath.lineTo(mWidth / 2 - w, mHeight / 2 - (radius - h1 - h0));
        mPath.lineTo(mWidth / 2 + w, mHeight / 2 - (radius - h1 - h0));
        canvas.drawPath(mPath, mPaintTrangel);
        mPaintTrangel.setShadowLayer(4, 0, -3, Color.GRAY);
        mPath.moveTo(mWidth / 2, mHeight / 2 + (radius - h0));
        mPath.lineTo(mWidth / 2 - w, mHeight / 2 + (radius - h1 - h0));
        mPath.lineTo(mWidth / 2 + w, mHeight / 2 + (radius - h1 - h0));
        canvas.drawPath(mPath, mPaintTrangel);*/
    }

繪製CircleWaveView中心的文字 中心文字分為兩種情況:當藍芽未連線時,顯示為兩行。當藍芽已連線時顯示為一行。繪製思路是先計算出中心基線,然後再來分情況實現。程式碼如下:

//  繪製圓中的文字
    private void drawText(Canvas canvas) {
        if (isConnecting) return;
        if (TextUtils.isEmpty(mText)) { //   繪製單行文字
            String text = mContext.getResources().getString(R.string.ble_not_connect);
            canvas.drawText(text, mPieCenterX, getBaseline(text), mPaintText);
            return;
        }
        if (isBluetoothConnect) {   //   繪製單行文字
            canvas.drawText(mText, mPieCenterX, getBaseline(mText), mPaintText);
        } else {    //   繪製兩行文字
            String text = mContext.getResources().getString(R.string.ble_not_connect);
            int baseline = getBaseline(text);
            canvas.drawText(text, mPieCenterX, baseline - 30, mPaintText);
            mPaintText.setTextSize(DensityUtils.dp2px(mContext, 12));
            canvas.drawText(mText, mPieCenterX, baseline + 30, mPaintText);
        }
    }

    private int getBaseline(String text) {
        mPaintText.setTextSize(mTextSize);
        mPaintText.getTextBounds(text, 0, text.length(), bounds);
        Paint.FontMetricsInt fontMetricsInt = mPaintText.getFontMetricsInt();
        return (getMeasuredHeight() - fontMetricsInt.bottom + fontMetricsInt.top) / 2
                - fontMetricsInt.top;
    }

2.關於彈性滑動的實現。 關於滑動,通常我們會想到用ScrollTo或ScrollBy來實現。但由於這兩個方法實現滑動都是瞬間完成的,因此滑動看起來會比較生硬,體驗也很不好。因此想到可以使用Scroller來做一個滑動延遲實現帶有彈性效果的滑動。由於Scroller本身時無法實現滑動的,因此還必須配合computeScroll方法共同完成。這裡我們封裝一個smoothScroll方法,提供給LockView呼叫。具體程式碼如下:

public void smoothScroll(int destX, int destY) {
        int scrollY = getScrollY();
        int delta = destY - scrollY;
        mScroller.startScroll(destX, scrollY, 0, delta, 400);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

3.心跳動畫的實現以及狀態改變的擴散動畫。

首先來看心跳動畫的實現,這裡使用屬性動畫,首先為ValueAnimator設定從0到1再到0的一個數值變化,並且週期時間設定為600毫秒、重複次數設定為ValueAnimator.INFINITE,即為無限次迴圈。通過ValueAnimator中不斷變化的value來計算圓半徑的大小,並通過invalidate()方法不斷重繪View,從而達到一個心跳動畫的效果。程式碼如下:

//  開始心跳動畫
public void startWave() {
        if (animator != null && animator.isRunning())
            animator.end();
        animator = ValueAnimator.ofFloat(0f, 1f, 0f);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setRepeatMode(ValueAnimator.RESTART);
        animator.setDuration(600);

        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int verticalCenter = getHeight() / 2;
                int horizontalCenter = getWidth() / 2;
                waveDelta = (int) (Math.min(verticalCenter, horizontalCenter) * (float) animation.getAnimatedValue() / 16);
                invalidate();
            }
        });

        animator.start();
    }
 //  停止心跳動畫
 public void stopWave() {
        if (animator != null && animator.isRunning())
            animator.end();
    }

接下來看狀態改變時擴散動畫的實現,其實方法和心跳動畫一樣,都是採用ValueAnimator動態計算繪製圓的半徑,不在贅述。參考如下程式碼:

public void changeLockState(final boolean lock) {
        stopWave();
        if (this.isLock != lock) {
            transforming = true;
            ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 0.99f);
            valueAnimator.setDuration(500);
            valueAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {

                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    transforming = false;
                    isLock = lock;
                    invalidate();
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    transforming = false;
                }

                @Override
                public void onAnimationRepeat(Animator animation) {

                }
            });
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    int verticalCenter = getHeight() / 2;
                    int horizontalCenter = getWidth() / 2;
                    transformDelta = (int) ((Math.min(verticalCenter, horizontalCenter) - Math.min(verticalCenter, horizontalCenter) / 6)
                            * (float) animation.getAnimatedValue());
                    invalidate();
                }
            });
            valueAnimator.start();
        }
    }

至此,關於LockView的繪製到這裡就完全結束了。回顧一下本篇文章,重講解了自定義LockView以及彈性滑動實現,然後探討了關於事件分發的一些知識以及使用屬性動畫來實現心跳和擴散效果。相信看完本篇文章的小夥伴也會有不小的收穫,最後關於原始碼,已放在文章末尾,歡迎start、forck!

原始碼連結