本文分析版本: Android API 22
1.簡介
Android
開發(fā)中怖竭,如果我們希望使一個View
滑動的話,除了使用屬性動畫外叉谜。我們還可以使用系統(tǒng)提供給我們的兩個類Scroller
和OverScroller
用來實現(xiàn)彈性滑動旗吁。在我以前的一篇ViewDragHelper源碼分析中我們有講到過Scroller
的作用。那么我們今天就來仔細分析一下Scroller
的使用方法以及實現(xiàn)方式停局。
2.使用方法
在看Scroller
的使用方法之前我們需要先了解一下View
中的scrollBy()
和scrollTo()
方法很钓,scrollTo()
方法的實現(xiàn)如下:
public void scrollTo(int x, int y) {
//如果當前偏移量變化
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
//賦值偏移量
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
//回調(diào)onScrollChanged方法
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
scrollTo()
是指將前視圖內(nèi)容橫向偏移x
距離,縱向偏移y
距離董栽。注意這里是View
的內(nèi)容的偏移码倦,而不是View
本身。而scrollBy()
方法如下:
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
scrollBy()
方法里直接調(diào)用了scrollTo()
方法锭碳,表示在當前偏移量的基礎上繼續(xù)偏移(x,y)
≡現(xiàn)在我們來看看Scroller
的用法。SkyScrollerDemo是我寫的一個Scroller
和OverScroller
的使用demo
擒抛。下面的用法都是來自于這個demo
里推汽,大家可以clone
下來配合本文一起閱讀。本文我們主要研究Scroller
歧沪。對于OverScroller
我在demo
里也寫了相關的使用方法歹撒,在本文的最后我們再做討論。
Scroller
一般需要配合重寫computeScroll()
一起使用诊胞,代碼如下:
public class ScrollTextView extends TextView {
private Context mContext;
private Scroller mScroller;
public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
init();
}
private void init() {
mScroller = new Scroller(mContext);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
offsetLeftAndRight(mScroller.getCurrX() - mLeft);
offsetTopAndBottom(mScroller.getCurrY() - mTop);
invalidate();
}
}
//以mLeft,mTop為初始點暖夭,在DEFAULT_DURATION的時間內(nèi),在Y軸上滑動-400的偏移量
public void startScrollerScroll() {
mScroller.startScroll(mLeft, mTop, 0, -400, DEFAULT_DURATION);
invalidate();
}
//以mLeft,mTop為初始點,并以Y方向上-5000的加速度滑動鳞尔,最小Y坐標為200嬉橙,最大Y坐標為1200
public void startScrollerFling() {
mScroller.fling(mLeft, mTop, 0, -5000, mLeft, mLeft, 200, 1200);
invalidate();
}
}
在上面的代碼里,當我們調(diào)用startScrollerScroll()
與startScrollerFling()
方法時我們就發(fā)現(xiàn)View
滑動了寥假。如果以前沒了解過Scroller
的同學可能會不理解市框。這里大致分析一下調(diào)用流程,首先我們要知道Scroller
其實只負責計算糕韧,它并不負責滑動View
枫振,當我們調(diào)用了Scroller
的startScrollerScroll()
方法時,我們緊接著調(diào)用了invalidate()
方法萤彩。invalidate()
方法會使View
重新繪制粪滤。因此會調(diào)用View
的draw()
方法,在View
的draw()
方法中又會去調(diào)用computeScroll()
方法雀扶,computeScroll()
方法在View
中是一個空實現(xiàn)杖小,所以需要我們自己實現(xiàn)computeScroll()
方法。在上面的computeScroll()
方法中愚墓,我們調(diào)用了mScroller.computeScrollOffset()
方法來計算當前滑動的偏移量予权。如果還在滑動過程中就會返回true
。所以我們就能在if
中通過Scroller
拿到當前的滑動坐標從而做任何我們想做的處理浪册。在demo
里我們根據(jù)滑動的偏移量來改變了View
的坐標偏移量扫腺。從而形成了滑動動畫。下面我們解釋一下Scroller
的兩個方法的具體作用:
1.startScroll(int startX, int startY, int dx, int dy, int duration):
通過起始點村象、偏移的距離和滑動的時間來開始滑動笆环。
- startX 起始滑動點的X坐標
- startY 起始滑動點的Y坐標
- dx 滑動的水平偏移量。>0 則表示往左滑動厚者。
- dy 滑動的垂直偏移量躁劣。>0 則表示往上滑動。
- duration 滑動執(zhí)行的時間
2.fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) :
基于一個快速滑動手勢下的滑動库菲≌送滑動的距離與這個手勢最初的加速度有關。
- startX 起始滑動點的X坐標
- startY 起始滑動點的Y坐標
- velocityX X方向上的加速度
- velocityY Y方向上的加速度
- minX X方向上滑動的最小值蝙昙,不會滑動超過這個點
- maxX X方向上滑動的最大值闪萄,不會滑動超過這個點
- minY Y方向上滑動的最小值,不會滑動超過這個點
- maxY Y方向上滑動的最大值奇颠,不會滑動超過這個點
3.源碼分析
我們依然通過調(diào)用流程來分析Scroller
的實現(xiàn):
1.構(gòu)造方法
public Scroller(Context context) {
this(context, null);
}
public Scroller(Context context, Interpolator interpolator) {
this(context, interpolator,
context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
mFinished = true;
if (interpolator == null) {
mInterpolator = new ViscousFluidInterpolator();
} else {
mInterpolator = interpolator;
}
mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
mFlywheel = flywheel;
mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}
最終都會調(diào)用最后一個構(gòu)造方法败去。必須傳入Context
對象×揖埽可以傳入自定義的interpolator
和是否支持飛輪flywheel
的功能圆裕,當然這兩個并不是必須的广鳍。如果不傳入interpolator
會默認創(chuàng)建一個ViscousFluidInterpolator
,從字面意義上看是一個粘性流體插值器吓妆。對于flywheel
是指是否支持在滑動過程中赊时,如果有新的fling()
方法調(diào)用是否累加加速度。如果不傳默認在2.3以上都會支持行拢。剩下就是初始化了一些用于計算的參數(shù)祖秒。這樣就完成了Scroller
的初始化了。下面我們來看看startScroll()
方法的實現(xiàn):
2.startScroll()方法的實現(xiàn)
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
// mMode 分兩種方式 1.滑動:SCROLL_MODE 2. 加速度滑動:FLING_MODE
mMode = SCROLL_MODE;
// 是否滑動結(jié)束 這里是開始所以設置為false
mFinished = false;
// 滑動的時間
mDuration = duration;
// 開始的時間
mStartTime = AnimationUtils.currentAnimationTimeMillis();
// 開始滑動點的X坐標
mStartX = startX;
// 開始滑動點的Y坐標
mStartY = startY;
// 最終滑動到位置的X坐標
mFinalX = startX + dx;
// 最終滑動到位置的Y坐標
mFinalY = startY + dy;
// X方向上滑動的偏移量
mDeltaX = dx;
// Y方向上滑動的偏移量
mDeltaY = dy;
// 持續(xù)時間的倒數(shù) 最終用來計算得到插值器返回的值
mDurationReciprocal = 1.0f / (float) mDuration;
}
很簡單只是一些變量的賦值舟奠。根據(jù)我們前面使用方法里的分析竭缝,最終會調(diào)用computeScrollOffset()
方法:
3.computeScrollOffset() 方法中 SCROLL_MODE 的實現(xiàn)
// 當你需要知道新的位置的時候調(diào)用這個方法,如果動畫還未結(jié)束則返回true
public boolean computeScrollOffset() {
//如果已經(jīng)結(jié)束 則直接返回false
if (mFinished) {
return false;
}
//得到以及度過的時間
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
//如果還在動畫時間內(nèi)
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
// 根據(jù)timePassed * mDurationReciprocal,從mInterpolator中取出當前需要偏移量的比例
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
// 賦值給 mCurrX沼瘫,mCurrY
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
...
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
首先的到當前時間與滑動開始時間的時間差抬纸,如果還在滑動時間內(nèi)則通過插值器獲得當前的進度并乘以總偏移量并賦值給mCurrX
,mCurrY
耿戚。如果已經(jīng)結(jié)束則直接將mFinalX
和mFinalY
賦值并將mFinished
設置?為true
湿故。所以這樣我們就能通過getCurrX()
和getCurrY()
來得到對應的mCurrX
和mCurrY
來做相應的處理了。整個Scroll
的過程就是這樣了膜蛔。
4.fling()方法的實現(xiàn)
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
// 如果前一次滑動還未結(jié)束坛猪,又調(diào)用了新的fling()方法時,
// 則累加相同方向上加速度
if (mFlywheel && !mFinished) {
float oldVel = getCurrVelocity();
float dx = (float) (mFinalX - mStartX);
float dy = (float) (mFinalY - mStartY);
float hyp = FloatMath.sqrt(dx * dx + dy * dy);
float ndx = dx / hyp;
float ndy = dy / hyp;
float oldVelocityX = ndx * oldVel;
float oldVelocityY = ndy * oldVel;
if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
Math.signum(velocityY) == Math.signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
}
//設置為FLING_MODE
mMode = FLING_MODE;
mFinished = false;
//根據(jù)勾股定理獲得總加速度
float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
mVelocity = velocity;
// 通過加速度得到滑動持續(xù)時間
mDuration = getSplineFlingDuration(velocity);
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
double totalDistance = getSplineFlingDistance(velocity);
mDistance = (int) (totalDistance * Math.signum(velocity));
mMinX = minX;
mMaxX = maxX;
mMinY = minY;
mMaxY = maxY;
mFinalX = startX + (int) Math.round(totalDistance * coeffX);
// Pin to mMinX <= mFinalX <= mMaxX
mFinalX = Math.min(mFinalX, mMaxX);
mFinalX = Math.max(mFinalX, mMinX);
mFinalY = startY + (int) Math.round(totalDistance * coeffY);
// Pin to mMinY <= mFinalY <= mMaxY
mFinalY = Math.min(mFinalY, mMaxY);
mFinalY = Math.max(mFinalY, mMinY);
}
依然是為計算需要的各種變量賦值飞几。因為引入了加速度的概念所以變得相對復雜砚哆,首先先判斷了如果一次滑動未結(jié)束又觸發(fā)另一次滑動時独撇,是否需要累加加速度屑墨。然后是設置mMode
為FLING_MODE
。然后根據(jù)velocityX
和velocityY
算出總的加速度velocity
纷铣,緊接著算出這個加速度下可以滑動的距離mDistance
卵史。最后再通過x
或y
方向上的加速度比值以及我們設定的最大值和最小值來給mFinalX
或mFinalY
賦值。賦值結(jié)束后搜立,通過調(diào)用invalidate()
以躯,最終依然會調(diào)用computeScrollOffset()
方法:
5.computeScrollOffset() 方法中 FLING_MODE 的實現(xiàn)
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
...
break;
case FLING_MODE:
// 當前已滑動的時間與總滑動時間的比值
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
// 距離系數(shù)
float distanceCoef = 1.f;
// 加速度系數(shù)
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 與mCurrY
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);
// 如果到達了終點 則結(jié)束
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
由于fling()
方法中將mMode
賦值為FLING_MODE
。所以我們直接來看FLING_MODE
中的代碼啄踊∮巧瑁可以看出根據(jù)當前滑動時間與總滑動時間的比例。再根據(jù)一個SPLINE_POSITION
數(shù)組計算出了距離系數(shù)distanceCoef
與加速度系數(shù)velocityCoef
颠通。再根據(jù)這兩個系數(shù)計算出當前加速度與當前的mCurrX
與mCurrY
址晕。關于SPLINE_POSITION
的初始化是在下面的靜態(tài)代碼塊里賦值的:
static {
float x_min = 0.0f;
float y_min = 0.0f;
for (int i = 0; i < NB_SAMPLES; i++) {
final float alpha = (float) i / NB_SAMPLES;
float x_max = 1.0f;
float x, tx, coef;
while (true) {
x = x_min + (x_max - x_min) / 2.0f;
coef = 3.0f * x * (1.0f - x);
tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
if (Math.abs(tx - alpha) < 1E-5) break;
if (tx > alpha) x_max = x;
else x_min = x;
}
SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
float y_max = 1.0f;
float y, dy;
while (true) {
y = y_min + (y_max - y_min) / 2.0f;
coef = 3.0f * y * (1.0f - y);
dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
if (Math.abs(dy - alpha) < 1E-5) break;
if (dy > alpha) y_max = y;
else y_min = y;
}
SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
}
SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
}
我并沒有看懂這段代碼的實際意義。網(wǎng)上也沒有找到比較清晰的解釋顿锰。通過debug
得知SPLINE_POSITION
是一個長度為101
并且從0-1
遞增數(shù)組谨垃。猜想這應該是一個函數(shù)模型并且最終用于計算出滑動過程中的加速度與位置启搂。如果有同學能詳細解釋這段代碼的作用,歡迎在這篇文章留言刘陶。至此Scroller
的兩個主要方法的實現(xiàn)我們就分析完了胳赌。
4.OverScroller解析
OverScroller
是對Scroller
的拓展,它在Scroller
的基礎上拓展出了更多的方法匙隔。OverScroller
的fling
方法支持滑動到終點之后并超出一段距離并返回疑苫,類似于彈性效果。另外一個springBack()
方法是指將指定的點平滑滾動到指定的終點上纷责。這個終點由設置的參數(shù)決定缀匕。原理我們就不再探究了,大家可以自行研究這兩個類的差別碰逸。最后具體的使用方法在文章最上面的demo
里都有提供乡小。可以clone
下來幫助理解饵史。
我每周會寫一篇源代碼分析的文章,以后也可能會有其他主題.
如果你喜歡我寫的文章的話,歡迎關注我的新浪微博@達達達達sky
地址: http://weibo.com/u/2030683111
每周我會第一時間在微博分享我寫的文章,也會積極轉(zhuǎn)發(fā)更多有用的知識給大家.