1. 程式人生 > >Android中View的彈性滑動——Android開發藝術探索筆記

Android中View的彈性滑動——Android開發藝術探索筆記

介紹

彈性滑動也就是漸進式滑動,實現彈性滑動的方法有很多,但是他們都有一個共同的思想:將一次大的滑動分成若干次小的滑動並在一段時間內完成。本文主要介紹三種彈性滑動方式,Scroller、動畫和Handler。

本文中的“滑動”是指View內容的滑動而非View本身位置的改變。

示例

點選螢幕任意地方,手指與螢幕接觸時,觸發ACTION_DOWN螢幕中的文字會向上滑動400px,手指離開螢幕時觸發ACTION_UP文字下滑400px。

示例

基礎知識補充

  • View的寬高和座標關係:width = right - left,height = top - bottom。
  • View在平移過程中,top和left表示的是原始左上角的位置資訊,其值不會改變,發生改變的是x、y、translationX、translationY這四個引數。
  • x是View左上角的座標,translation是view移動後相對於父容器的偏移量,所以有x = left + translationX。y的原理相同。
  • getX/getY返回的是相對於當前View左上角的x和y座標,而getRawX/getRawY返回的是相對於手機螢幕左上角的x和y座標。

使用Scroller

private void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int detlaX = destX - scrollX;
        int
scrollY = getScrollY(); int detlaY = destY - scrollY; Log.d(TAG, "smoothScrollTo:scrollY, detlaY= " + scrollY+" " + detlaY); mScroller.startScroll(scrollX, scrollY, detlaX, detlaY, 1000); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }

上面是使用Scroller實現彈性滑動的一個典型方法。

實現原理為:Scroller本身無法讓View彈性滑動,它需要和View的computeScroll方法配合使用才能共同完成這個功能。在startScroll()方法下呼叫了invalidate(),這使得View重繪,View重繪的時候會在draw方法中呼叫computeScroll(),在此方法中呼叫scrollTo滑向指定位置。之後再通過postInvalidate()進行二次重繪,如此重複直到滑動結束。

下面是computeScrollOffset()這個函式的原始碼。從中可以看出判斷滑動是否結束是以timePassed為標準的。

/**
     * 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:
            ...
            ...
                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

使用動畫

    private static final int FRAME_COUNT = 30;
    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();

上述程式碼中,動畫本質上沒有作用於任何物件上,只是在1000ms內完成了整個動畫過程。這個方法scrollTo(startX + (int) (deltaX * fraction), 0)使得動畫每次更新時view都滑動一點。

使用Handler

    private static final int MESSAGE_SCROLL_TO = 1;
    private static final int DELAYED_TIME = 33;

 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;
            }
        };
    };

在Handler的handleMessage中呼叫View的scrollTo方法滑動一些距離,緊接著再向Handler傳送一個Delay的訊息再次滑動。關鍵方法mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME)。

示例動畫效果的原始碼

示例中的動畫效果是在一個TextView進行的,滑動的是TextView中的文字。原始碼如下。


/**
 * Created by Spark on 2/22/2016 21:12.
 */

public class ElasticText extends TextView {
    private static final String TAG = "ElasticText";
    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    public ElasticText(Context context) {
        super(context);
        init();
    }

    public ElasticText(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ElasticText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                Log.d(TAG, "onTouchEvent: ActionDown");
                mScroller.startScroll(getScrollX(), getScrollY(), 0, 400, 600);
                invalidate();
                break;
            }
            case MotionEvent.ACTION_MOVE: {

                break;
            }
            case MotionEvent.ACTION_UP: {
                Log.d(TAG, "onTouchEvent: ACTION_UP");
                mScroller.startScroll(getScrollX(), getScrollY(), 0, -400, 600);
                invalidate();
                break;
            }
            default:
                break;
        }

        return true;
    }

    private void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int detlaX = destX - scrollX;
        int scrollY = getScrollY();
        int detlaY = destY - scrollY;
        mScroller.startScroll(scrollX, scrollY, detlaX, detlaY, 1000);
        invalidate();
    }

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