本文分析版本: Android API 22
1.簡(jiǎn)介
Android
開(kāi)發(fā)中,如果我們希望使一個(gè)View
滑動(dòng)的話,除了使用屬性動(dòng)畫外。我們還可以使用系統(tǒng)提供給我們的兩個(gè)類Scroller
和OverScroller
用來(lái)實(shí)現(xiàn)彈性滑動(dòng)。在我以前的一篇ViewDragHelper源碼分析中我們有講到過(guò)Scroller
的作用。那么我們今天就來(lái)仔細(xì)分析一下Scroller
的使用方法以及實(shí)現(xiàn)方式。
2.使用方法
在看Scroller
的使用方法之前我們需要先了解一下View
中的scrollBy()
和scrollTo()
方法,scrollTo()
方法的實(shí)現(xiàn)如下:
public void scrollTo(int x, int y) {
//如果當(dāng)前偏移量變化
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()
方法,表示在當(dāng)前偏移量的基礎(chǔ)上繼續(xù)偏移(x,y)
。現(xiàn)在我們來(lái)看看Scroller
的用法。SkyScrollerDemo是我寫的一個(gè)Scroller
和OverScroller
的使用demo
。下面的用法都是來(lái)自于這個(gè)demo
里,大家可以clone
下來(lái)配合本文一起閱讀。本文我們主要研究Scroller
。對(duì)于OverScroller
我在demo
里也寫了相關(guān)的使用方法,在本文的最后我們?cè)僮鲇懻摗?/p>
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為初始點(diǎn),在DEFAULT_DURATION的時(shí)間內(nèi),在Y軸上滑動(dòng)-400的偏移量
public void startScrollerScroll() {
mScroller.startScroll(mLeft, mTop, 0, -400, DEFAULT_DURATION);
invalidate();
}
//以mLeft,mTop為初始點(diǎn),并以Y方向上-5000的加速度滑動(dòng),最小Y坐標(biāo)為200,最大Y坐標(biāo)為1200
public void startScrollerFling() {
mScroller.fling(mLeft, mTop, 0, -5000, mLeft, mLeft, 200, 1200);
invalidate();
}
}
在上面的代碼里,當(dāng)我們調(diào)用startScrollerScroll()
與startScrollerFling()
方法時(shí)我們就發(fā)現(xiàn)View
滑動(dòng)了。如果以前沒(méi)了解過(guò)Scroller
的同學(xué)可能會(huì)不理解。這里大致分析一下調(diào)用流程,首先我們要知道Scroller
其實(shí)只負(fù)責(zé)計(jì)算,它并不負(fù)責(zé)滑動(dòng)View
,當(dāng)我們調(diào)用了Scroller
的startScrollerScroll()
方法時(shí),我們緊接著調(diào)用了invalidate()
方法。invalidate()
方法會(huì)使View
重新繪制。因此會(huì)調(diào)用View
的draw()
方法,在View
的draw()
方法中又會(huì)去調(diào)用computeScroll()
方法,computeScroll()
方法在View
中是一個(gè)空實(shí)現(xiàn),所以需要我們自己實(shí)現(xiàn)computeScroll()
方法。在上面的computeScroll()
方法中,我們調(diào)用了mScroller.computeScrollOffset()
方法來(lái)計(jì)算當(dāng)前滑動(dòng)的偏移量。如果還在滑動(dòng)過(guò)程中就會(huì)返回true
。所以我們就能在if
中通過(guò)Scroller
拿到當(dāng)前的滑動(dòng)坐標(biāo)從而做任何我們想做的處理。在demo
里我們根據(jù)滑動(dòng)的偏移量來(lái)改變了View
的坐標(biāo)偏移量。從而形成了滑動(dòng)動(dòng)畫。下面我們解釋一下Scroller
的兩個(gè)方法的具體作用:
1.startScroll(int startX, int startY, int dx, int dy, int duration):
通過(guò)起始點(diǎn)、偏移的距離和滑動(dòng)的時(shí)間來(lái)開(kāi)始滑動(dòng)。
- startX 起始滑動(dòng)點(diǎn)的X坐標(biāo)
- startY 起始滑動(dòng)點(diǎn)的Y坐標(biāo)
- dx 滑動(dòng)的水平偏移量。>0 則表示往左滑動(dòng)。
- dy 滑動(dòng)的垂直偏移量。>0 則表示往上滑動(dòng)。
- duration 滑動(dòng)執(zhí)行的時(shí)間
2.fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) :
基于一個(gè)快速滑動(dòng)手勢(shì)下的滑動(dòng)。滑動(dòng)的距離與這個(gè)手勢(shì)最初的加速度有關(guān)。
- startX 起始滑動(dòng)點(diǎn)的X坐標(biāo)
- startY 起始滑動(dòng)點(diǎn)的Y坐標(biāo)
- velocityX X方向上的加速度
- velocityY Y方向上的加速度
- minX X方向上滑動(dòng)的最小值,不會(huì)滑動(dòng)超過(guò)這個(gè)點(diǎn)
- maxX X方向上滑動(dòng)的最大值,不會(huì)滑動(dòng)超過(guò)這個(gè)點(diǎn)
- minY Y方向上滑動(dòng)的最小值,不會(huì)滑動(dòng)超過(guò)這個(gè)點(diǎn)
- maxY Y方向上滑動(dòng)的最大值,不會(huì)滑動(dòng)超過(guò)這個(gè)點(diǎn)
3.源碼分析
我們依然通過(guò)調(diào)用流程來(lái)分析Scroller
的實(shí)現(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
}
最終都會(huì)調(diào)用最后一個(gè)構(gòu)造方法。必須傳入Context
對(duì)象??梢詡魅胱远x的interpolator
和是否支持飛輪flywheel
的功能,當(dāng)然這兩個(gè)并不是必須的。如果不傳入interpolator
會(huì)默認(rèn)創(chuàng)建一個(gè)ViscousFluidInterpolator
,從字面意義上看是一個(gè)粘性流體插值器。對(duì)于flywheel
是指是否支持在滑動(dòng)過(guò)程中,如果有新的fling()
方法調(diào)用是否累加加速度。如果不傳默認(rèn)在2.3以上都會(huì)支持。剩下就是初始化了一些用于計(jì)算的參數(shù)。這樣就完成了Scroller
的初始化了。下面我們來(lái)看看startScroll()
方法的實(shí)現(xiàn):
2.startScroll()方法的實(shí)現(xiàn)
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
// mMode 分兩種方式 1.滑動(dòng):SCROLL_MODE 2. 加速度滑動(dòng):FLING_MODE
mMode = SCROLL_MODE;
// 是否滑動(dòng)結(jié)束 這里是開(kāi)始所以設(shè)置為false
mFinished = false;
// 滑動(dòng)的時(shí)間
mDuration = duration;
// 開(kāi)始的時(shí)間
mStartTime = AnimationUtils.currentAnimationTimeMillis();
// 開(kāi)始滑動(dòng)點(diǎn)的X坐標(biāo)
mStartX = startX;
// 開(kāi)始滑動(dòng)點(diǎn)的Y坐標(biāo)
mStartY = startY;
// 最終滑動(dòng)到位置的X坐標(biāo)
mFinalX = startX + dx;
// 最終滑動(dòng)到位置的Y坐標(biāo)
mFinalY = startY + dy;
// X方向上滑動(dòng)的偏移量
mDeltaX = dx;
// Y方向上滑動(dòng)的偏移量
mDeltaY = dy;
// 持續(xù)時(shí)間的倒數(shù) 最終用來(lái)計(jì)算得到插值器返回的值
mDurationReciprocal = 1.0f / (float) mDuration;
}
很簡(jiǎn)單只是一些變量的賦值。根據(jù)我們前面使用方法里的分析,最終會(huì)調(diào)用computeScrollOffset()
方法:
3.computeScrollOffset() 方法中 SCROLL_MODE 的實(shí)現(xiàn)
// 當(dāng)你需要知道新的位置的時(shí)候調(diào)用這個(gè)方法,如果動(dòng)畫還未結(jié)束則返回true
public boolean computeScrollOffset() {
//如果已經(jīng)結(jié)束 則直接返回false
if (mFinished) {
return false;
}
//得到以及度過(guò)的時(shí)間
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
//如果還在動(dòng)畫時(shí)間內(nèi)
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
// 根據(jù)timePassed * mDurationReciprocal,從mInterpolator中取出當(dāng)前需要偏移量的比例
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;
}
首先的到當(dāng)前時(shí)間與滑動(dòng)開(kāi)始時(shí)間的時(shí)間差,如果還在滑動(dòng)時(shí)間內(nèi)則通過(guò)插值器獲得當(dāng)前的進(jìn)度并乘以總偏移量并賦值給mCurrX
,mCurrY
。如果已經(jīng)結(jié)束則直接將mFinalX
和mFinalY
賦值并將mFinished
設(shè)置?為true
。所以這樣我們就能通過(guò)getCurrX()
和getCurrY()
來(lái)得到對(duì)應(yīng)的mCurrX
和mCurrY
來(lái)做相應(yīng)的處理了。整個(gè)Scroll
的過(guò)程就是這樣了。
4.fling()方法的實(shí)現(xiàn)
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
// 如果前一次滑動(dòng)還未結(jié)束,又調(diào)用了新的fling()方法時(shí),
// 則累加相同方向上加速度
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;
}
}
//設(shè)置為FLING_MODE
mMode = FLING_MODE;
mFinished = false;
//根據(jù)勾股定理獲得總加速度
float velocity = FloatMath.sqrt(velocityX * velocityX + velocityY * velocityY);
mVelocity = velocity;
// 通過(guò)加速度得到滑動(dòng)持續(xù)時(shí)間
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);
}
依然是為計(jì)算需要的各種變量賦值。因?yàn)橐肓思铀俣鹊母拍钏宰兊孟鄬?duì)復(fù)雜,首先先判斷了如果一次滑動(dòng)未結(jié)束又觸發(fā)另一次滑動(dòng)時(shí),是否需要累加加速度。然后是設(shè)置mMode
為FLING_MODE
。然后根據(jù)velocityX
和velocityY
算出總的加速度velocity
,緊接著算出這個(gè)加速度下可以滑動(dòng)的距離mDistance
。最后再通過(guò)x
或y
方向上的加速度比值以及我們?cè)O(shè)定的最大值和最小值來(lái)給mFinalX
或mFinalY
賦值。賦值結(jié)束后,通過(guò)調(diào)用invalidate()
,最終依然會(huì)調(diào)用computeScrollOffset()
方法:
5.computeScrollOffset() 方法中 FLING_MODE 的實(shí)現(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:
// 當(dāng)前已滑動(dòng)的時(shí)間與總滑動(dòng)時(shí)間的比值
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;
}
// 計(jì)算出當(dāng)前的加速度
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
// 計(jì)算出當(dāng)前的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);
// 如果到達(dá)了終點(diǎn) 則結(jié)束
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
由于fling()
方法中將mMode
賦值為FLING_MODE
。所以我們直接來(lái)看FLING_MODE
中的代碼??梢钥闯龈鶕?jù)當(dāng)前滑動(dòng)時(shí)間與總滑動(dòng)時(shí)間的比例。再根據(jù)一個(gè)SPLINE_POSITION
數(shù)組計(jì)算出了距離系數(shù)distanceCoef
與加速度系數(shù)velocityCoef
。再根據(jù)這兩個(gè)系數(shù)計(jì)算出當(dāng)前加速度與當(dāng)前的mCurrX
與mCurrY
。關(guān)于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;
}
我并沒(méi)有看懂這段代碼的實(shí)際意義。網(wǎng)上也沒(méi)有找到比較清晰的解釋。通過(guò)debug
得知SPLINE_POSITION
是一個(gè)長(zhǎng)度為101
并且從0-1
遞增數(shù)組。猜想這應(yīng)該是一個(gè)函數(shù)模型并且最終用于計(jì)算出滑動(dòng)過(guò)程中的加速度與位置。如果有同學(xué)能詳細(xì)解釋這段代碼的作用,歡迎在這篇文章留言。至此Scroller
的兩個(gè)主要方法的實(shí)現(xiàn)我們就分析完了。
4.OverScroller解析
OverScroller
是對(duì)Scroller
的拓展,它在Scroller
的基礎(chǔ)上拓展出了更多的方法。OverScroller
的fling
方法支持滑動(dòng)到終點(diǎn)之后并超出一段距離并返回,類似于彈性效果。另外一個(gè)springBack()
方法是指將指定的點(diǎn)平滑滾動(dòng)到指定的終點(diǎn)上。這個(gè)終點(diǎn)由設(shè)置的參數(shù)決定。原理我們就不再探究了,大家可以自行研究這兩個(gè)類的差別。最后具體的使用方法在文章最上面的demo
里都有提供??梢?code>clone下來(lái)幫助理解。
我每周會(huì)寫一篇源代碼分析的文章,以后也可能會(huì)有其他主題.
如果你喜歡我寫的文章的話,歡迎關(guān)注我的新浪微博@達(dá)達(dá)達(dá)達(dá)sky
地址: http://weibo.com/u/2030683111
每周我會(huì)第一時(shí)間在微博分享我寫的文章,也會(huì)積極轉(zhuǎn)發(fā)更多有用的知識(shí)給大家.