讓控制元件如此絲滑Scroller和VelocityTracker的API講解與實戰——Android高階UI

猛猛的小盆友發表於2019-03-06

目錄

一、前言

二、Scroller

三、VelocityTracker

四、實戰——帶慣性滑動的柱狀圖

五、寫在最後

一、前言

自定義控制元件中,難免會遇到需要滑動的場景。而Canvas提供的scrollTo和scrollBy方法只能達到移動的效果,需要達到真正的滑動便需要我們今天分享的兩把基礎利器ScrollerVelocityTracker。老規矩,先上實戰圖,再進行分享。

帶慣性滑動的柱狀圖

讓控制元件如此絲滑Scroller和VelocityTracker的API講解與實戰——Android高階UI

二、Scroller

1、作用

童鞋們可以先看下下面這段官方的英文類註釋。小盆友以自己的理解給出這個類的作用是,Scroller 是一個讓檢視 滾動起來的工具類,負責根據我們提供的資料計算出相應的座標,但是具體的滾動邏輯還是由我們程式猿來進行 移動內容 實現(?為啥說是移動內容,我們在實戰一節中便知道了,稍安勿躁)。

* <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller}
* or {@link OverScroller}) to collect the data you need to produce a scrolling
* animation&mdash;for example, in response to a fling gesture. Scrollers track
* scroll offsets for you over time, but they don't automatically apply those
* positions to your view. It's your responsibility to get and apply new
* coordinates at a rate that will make the scrolling animation look smooth.</p>
複製程式碼

2、API講解

這一小節是對 Scroller構造方法常用的公有方法 進行講解,如果您已經對這些方法很熟悉,可以跳過。

構造方法

(1) Scroller(Context context)

public Scroller(Context context) 
複製程式碼

方法描述:

建立一個 Scroller 例項。

引數解析:

第一個引數 context: 上下文;

(2) Scroller(Context context, Interpolator interpolator)

public Scroller(Context context, Interpolator interpolator)
複製程式碼

方法描述:

建立一個 Scroller 例項。

引數解析:

第一個引數 context: 上下文;

第二個引數 interpolator: 插值器,用於在 computeScrollOffset 方法中,並且是在 SCROLL_MODE 模式下,根據時間的推移計算位置。為null時,使用預設 ViscousFluidInterpolator 插值器。

(3) Scroller(Context context, Interpolator interpolator, boolean flywheel)

public Scroller(Context context, Interpolator interpolator, boolean flywheel)
複製程式碼

方法描述:

建立一個 Scroller 例項。

引數解析:

第一個引數 context: 上下文;

第二個引數 interpolator: 插值器,用於在 computeScrollOffset 方法中,並且是在 SCROLL_MODE 模式下,根據時間的推移計算位置。為null時,使用預設 ViscousFluidInterpolator 插值器。

第三個引數 flywheel: 支援漸進式行為,該引數只作用於 FLING_MODE 模式下。

常用公有方法

(1) setFriction(float friction)

public final void setFriction(float friction) 
複製程式碼

方法描述:

用於設定在 FLING_MODE 模式下的摩擦係數

引數解析:

第一個引數 friction: 摩擦係數

(2) isFinished()

public final boolean isFinished()
複製程式碼

方法描述:

滾動是否已結束,用於判斷 Scroller 在滾動過程的狀態,我們可以做一些終止或繼續執行的邏輯分支。

(3) forceFinished(boolean finished)

public final void forceFinished(boolean finished) 
複製程式碼

方法描述:

強制的讓滾動狀態置為我們所設定的引數值 finished 。

(4) getDuration()

public final int getDuration()
複製程式碼

方法描述:

返回 Scroller 將持續的時間(以毫秒為單位)。

(5) getCurrX()

public final int getCurrX()
複製程式碼

方法描述:

返回滾動中的當前X相對於原點的偏移量,即當前座標的X座標。

(6) getCurrY()

