Android Scroll詳解(二):OverScroller實戰

做者:ztelur
聯繫方式:segmentfaultcsdngithub
本文僅供我的學習,不用於任何形式商業目的,轉載請註明原做者、文章來源,連接,版權歸原文做者全部。html

本文是android滾動相關的系列文章的第二篇,主要總結一下使用手勢相關的代碼邏輯。主要是單點拖動,多點拖動,fling和OveScroll的實現。每一個手勢都會有代碼片斷。java

對android滾動相關的知識還不太瞭解的同窗能夠先閱讀一下文章:android

爲了節約你的時間,我特意將文章大體內容總結以下:spring

  • 手勢Drag的實現和原理canvas

  • 手勢Fling的實現和原理segmentfault

  • OverScroll效果和EdgeEffect效果的實現和原理。ide

詳細代碼請查看個人github函數

Drag

Drag是最爲基本的手勢:用戶可使用手指在屏幕上滑動,以拖動屏幕相應內容移動。實現Drag手勢其實很簡單,步驟以下:

  • ACTION_DOWN事件發生時,調用getXgetY函數得到事件發生的x,y座標值,並記錄在mLastXmLastY變量中。

  • ACTION_MOVE事件發生時,調用getXgetY函數得到事件發生的x,y座標值,將其與mLastXmLastY比較,若是兩者差值大於必定限制(ScaledTouchSlop),就執行scrollBy函數,進行滾動,最後更新mLastXmLastY的值。

  • ACTION_UPACTION_CANCEL事件發生時,清空mLastXmLastY

