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

目錄java

1、前言git

2、Scrollergithub

3、VelocityTrackercanvas

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

5、寫在最後app

1、前言

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

帶慣性滑動的柱狀圖 函數

2、Scroller

一、做用

童鞋們能夠先看下下面這段官方的英文類註釋。小盆友以本身的理解給出這個類的做用是,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>
複製代碼

二、API講解

這一小節是對 Scroller構造方法經常使用的公有方法 進行講解,若是您已經對這些方法很熟悉,能夠跳過。oop

構造方法

(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 直接將動畫中止是不相同的。

三、小結

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

3、VelocityTracker

一、做用

一樣先給出官方的英文類註釋。小盆友以本身的理解給出這個的定義,VelocityTracker 是一個根據咱們手指的觸摸事件,計算出滑動速度的工具類,咱們能夠根據這個速度自行作計算進行視圖的移動,達到粘性滑動之類的效果。

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

二、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

三、小結

VelocityTracker 的 API 簡單明瞭,咱們能夠用記住一個套路。

  1. 在觸摸事件爲 ACTION_DOWN 或是進入 onTouchEvent 方法時,經過 obtain 獲取一個 VelocityTracker ;
  2. 在觸摸事件爲 ACTION_UP 時,調用 recycle 進行釋放 VelocityTracker;
  3. 在進入 onTouchEvent 方法或將 ACTION_DOWNACTION_MOVEACTION_UP 的事件經過 addMovement 方法添加進 VelocityTracker;
  4. 在須要獲取速度的地方,先調用 computeCurrentVelocity 方法,而後經過 getXVelocitygetYVelocity 獲取對應方向的速度;

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

一、效果圖

github 地址:傳送門

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

二、繪製思路

咱們來看下面這張小盆友手繪的解析圖😂,黑色的框表明CANVAS,藍色的框表明用戶看到的手機屏幕,深藍色的框是咱們真正每次須要繪製的區域。 從上圖中,咱們其實會發現一個規律,就是每隔一個 BarInterval 就繪製一個下圖所示的柱子,循環的次數則由傳入的數據量的個數決定。 可是,(敲黑板啦!!)值得注意的,在屏幕以外的柱子,其實對於用戶來講是看不到的,咱們也就不必耗費這部分的資源來進行繪製,能夠經過下面這段代碼,判斷柱子是否在可視區域內,可視區域的範圍爲屏幕的寬度各自往左和往右擴一個柱子的間隔 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 進行從零至一的增長,達到不斷接近目標座標的效果。

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

三、如何慣性滑動起來

通過上一小節,咱們已經知道如何繪製這一簡單卻又常見的柱形圖了,但美中不足的就是沒有 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 最後開啓線程,便達到了咱們看到的效果。

完整代碼的github 地址:傳送門

5、寫在最後

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

高級UI系列的Github地址:請進入傳送門,若是喜歡的話給我一個star吧😄

相關文章
相關標籤/搜索