public final int getCurrY()
複製程式碼

方法描述:

返回滾動中的當前Y相對於原點的偏移量,即當前座標的Y座標。

(7) getCurrVelocity()

public float getCurrVelocity() 
複製程式碼

方法描述:

獲取當前速度。

(8) computeScrollOffset()

public boolean computeScrollOffset()
複製程式碼

方法描述:

計算滾動中的新座標,會配合著 getCurrXgetCurrY 方法使用,達到滾動效果。值得注意的是,如果返回true,說明動畫還未完成。相反,返回false,說明動畫已經完成或是被終止了。

(9) startScroll

public void startScroll(int startX, int startY, int dx, int dy) 

public void startScroll(int startX, int startY, int dx, int dy, int duration)
複製程式碼

方法描述:

通過提供起點,行程距離和滾動持續時間,進行滾動的一種方式,即 SCROLL_MODE。該方法可以用於實現像ViewPager的滑動效果。

引數解析:

第一個引數 startX: 開始點的x座標

第二個引數 startY: 開始點的y座標

第三個引數 dx: 水平方向的偏移量,正數會將內容向左滾動。

第四個引數 dy: 垂直方向的偏移量,正數會將內容向上滾動。

第五個引數 duration: 滾動的時長

(10) fling

public void fling(int startX, int startY, int velocityX, int velocityY,
                      int minX, int maxX, int minY, int maxY)
複製程式碼

方法描述:

用於帶速度的滑動,行進的距離將取決於投擲的初始速度。可以用於實現類似 RecycleView 的滑動效果。

引數解析: 第一個引數 startX: 開始滑動點的x座標

第二個引數 startY: 開始滑動點的y座標

第三個引數 velocityX: 水平方向的初始速度,單位為每秒多少畫素(px/s)

第四個引數 velocityY: 垂直方向的初始速度,單位為每秒多少畫素(px/s)

第五個引數 minX: x座標最小的值,最後的結果不會低於這個值;

第六個引數 maxX: x座標最大的值,最後的結果不會超過這個值;

第七個引數 minY: y座標最小的值,最後的結果不會低於這個值;

第八個引數 maxY: y座標最大的值,最後的結果不會超過這個值;

值得一說:
minX <= 終止值的x座標 <= maxX
minY <= 終止值的y座標 <= maxY

(11) abortAnimation()

public void abortAnimation() 
複製程式碼

方法描述:

停止動畫,值得注意的是,此時如果呼叫 getCurrX()getCurrY() 移動到的是最終的座標,這一點和通過 forceFinished 直接將動畫停止是不相同的。

3、小結

從上面的 API 講解中,我們會發現,至始至終都沒有對我們需要作用的View有任何的關聯,而是通過計算,然後獲取當前時間點對應的座標,如此而已。這也就印證了前面的定義,至於怎麼真正的使用,我們留到實戰篇。

三、VelocityTracker

1、作用

同樣先給出官方的英文類註釋。小盆友以自己的理解給出這個的定義,VelocityTracker 是一個根據我們手指的觸控事件,計算出滑動速度的工具類,我們可以根據這個速度自行做計算進行檢視的移動,達到粘性滑動之類的效果。

 * Helper for tracking the velocity of touch events, for implementing
 * flinging and other such gestures.
複製程式碼

2、API講解

這一小節是對 VelocityTracker 公有方法 進行講解,如果您已經對這些方法很熟悉,可以跳過。

(1) obtain()

static public VelocityTracker obtain()
複製程式碼

方法描述:

獲取一個 VelocityTracker 物件。VelocityTracker的建構函式是私有的,也就是不能通過new來建立。

(2) recycle()

public void recycle()
複製程式碼

方法描述:

回收 VelocityTracker 例項。

(3) clear()

public void clear()
複製程式碼

方法描述:

重置 VelocityTracker 回其初始狀態。

(4) addMovement(MotionEvent event)