@Override
public boolean onTouchEvent(MotionEvent event) {
    int actionId = MotionEventCompat.getActionMasked(event);
    switch (actionId) {
        case MotionEvent.ACTION_DOWN:
            mLastX = event.getX();
            mLastY = event.getY();
            mIsBeingDragged = true;
            if (getParent() != null) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            break;
        case MotionEvent.ACTION_MOVE:
            float curX = event.getX();
            float curY = event.getY();
            int deltaX = (int) (mLastX - curX);
            int deltaY = (int) (mLastY - curY);
            if (!mIsBeingDragged && (Math.abs(deltaX)> mTouchSlop ||
                                                    Math.abs(deltaY)> mTouchSlop)) {
                mIsBeingDragged = true;
                // 讓第一次滑動的距離和以後的距離不至於差距太大
                // 由於第一次必須>TouchSlop,以後則是直接滑動
                if (deltaX > 0) {
                    deltaX -= mTouchSlop;
                } else {
                    deltaX += mTouchSlop;
                }
                if (deltaY > 0) {
                    deltaY -= mTouchSlop;
                } else {
                    deltaY += mTouchSlop;
                }
            }
            // 當mIsBeingDragged爲true時,就不用判斷> touchSlopg啦,否則會致使滾動是一段一段的
            // 不是很連續
            if (mIsBeingDragged) {
                    scrollBy(deltaX, deltaY);
                    mLastX = curX;
                    mLastY = curY;
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mIsBeingDragged = false;
            mLastY = 0;
            mLastX = 0;
            break;
        default:
    }
    return mIsBeingDragged;
}

多觸點Drag

上邊的代碼只適用於單點觸控的手勢,若是你是兩個手指觸摸屏幕,那麼它只會根據你第一個手指滑動的狀況來進行屏幕滾動。更爲致命的是,當你先鬆開第一個手指時,因爲咱們少監聽了ACTION_POINTER_UP事件,將會致使屏幕忽然滾動一大段距離,由於第二個手指移動事件的x,y值會和第一個手指移動時留下的mLastXmLastY比較,致使屏幕滾動。

若是咱們要監聽並處理多觸點的事件,咱們還須要對ACTION_POINTER_DOWNACTION_POINTER_UP事件進行監聽,而且在ACTION_MOVE事件時,要記錄全部觸摸點事件發生的x,y值。

  • ACTION_POINTER_DOWN事件發生時,咱們要記錄第二觸摸點事件發生的x,y值爲mSecondaryLastXmSecondaryLastY,和第二觸摸點pointer的id爲mSecondaryPointerId

  • ACTION_MOVE事件發生時,咱們除了根據第一觸摸點pointer的x,y值進行滾動外,也要更新mSecondayLastXmSecondaryLastY

  • ACTION_POINTER_UP事件發生時,咱們要先判斷是哪一個觸摸點手指被擡起來啦,若是是第一觸摸點,那麼咱們就將座標值和pointer的id都更換爲第二觸摸點的數據;若是是第二觸摸點,就只要重置一下數據便可。

switch (actionId) {
    .....
    case MotionEvent.ACTION_POINTER_DOWN:
        activePointerIndex = MotionEventCompat.getActionIndex(event);
        mSecondaryPointerId = MotionEventCompat.findPointerIndex(event,activePointerIndex);
        mSecondaryLastX = MotionEventCompat.getX(event,activePointerIndex);
        mSecondaryLastY = MotionEventCompat.getY(event,mActivePointerId);
        break;
    case MotionEvent.ACTION_MOVE:
        ......
        // handle secondary pointer move
        if (mSecondaryPointerId != INVALID_ID) {
            int mSecondaryPointerIndex = MotionEventCompat.findPointerIndex(event, mSecondaryPointerId);
            mSecondaryLastX = MotionEventCompat.getX(event, mSecondaryPointerIndex);
            mSecondaryLastY = MotionEventCompat.getY(event, mSecondaryPointerIndex);
        }
        break;
    case MotionEvent.ACTION_POINTER_UP:
        //判斷是不是activePointer up了
        activePointerIndex = MotionEventCompat.getActionIndex(event);
        int curPointerId  = MotionEventCompat.getPointerId(event,activePointerIndex);
        Log.e(TAG, "onTouchEvent: "+activePointerIndex +" "+curPointerId +" activeId"+mActivePointerId+
                                "secondaryId"+mSecondaryPointerId);
        if (curPointerId == mActivePointerId) { // active pointer up
            mActivePointerId = mSecondaryPointerId;
            mLastX = mSecondaryLastX;
            mLastY = mSecondaryLastY;
            mSecondaryPointerId = INVALID_ID;
            mSecondaryLastY = 0;
            mSecondaryLastX = 0;
            //重複代碼,爲了讓邏輯看起來更加清晰
        } else{ //若是是secondary pointer up
            mSecondaryPointerId = INVALID_ID;
            mSecondaryLastY = 0;
            mSecondaryLastX = 0;
        }
        break;
    case MotionEvent.ACTION_CANCEL:
    case MotionEvent.ACTION_UP:
        mIsBeingDragged = false;
        mActivePointerId = INVALID_ID;
        mLastY = 0;
        mLastX = 0;
        break;
    default:
}

Fling

當用戶手指快速劃過屏幕,而後快速馬上屏幕時,系統會斷定用戶執行了一個Fling手勢。視圖會快速滾動,而且在手指馬上屏幕以後也會滾動一段時間。Drag表示手指滑動多少距離,界面跟着顯示多少距離,而fling是根據你的滑動方向與輕重,還會自動滑動一段距離。Filing手勢在android交互設計中應用很是普遍:電子書的滑動翻頁、ListView滑動刪除item、滑動解鎖等。因此如何檢測用戶的fling手勢是很是重要的。

在檢測Fling時,你須要檢測手指在屏幕上滑動的速度,這是你就須要VelocityTrackerScroller這兩個類啦。

  • 咱們首先使用VelocityTracker.obtain()這個方法得到其實例

  • 而後每次處理觸摸時間時,咱們將觸摸事件經過addMovement方法傳遞給它

  • 最後在處理ACTION_UP事件時,咱們經過computeCurrentVelocity方法得到滑動速度;

  • 咱們判斷滑動速度是否大於必定數值(MinFlingSpeed),若是大於,那麼咱們調用Scrollerfling方法。而後調用invalidate()函數。

  • 咱們須要重載computeScroll方法,在這個方法內,咱們調用ScrollercomputeScrollOffset()方法啦計算當前的偏移量,而後得到偏移量,並調用scrollTo函數,最後調用postInvalidate()函數。

  • 除了上述的操做外,咱們須要在處理ACTION_DOWN事件時,對屏幕當前狀態進行判斷,若是屏幕如今正在滾動(用戶剛進行了Fling手勢),咱們須要中止屏幕滾動。

具體這一套流程是如何運轉的,我會在下一篇文章中詳細解釋,你們也能夠本身查閱代碼或者google來搞懂其中的原理。

@Override
public boolean onTouchEvent(MotionEvent event) {
    .....
    if (mVelocityTracker == null) {
        //檢查速度測量器,若是爲null,得到一個
        mVelocityTracker = VelocityTracker.obtain();
    }
    int action = MotionEventCompat.getActionMasked(event);
    int index = -1;
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            ......
                            if (!mScroller.isFinished()) { //fling
                mScroller.abortAnimation();
            }
            .....
            break;
        case MotionEvent.ACTION_MOVE:
            ......
            break;
        case MotionEvent.ACTION_CANCEL:
            endDrag();
            break;
        case MotionEvent.ACTION_UP:
            if (mIsBeingDragged) {
            //當手指馬上屏幕時,得到速度,做爲fling的初始速度     mVelocityTracker.computeCurrentVelocity(1000,mMaxFlingSpeed);
                int initialVelocity = (int)mVelocityTracker.getYVelocity(mActivePointerId);
                if (Math.abs(initialVelocity) > mMinFlingSpeed) {
                    // 因爲座標軸正方向問題,要加負號。
                    doFling(-initialVelocity);
                }
                endDrag();
            }
            break;
        default:
    }
    //每次onTouchEvent處理Event時,都將event交給時間
    //測量器
    if (mVelocityTracker != null) {
        mVelocityTracker.addMovement(event);
    }
    return true;
}
private void doFling(int speed) {
    if (mScroller == null) {
        return;
    }
    mScroller.fling(0,getScrollY(),0,speed,0,0,-500,10000);
    invalidate();
}
@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        postInvalidate();
    }
}

