View的事件體系-彈性滑動

知道了View的滑動,我們還要知道如何實現View的彈性滑動,比較生硬的滑動過去,這種方式的用戶體驗太差了,因此我們要實現漸進式滑動。那麼如何實現彈性滑動呢?其實實現方法有很多,但它們都有一個共同思想:將一次大的滑動分成若干次小的滑動,並在一個事件內完成,彈性滑動的具體實現方式有很多,比如通過Scroller、Handler#postDelayed以及Thread#Sleep等,下面一一進行介紹。

1.使用Scroller

Scroller在前面已經進行了介紹,下面我們來分析以下它的源碼,從而探究爲什麼它能實現View的彈性滑動。

    Scroller scroller = new Scroller(mContext);

    //緩慢滑動到指定位置
    private void smoothScrollTo(int destX,int destY){
        int scrollX = getScrollX();
        int deltaX = destX - scrollX;
        //1000ms內滑向destX,效果就是慢慢滑動
        scroller.startScroll(scrollX,0,deltaX, 0,1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();
        }
    }

上面是Scroller的典型的使用方法,這裏先描述它的工作原理:當我們構造一個Scroller對象並且調用它的startScroll方法時,Scroller內部其實什麼也沒有做,它只是保存了我們傳遞的幾個參數,這幾個參數從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;
    }

這個方法的參數含義很清楚,startX和startY表示的是滑動的起點,dx和dy表示的是滑動的距離,而duration表示的是滑動的時間,即整個滑動過程完成所需要的時間,注意這裏的滑動式指View內容的滑動而非View本身位置的改變。可以看到,僅僅調用startScroll方法式無法讓View滑動的,因爲它內部並沒有做滑動的相關的事,那麼Scroller到底事如何讓View彈性滑動的呢?答案就是startScroll方法下面的invalidate方法,雖然有點不可思議,但是的確是這樣的。invalidate方法導致View重繪,在View的draw方法中又會去調用computeScroll方法,computeScroll在View中是一個空實現,因此需要我們自己去實現,上面的代碼已經實現了computeScroll方法。正是因爲這個computeScroll方法,View才能實現彈性滑動。這看起來還是很抽象的,其實這樣的:當View重繪後會在draw方法中調用computeScroll,而computeScroll又回去向Scroller獲取當前的scrollX和scrollY;然後通過scrollTo方法實現滑動;接着又調用postInvalidate方法來進行第二次重繪,這一次的重繪過程和第一次重繪一樣,還是會導致computeScroll方法被調用;然後繼續向Scroller獲取當前的scrollX和scrollY,並通過scrollTo方法滑動到新的位置,如此反覆,直到整個滑動過程結束。
我們再看以下Scroller的computeScrollOffset方法的實現,如下所示。
    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;
    }

是不是突然就明白了?這個方法會根據時間的流逝來計算出當前的scrollX和scrollY的值。計算方法也很簡單,大意就是根據時間流逝的百分比來算出scrollX和scrollY改變的百分比並計算出當前的值,這個過程類似與動畫中的插值器的概念,這裏我們先不去探究這個過程這個具體過程。這個方法的返回值也很重要,它返回true表示滑動未結束,false表示滑動已經結束,因此當這個方法返回tue時,我們要繼續進行View的滑動。
通過上面的分析,我們繼續明白Scroller的工作原理,這裏做一下概括:Scroller本身並不能實現View的滑動,它需要配合View的computeScroll方法才能完成彈性滑動的效果,它不斷地讓View重繪,而每一次重繪滑動其實時間會有一個時間間隔,通過這個時間間隔Scroller就可以得出View當前地滑動位置,知道了滑動位置就可以通過scrollTo方法來完成View滑動。就這樣,View的每一次重繪都會導致View進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,這就是Scroller的工作機制。因此可見,Scroller的設計思想時多麼值得稱讚,這個過程中它對View沒有絲毫的引用,甚至在它內部連計時器都沒有。

2.通過動畫

動畫本身就是一個漸近的過程,因此通過它來實現的滑動天然就具有彈性效果,比如以下代碼可以讓一個View的內容在100ms內向左移動100像素。
ObjectAnimator.ofFloat(targetView,"translationX", 0,100).setDuration(100).start();

不過這裏想說的並不是這個問題,我們可以利用動畫的特性來實現一些動畫不能實現的效果。還拿scrollTo來說,我們也想模仿Scroller來實現View的彈性滑動,那麼利用動畫的特性,我們可以採用如下方式來實現:

final int startX = 0;
        final int deltaX = 100;
        ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                targetView.scrollTo(startX+(int)(deltaX*fraction),0);
            }
        });
        animator.start();

在上述代碼中,我們的公話本質上沒有作用於任何對象上,他只是在1000ms內完成了整個動畫過程。利用這個特性,我們就可以在動畫的每一幀到來時獲取動畫完成的比例,然後再根據這個比例計算出當前View所要滑動的距離。注意,這裏的滑動針對的是View的內容而非View本身。可以發現,這個方法的思想其實和Scroller比較類似,都是通過改變一個百分比配合scrollTo方法來完成View的滑動。需要說明的一點,採用這種方式除了能夠完成滑動以外,還可以實現其他動畫效果,我們完全可以再onAnimationUpdate方法中加上我們想要的其他操作。

3.使用延時策略

本節介紹另外一種實現彈性滑動的方法,那就是延時策略。它的核心思想是通過發生一系列延時消息從而達到一種漸進式的效果,具體來說可以使用Handler或者View的postDelayed方法,也可以使用線程Sleep方法。對於postDelayed方法來說,我們可以通過它來延遲發送一個消息,然後再消息中來進行View的滑動,如果接不斷地發送這種延時消息,那麼就可以實現彈性滑動的效果。對於sleep方法來說,通過在while循環中不斷地滑動View和sleep,就可以實現彈性滑動的效果。
下面採用Handler來左個示例,其他方法請自行去嘗試,思想都是類似的。下面的代碼大約1000ms內將View的內容向左移動了100像素代碼比較簡單,就不再詳細介紹了。之所以說大約1000ms,是因爲採用這種方式無法精確地定時,原因是系統的消息調度也是需要時間的,並且所需時間不定。
    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;

    private Handler mHandler = new Handler(){
        @Override
        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);
                        targetView.scrollTo(scrollX,0);
                        mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME);
                    }
                    break;
                }
                default:
                    break;
            }
        }
    };
上面幾種彈性滑動的實現方式,在介紹中側重更多的是實現思想,在實際使用中可以對其靈活地進行擴展從而實現更多複雜地效果。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章