public void addMovement(MotionEvent event)
複製程式碼

方法描述:

為 VelocityTracker 傳入觸控事件(包括ACTION_DOWNACTION_MOVEACTION_UP等),這樣 VelocityTracker 才能在呼叫了 computeCurrentVelocity 方法後,正確的獲得當前的速度。

(5) computeCurrentVelocity(int units)

public void computeCurrentVelocity(int units)
複製程式碼

方法描述:

根據已經傳入的觸控事件計算出當前的速度,可以通過getXVelocitygetYVelocity進行獲取對應方向上的速度。值得注意的是,計算出的速度值不超過Float.MAX_VALUE

引數解析:

第一個引數 units: 速度的單位。值為1表示每毫秒畫素數,1000表示每秒畫素數。

(6) computeCurrentVelocity(int units, float maxVelocity)

public void computeCurrentVelocity(int units, float maxVelocity)
複製程式碼

方法描述:

根據已經傳入的觸控事件計算出當前的速度,可以通過getXVelocitygetYVelocity進行獲取對應方向上的速度。值得注意的是,計算出的速度值不超過maxVelocity

引數解析:

第一個引數 units: 速度的單位。值為1表示每毫秒畫素數,1000表示每秒畫素數。

第二個引數 maxVelocity: 最大的速度,計算出的速度不會超過這個值。值得注意的是,這個引數必須是正數,且其單位就是我們在第一引數設定的單位。

(7) getXVelocity()

public float getXVelocity()
複製程式碼

方法描述:

獲取最後計算的水平方向速度,使用此方法前需要記得先呼叫computeCurrentVelocity

(8) getYVelocity()

 public float getYVelocity() 
複製程式碼

方法描述:

獲取最後計算的垂直方向速度,使用此方法前需要記得先呼叫computeCurrentVelocity

(9) getXVelocity(int id)

public float getXVelocity(int id)
複製程式碼

方法描述:

獲取對應的手指id最後計算的水平方向速度,使用此方法前需要記得先呼叫computeCurrentVelocity

引數解析:

第一個引數 id: 觸碰的手指的id

(10) getYVelocity(int id)

public float getYVelocity(int id)
複製程式碼

方法描述:

獲取對應的手指id最後計算的垂直方向速度,使用此方法前需要記得先呼叫computeCurrentVelocity

引數解析:

第一個引數 id: 觸碰的手指的id

3、小結

VelocityTracker 的 API 簡單明瞭,我們可以用記住一個套路。

  1. 在觸控事件為 ACTION_DOWN 或是進入 onTouchEvent 方法時,通過 obtain 獲取一個 VelocityTracker ;
  2. 在觸控事件為 ACTION_UP 時,呼叫 recycle 進行釋放 VelocityTracker;
  3. 在進入 onTouchEvent 方法或將 ACTION_DOWNACTION_MOVEACTION_UP 的事件通過 addMovement 方法新增進 VelocityTracker;
  4. 在需要獲取速度的地方,先呼叫 computeCurrentVelocity 方法,然後通過 getXVelocitygetYVelocity 獲取對應方向的速度;

四、實戰——帶慣性滑動的柱狀圖

1、效果圖

讓控制元件如此絲滑Scroller和VelocityTracker的API講解與實戰——Android高階UI

github 地址:傳送門

雖然我們是 ScrollerVelocityTracker 的實戰,但我們還是有必要先略提一下柱子和點的繪製,以及其動畫的大致思路。然後再加入 ScrollerVelocityTracker

2、繪製思路

我們來看下面這張小盆友手繪的解析圖?,黑色的框代表CANVAS,藍色的框代表使用者看到的手機螢幕,深藍色的框是我們真正每次需要繪製的區域。