OverScroll

在Android手機上,當咱們滾動屏幕內容到達內容邊界時,若是再滾動就會有一個發光效果。並且界面會進行滾動一小段距離以後再回復原位,這些效果是如何實現的呢?咱們須要使用ScrollerscrollTo的升級版OverScrolleroverScrollBy了,還有發光的EdgeEffect類。

咱們先來了解一下相關的API,理解了這些接口參數的含義,你就能夠輕鬆使用這些接口來實現上述的效果啦。

protected boolean overScrollBy(int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent)
  • int deltaX,int deltaY : 偏移量,也就是當前要滾動的x,y值。

  • int scrollX,int scrollY : 當前的mScrollX和mScrollY的值。

  • int maxOverScrollX,int maxOverScrollY: 標示能夠滾動的最大的x,y值,也就是你視圖真實的長和寬。也就是說,你的視圖可視大小多是100,100,可是視圖中的內容的大小爲200,200,因此,上述兩個值就爲200,200

  • int maxOverScrollX,int maxOverScrollY:容許超過滾動範圍的最大值,x方向的滾動範圍就是0~maxOverScrollX,y方向的滾動範圍就是0~maxOverScrollY。

  • boolean isTouchEvent:是否在onTouchEvent中調用的這個函數。因此,當你在computeScroll中調用這個函數時,就能夠傳入false。

protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)
  • int scrollX,int scrollY:就是x,y方向的滾動距離,就至關於mScrollXmScrollY。你既能夠直接把兩者賦值給相應的成員變量,也可使用scrollTo函數。

  • boolean clampedX,boolean clampY:表示是否到達超出滾動範圍的最大值。若是爲true,就須要調用OverScrollspringBack函數來讓視圖回覆原來位置。

public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
  • int startX,int startY:標示當前的滾動值,也就是mScrollXmScrollY的值

  • int minX,int maxX:標示x方向的合理滾動值

  • int minY,int maxY:標示y方向的合理滾動值

相信看完上述的API以後,你們會有不少的疑惑,因此這裏我來舉個例子。

假設視圖大小爲100*100。當你一直下拉到視圖上邊緣,而後在下拉,這時,mScrollY已經達到或者超過正常的滾動範圍的最小值了,也就是0,可是你的maxOverScrollY傳入的是10,因此,mScrollY最小能夠到達-10,最大能夠爲110。因此,你能夠繼續下拉。等到mScrollY到達或者超過-10時,clampedY就爲true,標示視圖已經達到能夠OverScroll的邊界,須要回滾到正常滾動範圍,因此你調用springBack(0,0,0,100)。

而後咱們再來看一下發光效果是如何實現的。

使用EdgeEffect類。通常來講,當你只上下滾動時,你只須要兩個EdgeEffect實例,分別表明上邊界和下邊界的發光效果。你須要在下面兩個情景下改變EdgeEffect的狀態,而後在draw()方法中繪製EdgeEffect

  • 處理ACTION_MOVE時,若是發現y方向的滾動值超過了正常範圍的最小值時,你須要調用上邊界實例的onPull方法。若是是超過最大值,那麼就是調用下邊界的onPull方法。

  • computeScroll函數中,也就是說Fling手勢執行過程當中,若是發現y方向的滾動值超過正常範圍時的最小值時,調用onAbsorb函數。

