- Android開發(fā)藝術探索
- 任玉剛
- 2242字
- 2019-01-10 17:37:39
3.3 彈性滑動
知道了View的滑動,我們還要知道如何實現(xiàn)View的彈性滑動,比較生硬地滑動過去,這種方式的用戶體驗實在太差了,因此我們要實現(xiàn)漸近式滑動。那么如何實現(xiàn)彈性滑動呢?其實實現(xiàn)方法有很多,但是它們都有一個共同思想:將一次大的滑動分成若干次小的滑動并在一個時間段內(nèi)完成,彈性滑動的具體實現(xiàn)方式有很多,比如通過Scroller、Handler#postDelayed以及Thread#sleep等,下面一一進行介紹。
3.3.1 使用Scroller
Scroller的使用方法在3.1.4節(jié)中已經(jīng)進行了介紹,下面我們來分析一下它的源碼,從而探究為什么它能實現(xiàn)View的彈性滑動。
Scroller scroller = new Scroller(mContext); // 緩慢滾動到指定位置 private void smoothScrollTo(int destX, int destY) { int scrollX = getScrollX(); int deltaX = destX - scrollX; // 1000ms內(nèi)滑向destX,效果就是慢慢滑動 mScroller.startScroll(scrollX, 0, deltaX, 0, 1000); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
上面是Scroller的典型的使用方法,這里先描述它的工作原理:當我們構(gòu)造一個Scroller對象并且調(diào)用它的startScroll方法時,Scroller內(nèi)部其實什么也沒做,它只是保存了我們傳遞的幾個參數(shù),這幾個參數(shù)從startScroll的原型上就可以看出來,如下所示。
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; }
這個方法的參數(shù)含義很清楚,startX和startY表示的是滑動的起點,dx和dy表示的是要滑動的距離,而duration表示的是滑動時間,即整個滑動過程完成所需要的時間,注意這里的滑動是指View內(nèi)容的滑動而非View本身位置的改變。可以看到,僅僅調(diào)用startScroll方法是無法讓View滑動的,因為它內(nèi)部并沒有做滑動相關的事,那么Scroller到底是如何讓View彈性滑動的呢?答案就是startScroll方法下面的invalidate方法,雖然有點不可思議,但是的確是這樣的。invalidate方法會導致View重繪,在View的draw方法中又會去調(diào)用computeScroll方法,computeScroll方法在View中是一個空實現(xiàn),因此需要我們自己去實現(xiàn),上面的代碼已經(jīng)實現(xiàn)了computeScroll方法。正是因為這個computeScroll方法,View才能實現(xiàn)彈性滑動。這看起來還是很抽象,其實這樣的:當View重繪后會在draw方法中調(diào)用computeScroll,而computeScroll又會去向Scroller獲取當前的scrollX和scrollY;然后通過scrollTo方法實現(xiàn)滑動;接著又調(diào)用postInvalidate方法來進行第二次重繪,這一次重繪的過程和第一次重繪一樣,還是會導致computeScroll方法被調(diào)用;然后繼續(xù)向Scroller獲取當前的scrollX和scrollY,并通過scrollTo方法滑動到新的位置,如此反復,直到整個滑動過程結(jié)束。
我們再看一下Scroller的computeScrollOffset方法的實現(xiàn),如下所示。
/** * Call this when you want to know the new location. If it returns true, * the animation is not yet finished. */ public boolean computeScrollOffset() { ... 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; ... } } return true; }
是不是突然就明白了?這個方法會根據(jù)時間的流逝來計算出當前的scrollX和scrollY的值。計算方法也很簡單,大意就是根據(jù)時間流逝的百分比來算出scrollX和scrollY改變的百分比并計算出當前的值,這個過程類似于動畫中的插值器的概念,這里我們先不去深究這個具體過程。這個方法的返回值也很重要,它返回true表示滑動還未結(jié)束,false則表示滑動已經(jīng)結(jié)束,因此當這個方法返回true時,我們要繼續(xù)進行View的滑動。
通過上面的分析,我們應該明白Scroller的工作原理了,這里做一下概括:Scroller本身并不能實現(xiàn)View的滑動,它需要配合View的computeScroll方法才能完成彈性滑動的效果,它不斷地讓View重繪,而每一次重繪距滑動起始時間會有一個時間間隔,通過這個時間間隔Scroller就可以得出View當前的滑動位置,知道了滑動位置就可以通過scrollTo方法來完成View的滑動。就這樣,View的每一次重繪都會導致View進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,這就是Scroller的工作機制。由此可見,Scroller的設計思想是多么值得稱贊,整個過程中它對View沒有絲毫的引用,甚至在它內(nèi)部連計時器都沒有。
3.3.2 通過動畫
動畫本身就是一種漸近的過程,因此通過它來實現(xiàn)的滑動天然就具有彈性效果,比如以下代碼可以讓一個View的內(nèi)容在100ms內(nèi)向左移動100像素。
ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration (100).start();
不過這里想說的并不是這個問題,我們可以利用動畫的特性來實現(xiàn)一些動畫不能實現(xiàn)的效果。還拿scrollTo來說,我們也想模仿Scroller來實現(xiàn)View的彈性滑動,那么利用動畫的特性,我們可以采用如下方式來實現(xiàn):
final int startX = 0; final int deltaX = 100; ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000); animator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animator) { float fraction = animator.getAnimatedFraction(); mButton1.scrollTo(startX + (int) (deltaX * fraction), 0); } }); animator.start();
在上述代碼中,我們的動畫本質(zhì)上沒有作用于任何對象上,它只是在1000ms內(nèi)完成了整個動畫過程。利用這個特性,我們就可以在動畫的每一幀到來時獲取動畫完成的比例,然后再根據(jù)這個比例計算出當前View所要滑動的距離。注意,這里的滑動針對的是View的內(nèi)容而非View本身。可以發(fā)現(xiàn),這個方法的思想其實和Scroller比較類似,都是通過改變一個百分比配合scrollTo方法來完成View的滑動。需要說明一點,采用這種方法除了能夠完成彈性滑動以外,還可以實現(xiàn)其他動畫效果,我們完全可以在onAnimationUpdate方法中加上我們想要的其他操作。
3.3.3 使用延時策略
本節(jié)介紹另外一種實現(xiàn)彈性滑動的方法,那就是延時策略。它的核心思想是通過發(fā)送一系列延時消息從而達到一種漸近式的效果,具體來說可以使用Handler或View的postDelayed方法,也可以使用線程的sleep方法。對于postDelayed方法來說,我們可以通過它來延時發(fā)送一個消息,然后在消息中來進行View的滑動,如果接連不斷地發(fā)送這種延時消息,那么就可以實現(xiàn)彈性滑動的效果。對于sleep方法來說,通過在while循環(huán)中不斷地滑動View和sleep,就可以實現(xiàn)彈性滑動的效果。
下面采用Handler來做個示例,其他方法請讀者自行去嘗試,思想都是類似的。下面的代碼在大約1000ms內(nèi)將View的內(nèi)容向左移動了100像素,代碼比較簡單,就不再詳細介紹了。之所以說大約1000ms,是因為采用這種方式無法精確地定時,原因是系統(tǒng)的消息調(diào)度也是需要時間的,并且所需時間不定。
private static final int MESSAGE_SCROLL_TO = 1; private static final int FRAME_COUNT = 30; private static final int DELAYED_TIME = 33; private int mCount = 0; @SuppressLint("HandlerLeak") private Handler mHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_SCROLL_TO: { mCount++; if (mCount <= FRAME_COUNT) { float fraction = mCount / (float) FRAME_COUNT; int scrollX = (int) (fraction * 100); mButton1.scrollTo(scrollX, 0); mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME); } break; } default: break; } }; };
上面幾種彈性滑動的實現(xiàn)方法,在介紹中側(cè)重更多的是實現(xiàn)思想,在實際使用中可以對其靈活地進行擴展從而實現(xiàn)更多復雜的效果。