讓控制元件如此絲滑Scroller和VelocityTracker的API講解與實戰——Android高階UI
從上圖中,我們其實會發現一個規律,就是每隔一個 BarInterval 就繪製一個下圖所示的柱子,迴圈的次數則由傳入的資料量的個數決定。
讓控制元件如此絲滑Scroller和VelocityTracker的API講解與實戰——Android高階UI
但是,(敲黑板啦!!)值得注意的,在螢幕之外的柱子,其實對於使用者來說是看不到的,我們也就沒必要耗費這部分的資源來進行繪製,可以通過下面這段程式碼,判斷柱子是否在可視區域內,可視區域的範圍為螢幕的寬度各自往左和往右擴一個柱子的間隔 mBarInterval。這樣做的原因是,描述的文字或小紅點剛好在螢幕的左邊界或右邊界時,不會出現沒有繪製的情況。

/**
 * 是否在可視的範圍內
 *
 * @param x
 * @return true:在可視的範圍內;false:不在可視的範圍內
 */
private boolean isInVisibleArea(float x) {
    float dx = x - getScrollX();

    return -mBarInterval <= dx && dx <= mViewWidth + mBarInterval;
}
複製程式碼

至此,影象的繪製問題就解決了,程式碼就不貼上出來了,童鞋們可以進入傳送門 跟著思路捋一捋。

還有一個問題,就是如何讓畫面跟著手指 移動 起來,這就需要重寫 onTouchEvent 方法了,計算出手指的水平移動距離,然後通過 scrollBy 方法讓內容移動起來。

值得一提,scrollToscrollBy 方法,都是針對 內容 或是說 canvas 進行移動。

至於如何讓小紅點動起來,這裡使用了 ValueAnimator 進行從零至一的增加,達到不斷接近目標座標的效果。

對屬性動畫原始碼感興趣的童鞋,可以移步小盆友的另一片博文:帶有活力的屬性動畫原始碼分析與實戰

3、如何慣性滑動起來

經過上一小節,我們已經知道如何繪製這一簡單卻又常見的柱形圖了,但美中不足的就是沒有 fling 的效果。所以我們需要先借住 VelocityTracker 進行獲取我們當前手指的滑動速度,但這裡需要注意的是,要限制其最大和最小速度。因為速度過快和過慢,都會導致互動效果不佳。獲取程式碼如下

mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
複製程式碼

然後根據我們在 VelocityTracker小結 中的套路,進行獲取手指離屏時的水平速度。以下是隻保留 VelocityTracker 相關程式碼

/**
 * 控制螢幕不越界
 *
 * @param event
 * @return
 */
@Override
public boolean onTouchEvent(MotionEvent event) {
   	// 省略無關程式碼...
   	
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(event);

    if (MotionEvent.ACTION_DOWN == event.getAction()) {
        // 省略無關程式碼...
    } else if (MotionEvent.ACTION_MOVE == event.getAction()) {
        // 省略無關程式碼...
    } else if (MotionEvent.ACTION_UP == event.getAction()) {
        // 計算當前速度, 1000表示每秒畫素數等
        mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
        // 獲取橫向速度
        int velocityX = (int) mVelocityTracker.getXVelocity();

        // 速度要大於最小的速度值,才開始滑動
        if (Math.abs(velocityX) > mMinimumVelocity) {
        	// 省略無關程式碼...
        }

        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }
    return super.onTouchEvent(event);
}
複製程式碼

獲取完水平的速度,接下來我們需要進行真正的 fling 效果。通過一個執行緒來進行不斷的 移動 畫布,從而達到滾動效果(RecycleView中的滾動也是通過執行緒達到效果,有興趣的同學可以進入RecycleView 的原始碼進行檢視,該執行緒類的名字為 ViewFlinger )。

/**
 * 滾動執行緒
 */
private class FlingRunnable implements Runnable {

    private Scroller mScroller;

    private int mInitX;
    private int mMinX;
    private int mMaxX;
    private int mVelocityX;

    FlingRunnable(Context context) {
        this.mScroller = new Scroller(context, null, false);
    }