而後就是重載draw方法,讓EdgeEffect實例在畫布上繪製本身。你會發現,你必須對畫布進行移動或者旋轉來讓EdgeEffect繪製出上邊界或者下邊界的發光的效果,由於EdgeEffect對象本身是沒有上下左右的概念的。

@Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (mEdgeEffectTop != null) {
            final int scrollY = getScrollY();
            if (!mEdgeEffectTop.isFinished()) {
                final int count = canvas.save();
                final int width = getWidth() - getPaddingLeft() - getPaddingRight();
                canvas.translate(getPaddingLeft(),Math.min(0,scrollY));
                mEdgeEffectTop.setSize(width,getHeight());
                if (mEdgeEffectTop.draw(canvas)) {
                    postInvalidate();
                }
                canvas.restoreToCount(count);
            }

        }
        if (mEdgeEffectBottom != null) {
            final int scrollY = getScrollY();
            if (!mEdgeEffectBottom.isFinished()) {
                final int count = canvas.save();
                final int width = getWidth() - getPaddingLeft() - getPaddingRight();
                canvas.translate(-width+getPaddingLeft(),Math.max(getScrollRange(),scrollY)+getHeight());
                canvas.rotate(180,width,0);
                mEdgeEffectBottom.setSize(width,getHeight());
                if (mEdgeEffectBottom.draw(canvas)) {
                    postInvalidate();
                }
                canvas.restoreToCount(count);
            }

        }
    }
    
 @Override
    public boolean onTouchEvent(MotionEvent event) {
            ......
            case MotionEvent.ACTION_MOVE:
                .....
                if (mIsBeingDragged) {
                    overScrollBy(0,(int)deltaY,0,getScrollY(),0,getScrollRange(),0,mOverScrollDistance,true);
                    final int pulledToY = (int)(getScrollY()+deltaY);
                    mLastY = y;
                    if (pulledToY<0) {
                        mEdgeEffectTop.onPull(deltaY/getHeight(),event.getX(mActivePointerId)/getWidth());
                        if (!mEdgeEffectBottom.isFinished()) {
                            mEdgeEffectBottom.onRelease();
                        }
                    } else if(pulledToY> getScrollRange()) {
                        mEdgeEffectBottom.onPull(deltaY/getHeight(),1.0f-event.getX(mActivePointerId)/getWidth());
                        if (!mEdgeEffectTop.isFinished()) {
                            mEdgeEffectTop.onRelease();
                        }
                    }
                    if (mEdgeEffectTop != null && mEdgeEffectBottom != null &&(!mEdgeEffectTop.isFinished()
                                        || !mEdgeEffectBottom.isFinished())) {
                        postInvalidate();
                    }
                }
                .....
        }
        ....
    }
    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        if (!mScroller.isFinished()) {  
            int oldX = getScrollX();
            int oldY = getScrollY();
            scrollTo(scrollX,scrollY);
            onScrollChanged(scrollX,scrollY,oldX,oldY);
            if (clampedY) {
                Log.e("TEST1","springBack");
                mScroller.springBack(getScrollX(),getScrollY(),0,0,0,getScrollRange());
            }
        } else {
            // TouchEvent中的overScroll調用
            super.scrollTo(scrollX,scrollY);
        }
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            int oldX = getScrollX();
            int oldY = getScrollY();
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();

            int range = getScrollRange();
            if (oldX != x || oldY != y) {
                overScrollBy(x-oldX,y-oldY,oldX,oldY,0,range,0,mOverFlingDistance,false);
            }
            final int overScrollMode = getOverScrollMode();
            final boolean canOverScroll = overScrollMode == OVER_SCROLL_ALWAYS ||
                    (overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
            if (canOverScroll) {
                if (y<0 && oldY >= 0) {
                    mEdgeEffectTop.onAbsorb((int)mScroller.getCurrVelocity());
                } else if (y> range && oldY < range) {
                    mEdgeEffectBottom.onAbsorb((int)mScroller.getCurrVelocity());
                }
            }
        }
    }

後記

本篇文章是系列文章的第二篇,你們可能已經知道如何實現各種手勢,可是對其中的機制和原理還不是很瞭解,以後的第三篇會講解從本篇代碼的視角講解一下android視圖繪製的原理和Scroller的機制,但願你們多多關注。

參考文章

相關文章
相關標籤/搜索