目錄java
1、前言git
2、Scrollergithub
3、VelocityTrackercanvas
4、實戰——帶慣性滑動的柱狀圖markdown
5、寫在最後app
自定義控件中,不免會遇到須要滑動的場景。而Canvas提供的scrollTo和scrollBy方法只能達到移動的效果,須要達到真正的滑動便須要咱們今天分享的兩把基礎利器Scroller和VelocityTracker。老規矩,先上實戰圖,再進行分享。ide
帶慣性滑動的柱狀圖 函數
童鞋們能夠先看下下面這段官方的英文類註釋。小盆友以本身的理解給出這個類的做用是,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>
複製代碼
這一小節是對 Scroller 的 構造方法 和 經常使用的公有方法 進行講解,若是您已經對這些方法很熟悉,能夠跳過。oop
public Scroller(Context context) 複製代碼
方法描述:
建立一個 Scroller 實例。
參數解析:
第一個參數 context: 上下文;
public Scroller(Context context, Interpolator interpolator) 複製代碼
方法描述:
建立一個 Scroller 實例。
參數解析:
第一個參數 context: 上下文;
第二個參數 interpolator: 插值器,用於在 computeScrollOffset 方法中,而且是在 SCROLL_MODE 模式下,根據時間的推移計算位置。爲null時,使用默認 ViscousFluidInterpolator 插值器。
public Scroller(Context context, Interpolator interpolator, boolean flywheel) 複製代碼
方法描述:
建立一個 Scroller 實例。
參數解析:
第一個參數 context: 上下文;
第二個參數 interpolator: 插值器,用於在 computeScrollOffset 方法中,而且是在 SCROLL_MODE 模式下,根據時間的推移計算位置。爲null時,使用默認 ViscousFluidInterpolator 插值器。
第三個參數 flywheel: 支持漸進式行爲,該參數只做用於 FLING_MODE 模式下。
public final void setFriction(float friction) 複製代碼
方法描述:
用於設置在 FLING_MODE 模式下的摩擦係數
參數解析:
第一個參數 friction: 摩擦係數
public final boolean isFinished() 複製代碼
方法描述:
滾動是否已結束,用於判斷 Scroller 在滾動過程的狀態,咱們能夠作一些終止或繼續運行的邏輯分支。
public final void forceFinished(boolean finished) 複製代碼
方法描述:
強制的讓滾動狀態置爲咱們所設置的參數值 finished 。
public final int getDuration() 複製代碼
方法描述:
返回 Scroller 將持續的時間(以毫秒爲單位)。
public final int getCurrX() 複製代碼
方法描述:
返回滾動中的當前X相對於原點的偏移量,即當前座標的X座標。
public final int getCurrY() 複製代碼
方法描述:
返回滾動中的當前Y相對於原點的偏移量,即當前座標的Y座標。
public float getCurrVelocity() 複製代碼
方法描述:
獲取當前速度。
public boolean computeScrollOffset() 複製代碼
方法描述:
計算滾動中的新座標,會配合着 getCurrX 和 getCurrY 方法使用,達到滾動效果。值得注意的是,若是返回true,說明動畫還未完成。相反,返回false,說明動畫已經完成或是被終止了。
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: 滾動的時長
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
public void abortAnimation() 複製代碼
方法描述:
中止動畫,值得注意的是,此時若是調用 getCurrX() 和 getCurrY() 移動到的是最終的座標,這一點和經過 forceFinished 直接將動畫中止是不相同的。
從上面的 API 講解中,咱們會發現,至始至終都沒有對咱們須要做用的View有任何的關聯,而是經過計算,而後獲取當前時間點對應的座標,如此而已。這也就印證了前面的定義,至於怎麼真正的使用,咱們留到實戰篇。
一樣先給出官方的英文類註釋。小盆友以本身的理解給出這個的定義,VelocityTracker 是一個根據咱們手指的觸摸事件,計算出滑動速度的工具類,咱們能夠根據這個速度自行作計算進行視圖的移動,達到粘性滑動之類的效果。
* Helper for tracking the velocity of touch events, for implementing
* flinging and other such gestures.
複製代碼
這一小節是對 VelocityTracker 公有方法 進行講解,若是您已經對這些方法很熟悉,能夠跳過。
static public VelocityTracker obtain() 複製代碼
方法描述:
獲取一個 VelocityTracker 對象。VelocityTracker的構造函數是私有的,也就是不能經過new來建立。
public void recycle() 複製代碼
方法描述:
回收 VelocityTracker 實例。
public void clear() 複製代碼
方法描述:
重置 VelocityTracker 回其初始狀態。
public void addMovement(MotionEvent event) 複製代碼
方法描述:
爲 VelocityTracker 傳入觸摸事件(包括ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
等),這樣 VelocityTracker 才能在調用了 computeCurrentVelocity
方法後,正確的得到當前的速度。
public void computeCurrentVelocity(int units) 複製代碼
方法描述:
根據已經傳入的觸摸事件計算出當前的速度,能夠經過getXVelocity
或 getYVelocity
進行獲取對應方向上的速度。值得注意的是,計算出的速度值不超過Float.MAX_VALUE
。
參數解析:
第一個參數 units: 速度的單位。值爲1表示每毫秒像素數,1000表示每秒像素數。
public void computeCurrentVelocity(int units, float maxVelocity) 複製代碼
方法描述:
根據已經傳入的觸摸事件計算出當前的速度,能夠經過getXVelocity
或 getYVelocity
進行獲取對應方向上的速度。值得注意的是,計算出的速度值不超過maxVelocity
。
參數解析:
第一個參數 units: 速度的單位。值爲1表示每毫秒像素數,1000表示每秒像素數。
第二個參數 maxVelocity: 最大的速度,計算出的速度不會超過這個值。值得注意的是,這個參數必須是正數,且其單位就是咱們在第一參數設置的單位。
public float getXVelocity() 複製代碼
方法描述:
獲取最後計算的水平方向速度,使用此方法前須要記得先調用computeCurrentVelocity
public float getYVelocity() 複製代碼
方法描述:
獲取最後計算的垂直方向速度,使用此方法前須要記得先調用computeCurrentVelocity
public float getXVelocity(int id) 複製代碼
方法描述:
獲取對應的手指id最後計算的水平方向速度,使用此方法前須要記得先調用computeCurrentVelocity
參數解析:
第一個參數 id: 觸碰的手指的id
public float getYVelocity(int id) 複製代碼
方法描述:
獲取對應的手指id最後計算的垂直方向速度,使用此方法前須要記得先調用computeCurrentVelocity
參數解析:
第一個參數 id: 觸碰的手指的id
VelocityTracker 的 API 簡單明瞭,咱們能夠用記住一個套路。
ACTION_DOWN
或是進入 onTouchEvent
方法時,經過 obtain
獲取一個 VelocityTracker ;ACTION_UP
時,調用 recycle
進行釋放 VelocityTracker;onTouchEvent
方法或將 ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
的事件經過 addMovement
方法添加進 VelocityTracker;computeCurrentVelocity
方法,而後經過 getXVelocity
、getYVelocity
獲取對應方向的速度;github 地址:傳送門
雖然咱們是 Scroller 和 VelocityTracker 的實戰,但咱們仍是有必要先略提一下柱子和點的繪製,以及其動畫的大體思路。而後再加入 Scroller 和 VelocityTracker。
咱們來看下面這張小盆友手繪的解析圖😂,黑色的框表明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
進行從零至一的增長,達到不斷接近目標座標的效果。
對屬性動畫源碼感興趣的童鞋,能夠移步小盆友的另外一片博文:帶有活力的屬性動畫源碼分析與實戰
通過上一小節,咱們已經知道如何繪製這一簡單卻又常見的柱形圖了,但美中不足的就是沒有 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吧😄