在之前介紹的關於View滑動的內容時,關於使用使用Scroller進行View的平滑滑動沒有介紹,今天在這裏補充一下。關於爲什麼使用Scroller滑動,這裏就不多介紹,主要是因爲使用scrollTo()或者scrollBy()都是瞬間完成,用戶體驗不是太好,當然滑動最終的實現都是通過scrollTo()實現,所以,你也可以通過不斷移動一小段距離再加上一個Timer進行控制,也可以實現平滑移動的效果,具體可以參見Android中實現滑動(中)----實現(2)。下面就來看看如何使用Scroller。
1,使用步驟
這裏我們直接進入主題,先給出Scroller的使用步驟,然後給出示例代碼和效果圖,最後在結合源碼簡單分析一下。使用步驟如下:
- 自定義一個View繼承你想實現平滑滑動的控件
- 添加一個Scroller成員變量,並在構造方法中初始化
- 重寫View的computeScroll()方法
- 定義外部訪問的用於調用滑動實現的接口
有一點抽象,下面我們就結合具體的示例說一下,首先我們自定義各一個SelfButton繼承至Button,並在內部實現了上述四個步驟:
package com.hfut.operationscroll;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.Scroller;
/**
* author:why
* created on: 2019/5/25 13:31
* description:用Scroller實現View 的平滑滑動
*/
public class SelfButton extends android.support.v7.widget.AppCompatButton {
Scroller scroller;
public SelfButton(Context context, AttributeSet attrs) {
super(context, attrs);
scroller=new Scroller(context);
}
/**
* 第三步:重寫computeScroll()方法
*/
@Override
public void computeScroll() {
super.computeScroll();
if(scroller.computeScrollOffset()){
((View)getParent()).scrollTo(scroller.getCurrX(),scroller.getCurrY());
invalidate();//重繪,在重繪調用draw()方法中,內部會調用View的computeScroll()方法
}
}
/**
* 第四步:對外調用接口,可以封裝成接口
* @param destX:滑動Dest X
* @param destY:滑動Dest Y
* @param duration:滑動時間
*/
public void scrollerMove(int destX,int destY,int duration){
int scrollX=getScrollX();
int scrollY=getScrollY();
int moveDistX=destX-scrollX;
int moveDistY=destX-scrollY;
scroller.startScroll(scrollX,scrollY,moveDistX,moveDistY,duration);
invalidate();
}
}
然後我們在業務層調用一下看看效果:
button.scrollerMove(-400, -400,5000);
可以看到我們使用scroller再加上上面說的四步,成功實現view的平滑滑動,那麼究竟是Scroller究竟是什麼,怎麼實現的了,下面我們就來簡單分析一下。
2,Scroller實現View平滑滑動分析
首先,我們使用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>這是一個封裝了滾動的類,你可以通過滾動條來收集你想要產生滾動動畫的數據,比如,響應一個手勢動作;滾動條會實時獲取滾動的偏移量,但是它們不負責把這些數據應用到你的view上,你需要自己吧這些數據應用到你的view上實現一個平滑滾動動畫的效果。
上面是我自己簡單翻譯的,感覺比較抽象哈,其實一點都不難,我們就結合上面給的示例說明一下上面意思:
- 首先Scroller它是一個封裝了滾動類,你可以通過它來收集數據
- 究竟如何收集數據了,那我們就需要轉到Scroller源碼中一探究竟了。首先我們知道獲取數據的接口如下(在重寫的computeScroll()中):
scroller.getCurrX()
scroller.getCurrY()
我們跟到Scroller源碼中看一下(只給出一個,另一個類似):
/**
* Returns the current X offset in the scroll.
*
* @return The new X offset as an absolute distance from the origin.
*/
public final int getCurrX() {
return mCurrX;
}
這裏只是返回了一個內部變量的值,那麼它的賦值邏輯我們在哪個調用的接口中實現的了。我們在重寫獲取這些數值之前還調用了Scroller的一個接口,那就是computeScrollOffset(),下面就來看看它的邏輯代碼:
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
這個就是實現在Scroller開始滾動後(startScroll())計算數據的邏輯,其中startScroll()的邏輯如下:
/**
* Start scrolling by providing a starting point and the distance to travel.
* The scroll will use the default value of 250 milliseconds for the
* duration.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
*/
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
- 收集之後,如果你不做任何操作,Scroller本身也不會爲你的View和這些data建立聯繫,那我們就需要重寫computeScroll()方法自己應用這些數據
((View)getParent()).scrollTo(scroller.getCurrX(),scroller.getCurrY());
invalidate();//重繪,在重繪調用draw()方法中,內部會調用View的computeScroll()方法
- 就這樣,Scroller內部獲取數據通過接口拋出來,我們拿到之後應用到View的滑動動畫上就可以了。
所以整個流程梳理下來就是如下這樣:
- 我們開啓Scroller的startScroll()實現一些狀態的賦值,比如mFinished等
- 我們在自定義View的重寫的computeScroll()方法中調用Scroller的computeScrollerOffset()接口計算滑動數據,然後通過Scroller的getCurrX()等接口獲取實時計算的數據
- 拿到數據之後,我們需要根據自己的需要來使用這些數據,我們這裏想要實現一個滑動的效果,所以直接調用了scrollTo()來接收這些數據,最後通過invalidate()方法實現view的重繪
- 在View重繪的過程中,必然需要調用draw()方法,而在draw()方法中,又會調用computeScroll()方法,就這樣實現了一個平滑動畫的效果,直至結束(以下條件不滿足,邏輯來自Scroller中的computeScrollOffset())。
if (timePassed < mDuration)
至此,關於Scroller實現View的平滑滑動的實現以及分析就結束了。如果想了解View其他滑動實現,歡迎閱讀
注:歡迎掃碼關注