View的滑動 一、座標系 二、VelocityTracker、GestureDetector 三、View的滑動

一、座標系

1、上圖圓點是手指觸摸點,藍色的是MotionEvent的方法,點擊事件走到onTouchEvent,獲得點擊事件的各種座標:

getX、getY是相對view;getRawX、getRawY是相對屏幕。

2、綠色的是View獲取座標的方法。ViewGroup是View的父佈局。getLeft、getRight、getTop、getBottom相對父佈局。

width = getRight()- getLeft()

height = getTop()- getBottom()

二、VelocityTracker、GestureDetector

1、VelocityTracker

用於追蹤手指滑動速度的。例如相冊的圖片,手指快速左右滑動會切換圖片,慢則不會切換。

獲取速度前,要先調用computeCurrentVelocity計算速度,如下代碼。效果是手指滑的快時,就會彈Toast。

    private void init() {
        //獲取速度追蹤器
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        //獲取觸摸點座標
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
                //這句是爲了隨手指滑動,下面會講
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

                //速度器添加事件
                mVelocityTracker.addMovement(event);

                break;
            case MotionEvent.ACTION_UP:

                //計算手指滑動速度:1000ms內滑過的像素,(終點-起點)/時間段,所以從右向左滑 爲負值。
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                Log.i(TAG, "onTouchEvent: xVelocity = " + xVelocity);
                if (Math.abs(xVelocity) > 100) {
                    Toast.makeText(getContext(), "滑的有點快!", Toast.LENGTH_SHORT).show();
                }

                break;
            default:
                break;
        }

2、GestureDetector

手勢檢測。通常監聽雙擊 才使用GestureDetector,其他的滑動就在onTouchEvent中實現(DOWN、MOVE、UP)就可以了。

    private void init() {
        //要設置兩個監聽OnGestureListener、OnDoubleTapListener
        mGestureDetector = new GestureDetector(getContext(),this);
        mGestureDetector.setOnDoubleTapListener(this);
    }

    @Override
    public boolean onDown(MotionEvent e) {
        //手指觸摸的一瞬間,由1個DOWN觸發
        Log.i(TAG, "onDown: ");
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
        //手指觸摸的狀態,由1個DOWN觸發,強調的是沒有拖動的狀態,就是按着沒動。
        Log.i(TAG, "onShowPress: ");
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        //單擊,UP觸發
        Log.i(TAG, "onSingleTapUp: ");
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        //滾動,1個DOWN,多個MOVE觸發
        Log.i(TAG, "onScroll: ");
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
        //長按
        Log.i(TAG, "onLongPress: ");
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        //快速move後up
        Log.i(TAG, "onFling: ");
        return false;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        //確認的單擊,不是雙擊中的某一擊
        Log.i(TAG, "onSingleTapConfirmed: ");
        return false;
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        //雙擊,兩個單擊組成。 和onSingleTapConfirmed不能共存
        Log.i(TAG, "onDoubleTap: ");
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        //發生了雙擊行爲,雙擊期間DOWN、MOVE、UP都會觸發此回調
        Log.i(TAG, "onDoubleTapEvent: ");
        return false;
    }

三、View的滑動

滑動是自定義view的基礎。共有6種滑動方法。

1、layout()

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        //獲取觸摸點座標
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                //DOWN時,即剛開始的觸摸點相對view的座標。
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //滑動的距離 = 觸摸點滑動到的座標 - 開始觸摸的座標 (都是相對於view本身)
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;

                //所以View也要跟上這個滑動距離——有多重方式:

                //方法一,layout()
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

每次移動都會調layout重新佈局,也就是滑動的效果。

效果如圖,隨着手指滑動:

2、offsetLeftAndRight、offsetTopAndBottom

            case MotionEvent.ACTION_MOVE:
                //滑動的距離 = 觸摸點滑動到的座標 - 開始觸摸的座標 (都是相對於view本身)
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;

                //所以View也要跟上這個滑動距離——有多重方式:

                //方法一,layout()
//                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

                //方法二,offsetLeftAndRight、offsetTopAndBottom
                offsetLeftAndRight(offsetX);
                offsetTopAndBottom(offsetY);

3、LayouParams

            case MotionEvent.ACTION_MOVE:
                //滑動的距離 = 觸摸點滑動到的座標 - 開始觸摸的座標 (都是相對於view本身)
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;

                //所以View也要跟上這個滑動距離——有多重方式:

                //方法一,layout()
//                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);

                //方法二,offsetLeftAndRight、offsetTopAndBottom
//                offsetLeftAndRight(offsetX);
//                offsetTopAndBottom(offsetY);

                //方法三,LayoutParams
                ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                setLayoutParams(layoutParams);

4、動畫