    void start(int initX,
               int velocityX,
               int minX,
               int maxX) {
        this.mInitX = initX;
        this.mVelocityX = velocityX;
        this.mMinX = minX;
        this.mMaxX = maxX;

        // 先停止上一次的滾動
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
        }

        // 開始 fling
        mScroller.fling(initX, 0, velocityX,
                0, 0, maxX, 0, 0);
        post(this);
    }

    @Override
    public void run() {

        // 如果已經結束,就不再進行
        if (!mScroller.computeScrollOffset()) {
            return;
        }

        // 計算偏移量
        int currX = mScroller.getCurrX();
        int diffX = mInitX - currX;

        // 用於記錄是否超出邊界,如果已經超出邊界,則不再進行回撥,即使滾動還沒有完成
        boolean isEnd = false;

        if (diffX != 0) {

            // 超出右邊界,進行修正
            if (getScrollX() + diffX >= mCanvasWidth - mViewWidth) {
                diffX = (int) (mCanvasWidth - mViewWidth - getScrollX());
                isEnd = true;
            }

            // 超出左邊界,進行修正
            if (getScrollX() <= 0) {
                diffX = -getScrollX();
                isEnd = true;
            }
            
            if (!mScroller.isFinished()) {
                scrollBy(diffX, 0);
            }
            mInitX = currX;
        }

        if (!isEnd) {
            post(this);
        }
    }

    /**
     * 進行停止
     */
    void stop() {
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
        }
    }
}
複製程式碼

最後就是使用起這個執行緒,而使用的地方主要有兩個點,一個手指按下時(即MotionEvent.ACTION_DOWN)和手指抬起時(即 MotionEvent.ACTION_UP ),刪除了不相關程式碼,剩餘程式碼如下。

public boolean onTouchEvent(MotionEvent event) {
    // 省略不相關程式碼...

    if (MotionEvent.ACTION_DOWN == event.getAction()) {
		// 省略不相關程式碼...
        mFling.stop();
    } else if (MotionEvent.ACTION_MOVE == event.getAction()) {
        // 省略不相關程式碼...
    } else if (MotionEvent.ACTION_UP == event.getAction()) {
        // 省略不相關程式碼...

        // 速度要大於最小的速度值,才開始滑動
        if (Math.abs(velocityX) > mMinimumVelocity) {

            int initX = getScrollX();

            int maxX = (int) (mCanvasWidth - mViewWidth);
            if (maxX > 0) {
                mFling.start(initX, velocityX, initX, maxX);
            }
        }
        // 省略不相關程式碼...
    }

    return super.onTouchEvent(event);

}
複製程式碼

當我們 MotionEvent.ACTION_DOWN 時,我們需要停止滾動的效果,達到立馬停止到手指觸碰的地方。

當我們 MotionEvent.ACTION_UP 時,我們需要計算 fling 方法所需的最小值和最大值。根據我們線上程中的計算方式,所以我們的最小值初始值getScrollX() 的值 而最大值mCanvasWidth - mViewWidth

讓控制元件如此絲滑Scroller和VelocityTracker的API講解與實戰——Android高階UI
最後開啟執行緒,便達到了我們看到的效果。

完整程式碼的github 地址:傳送門

五、寫在最後

ScrollerVelocityTracker 的搭配使用,能讓我們的控制元件使用起來更加絲滑,互動感更強,當然使用者體驗就越好。最後如果你從這篇文章有所收穫,請給我個贊❤️,並關注我吧。文章中如有理解錯誤或是晦澀難懂的語句,請評論區留言,我們進行討論共同進步。你的鼓勵是我前進的最大動力。

高階UI系列的Github地址:請進入傳送門,如果喜歡的話給我一個star吧?

如果需要更為深入的探討,加我微信吧?。

讓控制元件如此絲滑Scroller和VelocityTracker的API講解與實戰——Android高階UI

相關文章