目錄
一、前言
二、Scroller
三、VelocityTracker
四、實戰——帶慣性滑動的柱狀圖
五、寫在最後
一、前言
自定義控制元件中,難免會遇到需要滑動的場景。而Canvas提供的scrollTo和scrollBy方法只能達到移動的效果,需要達到真正的滑動便需要我們今天分享的兩把基礎利器Scroller和VelocityTracker。老規矩,先上實戰圖,再進行分享。
帶慣性滑動的柱狀圖
二、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—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()
複製程式碼
方法描述:
計算滾動中的新座標,會配合著 getCurrX 和 getCurrY 方法使用,達到滾動效果。值得注意的是,如果返回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_DOWN
、ACTION_MOVE
、ACTION_UP
等),這樣 VelocityTracker 才能在呼叫了 computeCurrentVelocity
方法後,正確的獲得當前的速度。
(5) computeCurrentVelocity(int units)
public void computeCurrentVelocity(int units)
複製程式碼
方法描述:
根據已經傳入的觸控事件計算出當前的速度,可以通過getXVelocity
或 getYVelocity
進行獲取對應方向上的速度。值得注意的是,計算出的速度值不超過Float.MAX_VALUE
。
引數解析:
第一個引數 units: 速度的單位。值為1表示每毫秒畫素數,1000表示每秒畫素數。
(6) computeCurrentVelocity(int units, float maxVelocity)
public void computeCurrentVelocity(int units, float maxVelocity)
複製程式碼
方法描述:
根據已經傳入的觸控事件計算出當前的速度,可以通過getXVelocity
或 getYVelocity
進行獲取對應方向上的速度。值得注意的是,計算出的速度值不超過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 簡單明瞭,我們可以用記住一個套路。
- 在觸控事件為
ACTION_DOWN
或是進入onTouchEvent
方法時,通過obtain
獲取一個 VelocityTracker ; - 在觸控事件為
ACTION_UP
時,呼叫recycle
進行釋放 VelocityTracker; - 在進入
onTouchEvent
方法或將ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
的事件通過addMovement
方法新增進 VelocityTracker; - 在需要獲取速度的地方,先呼叫
computeCurrentVelocity
方法,然後通過getXVelocity
、getYVelocity
獲取對應方向的速度;
四、實戰——帶慣性滑動的柱狀圖
1、效果圖
github 地址:傳送門
雖然我們是 Scroller 和 VelocityTracker 的實戰,但我們還是有必要先略提一下柱子和點的繪製,以及其動畫的大致思路。然後再加入 Scroller 和 VelocityTracker。
2、繪製思路
我們來看下面這張小盆友手繪的解析圖?,黑色的框代表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
方法讓內容移動起來。
值得一提,
scrollTo
和scrollBy
方法,都是針對 內容 或是說 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
。
完整程式碼的github 地址:傳送門
五、寫在最後
Scroller 和 VelocityTracker 的搭配使用,能讓我們的控制元件使用起來更加絲滑,互動感更強,當然使用者體驗就越好。最後如果你從這篇文章有所收穫,請給我個贊❤️,並關注我吧。文章中如有理解錯誤或是晦澀難懂的語句,請評論區留言,我們進行討論共同進步。你的鼓勵是我前進的最大動力。
高階UI系列的Github地址:請進入傳送門,如果喜歡的話給我一個star吧?
如果需要更為深入的探討,加我微信吧?。