關於動畫,後面單獨寫一篇詳細講。現在知道有 :View動畫(不能改變位置參數)、屬性動畫可以實現。

                //方法四,動畫(一般在外面調用)
                //1、view動畫(最終效果是滑動第一下可以滑動,後面再滑不行,因爲view不能改變view的位置參數,不能真正的交互。)
                float toXDelta = offsetX / getWidth();
                float toYDelta = offsetY / getHeight();
                TranslateAnimation animation = new TranslateAnimation(
                        TranslateAnimation.RELATIVE_TO_SELF, 0,
                        TranslateAnimation.RELATIVE_TO_SELF, toXDelta,
                        TranslateAnimation.RELATIVE_TO_SELF, 0,
                        TranslateAnimation.RELATIVE_TO_SELF, toYDelta);
                animation.setDuration(0);
                animation.setFillAfter(true);
                startAnimation(animation);

                //2、屬性動畫(橫移,貌似不適合放這裏使用,效果會閃)
                ObjectAnimator.ofFloat(this,"translationX",0, offsetX).setDuration(0).start();

5、scrollTo、scrollBy

scrollTo(x,y)是瞬間移動到(x,y),scrollBy(deltaX,deltaY)是移動增量。代碼使用方法:

            case MotionEvent.ACTION_MOVE:
                //滑動的距離 = 觸摸點滑動到的座標 - 開始觸摸的座標 (都是相對於view本身)
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;

                //所以View也要跟上這個滑動距離——有多重方式:

                //方法五,scrollTo、scrollBy。

//                ((View) getParent()).scrollTo(getScrollX() - offsetX, getScrollY() - offsetY);
                //scrollBy同理:
                ((View)getParent()).scrollBy(-offsetX, -offsetY);

說明1:

scrollBy還是調的scrollTo :

    /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

mScrollX:View左邊緣和 view的內容(即view的子view)左邊緣的距離。負值表示 view左邊緣在view內容的左側。

mScrollY:VIew上邊緣和 view的內容(即view的子view)上邊緣的距離。負值表示 view上邊緣在view內容的上面。

說明2:

scrollBy、scrollTo移動的是view的內容,如果是ViewGroup使用,即移動其所有的子view,若沒有子view就沒有效果。所以上面代碼使用getParent()來調用。

或者,換一種理解方式(個人感覺這個更好理解)scrollBy、scrollTo移動的就是view本身,而view的內容不動,只不過此時屏幕也隨view本身一起移動,視覺上就是 view的內容 就會反向移動。 例如,使用view.scrollBy(100,0),那麼view和屏幕一起右移100,即視覺上view的內容左移100。所以要讓view的內容視覺上右移100,需要view的父view左移100,view.scrollBy(-100,0)。 所以,上面用getParent,而且傳的是負值

6、Scroller

Scroller是處理滑動效果的工具類,來實現有過度效果的彈性滑動(有個過程,不是瞬間完成的)。

Scroller本身不能實現彈性,需要結合View的computeScroll()方法。基本是固定的代碼:先new 一個Scroller,然後重寫computeScroll(),在寫一個方法供調用,這裏是smoothScrollTo。

    private void init() {
        mScroller = new Scroller(getContext());
    }

    /**
     * 彈性滑動
     *
     * @param desX     目標X
     * @param desY     目標Y
     * @param duration 時間
     */
    private void smoothScrollTo(int desX, int desY, int duration) {
        int deltaX = desX - getScrollX();
        int deltaY = desY - getScrollY();

        //前兩個是起點左邊,中間兩個是滑動距離,duration是時間。此時僅僅是存入數據,並沒有滑動。
        mScroller.startScroll(getScrollX(), getScrollY(), deltaX, deltaY, duration);

        //invalidate會導致重新繪製,即走draw(),然後走computeScroll()
        invalidate();
    }

    @Override
    public void computeScroll() {
        //計算本次滾動的位置,數據保存在Scroller中。返回true表示滾動未結束。
        if (mScroller.computeScrollOffset()) {
            //從Scroller中取出計算好的位置,並使用父view調scrollerTo來滑動 本身。
            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            //再次調繪製,又會走computeScroll(),繼續這個過程,直到mScroller.computeScrollOffset()返回false結束滑動。
            invalidate();
        }
    }

看下startScroll方法,僅僅保存了傳入的起點、移動距離、時間,並沒有真正移動。


computeScroll方法:根據時間計算當前一個滑動的距離,返回true表示滑動還沒結束。

所以,整個過程是:調用startScroll向Scroller傳入滑動的距離和時間,然後調用了invalidate(),invalidate會導致重新繪製,即走draw(),然後走computeScroll(),computeScroll中計算本次滾動的位置,數據保存在Scroller中,返回true表示滾動未結束。然後就調用scrollTo傳入計算好的當前的滑動距離,這樣就是實現了一小段的滑動。然後又調用invalidate(),就會繼續這個過程。最終實現彈性動畫。

參考資料:

《Android進階之光》第三章

《Android開發藝術探索》第三章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章