Android Scroll詳解(二):OverScroller實戰(zhàn)

作者: ztelur
聯(lián)系方式:segmentfault,csdn,github

本文僅供個人學(xué)習(xí),不用于任何形式商業(yè)目的,轉(zhuǎn)載請注明原作者、文章來源,鏈接,版權(quán)歸原文作者所有。

本文是android滾動相關(guān)的系列文章的第二篇,主要總結(jié)一下使用手勢相關(guān)的代碼邏輯。主要是單點拖動,多點拖動,fling和OveScroll的實現(xiàn)。每個手勢都會有代碼片段。
?對android滾動相關(guān)的知識還不太了解的同學(xué)可以先閱讀一下文章:

為了節(jié)約你的時間,我特地將文章大致內(nèi)容總結(jié)如下:

  • 手勢Drag的實現(xiàn)和原理
  • 手勢Fling的實現(xiàn)和原理
  • OverScroll效果和EdgeEffect效果的實現(xiàn)和原理。

詳細(xì)代碼請查看我的github

Drag

Drag是最為基本的手勢:用戶可以使用手指在屏幕上滑動,以拖動屏幕相應(yīng)內(nèi)容移動。實現(xiàn)Drag手勢其實很簡單,步驟如下:

  • ACTION_DOWN事件發(fā)生時,調(diào)用getXgetY函數(shù)獲得事件發(fā)生的x,y坐標(biāo)值,并記錄在mLastXmLastY變量中。
  • ACTION_MOVE事件發(fā)生時,調(diào)用getXgetY函數(shù)獲得事件發(fā)生的x,y坐標(biāo)值,將其與mLastXmLastY比較,如果二者差值大于一定限制(ScaledTouchSlop),就執(zhí)行scrollBy函數(shù),進行滾動,最后更新mLastXmLastY的值。
  • ACTION_UPACTION_CANCEL事件發(fā)生時,清空mLastX,mLastY
    @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;
                    }
                }
                // 當(dāng)mIsBeingDragged為true時,就不用判斷> touchSlopg啦,不然會導(dǎo)致滾動是一段一段的
                // 不是很連續(xù)
                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

上邊的代碼只適用于單點觸控的手勢,如果你是兩個手指觸摸屏幕,那么它只會根據(jù)你第一個手指滑動的情況來進行屏幕滾動。更為致命的是,當(dāng)你先松開第一個手指時,由于我們少監(jiān)聽了ACTION_POINTER_UP事件,將會導(dǎo)致屏幕突然滾動一大段距離,因為第二個手指移動事件的x,y值會和第一個手指移動時留下的mLastXmLastY比較,導(dǎo)致屏幕滾動。

如果我們要監(jiān)聽并處理多觸點的事件,我們還需要對ACTION_POINTER_DOWNACTION_POINTER_UP事件進行監(jiān)聽,并且在ACTION_MOVE事件時,要記錄所有觸摸點事件發(fā)生的x,y值。

  • 當(dāng)ACTION_POINTER_DOWN事件發(fā)生時,我們要記錄第二觸摸點事件發(fā)生的x,y值為mSecondaryLastXmSecondaryLastY,和第二觸摸點pointer的id為mSecondaryPointerId
  • 當(dāng)ACTION_MOVE事件發(fā)生時,我們除了根據(jù)第一觸摸點pointer的x,y值進行滾動外,也要更新mSecondayLastXmSecondaryLastY
  • 當(dāng)ACTION_POINTER_UP事件發(fā)生時,我們要先判斷是哪個觸摸點手指被抬起來啦,如果是第一觸摸點,那么我們就將坐標(biāo)值和pointer的id都更換為第二觸摸點的數(shù)據(jù);如果是第二觸摸點,就只要重置一下數(shù)據(jù)即可。
        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;
                    //重復(fù)代碼,為了讓邏輯看起來更加清晰
                } 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

當(dāng)用戶手指快速劃過屏幕,然后快速立刻屏幕時,系統(tǒng)會判定用戶執(zhí)行了一個Fling手勢。視圖會快速滾動,并且在手指立刻屏幕之后也會滾動一段時間。Drag表示手指滑動多少距離,界面跟著顯示多少距離,而fling是根據(jù)你的滑動方向與輕重,還會自動滑動一段距離。Filing手勢在android交互設(shè)計中應(yīng)用非常廣泛:電子書的滑動翻頁、ListView滑動刪除item、滑動解鎖等。所以如何檢測用戶的fling手勢是非常重要的。
?在檢測Fling時,你需要檢測手指在屏幕上滑動的速度,這是你就需要VelocityTrackerScroller這兩個類啦。

  • 我們首先使用VelocityTracker.obtain()這個方法獲得其實例
  • 然后每次處理觸摸時間時,我們將觸摸事件通過addMovement方法傳遞給它
  • 最后在處理ACTION_UP事件時,我們通過computeCurrentVelocity方法獲得滑動速度;
  • 我們判斷滑動速度是否大于一定數(shù)值(MinFlingSpeed),如果大于,那么我們調(diào)用Scrollerfling方法。然后調(diào)用invalidate()函數(shù)。
  • 我們需要重載computeScroll方法,在這個方法內(nèi),我們調(diào)用ScrollercomputeScrollOffset()方法啦計算當(dāng)前的偏移量,然后獲得偏移量,并調(diào)用scrollTo函數(shù),最后調(diào)用postInvalidate()函數(shù)。
  • 除了上述的操作外,我們需要在處理ACTION_DOWN事件時,對屏幕當(dāng)前狀態(tài)進行判斷,如果屏幕現(xiàn)在正在滾動(用戶剛進行了Fling手勢),我們需要停止屏幕滾動。

具體這一套流程是如何運轉(zhuǎn)的,我會在下一篇文章中詳細(xì)解釋,大家也可以自己查閱代碼或者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) {
                //當(dāng)手指立刻屏幕時,獲得速度,作為fling的初始速度     mVelocityTracker.computeCurrentVelocity(1000,mMaxFlingSpeed);
                    int initialVelocity = (int)mVelocityTracker.getYVelocity(mActivePointerId);
                    if (Math.abs(initialVelocity) > mMinFlingSpeed) {
                        // 由于坐標(biāo)軸正方向問題,要加負(fù)號。
                        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手機上,當(dāng)我們滾動屏幕內(nèi)容到達(dá)內(nèi)容邊界時,如果再滾動就會有一個發(fā)光效果。而且界面會進行滾動一小段距離之后再回復(fù)原位,這些效果是如何實現(xiàn)的呢?我們需要使用ScrollerscrollTo的升級版OverScrolleroverScrollBy了,還有發(fā)光的EdgeEffect類。
?我們先來了解一下相關(guān)的API,理解了這些接口參數(shù)的含義,你就可以輕松使用這些接口來實現(xiàn)上述的效果啦。

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 : 偏移量,也就是當(dāng)前要滾動的x,y值。
  • int scrollX,int scrollY : 當(dāng)前的mScrollX和mScrollY的值。
  • int maxOverScrollX,int maxOverScrollY: 標(biāo)示可以滾動的最大的x,y值,也就是你視圖真實的長和寬。也就是說,你的視圖可視大小可能是100,100,但是視圖中的內(nèi)容的大小為200,200,所以,上述兩個值就為200,200
  • int maxOverScrollX,int maxOverScrollY:允許超過滾動范圍的最大值,x方向的滾動范圍就是0maxOverScrollX,y方向的滾動范圍就是0maxOverScrollY。
  • boolean isTouchEvent:是否在onTouchEvent中調(diào)用的這個函數(shù)。所以,當(dāng)你在computeScroll中調(diào)用這個函數(shù)時,就可以傳入false。
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)
  • int scrollX,int scrollY:就是x,y方向的滾動距離,就相當(dāng)于mScrollXmScrollY。你既可以直接把二者賦值給相應(yīng)的成員變量,也可以使用scrollTo函數(shù)。
  • boolean clampedX,boolean clampY:表示是否到達(dá)超出滾動范圍的最大值。如果為true,就需要調(diào)用OverScrollspringBack函數(shù)來讓視圖回復(fù)原來位置。
public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
  • int startX,int startY:標(biāo)示當(dāng)前的滾動值,也就是mScrollXmScrollY的值。
  • int minX,int maxX:標(biāo)示x方向的合理滾動值
  • int minY,int maxY:標(biāo)示y方向的合理滾動值。

相信看完上述的API之后,大家會有很多的疑惑,所以這里我來舉個例子。
?假設(shè)視圖大小為100*100。當(dāng)你一直下拉到視圖上邊緣,然后在下拉,這時,mScrollY已經(jīng)達(dá)到或者超過正常的滾動范圍的最小值了,也就是0,但是你的maxOverScrollY傳入的是10,所以,mScrollY最小可以到達(dá)-10,最大可以為110。所以,你可以繼續(xù)下拉。等到mScrollY到達(dá)或者超過-10時,clampedY就為true,標(biāo)示視圖已經(jīng)達(dá)到可以O(shè)verScroll的邊界,需要回滾到正常滾動范圍,所以你調(diào)用springBack(0,0,0,100)。

然后我們再來看一下發(fā)光效果是如何實現(xiàn)的。
?使用EdgeEffect類。一般來說,當(dāng)你只上下滾動時,你只需要兩個EdgeEffect實例,分別代表上邊界和下邊界的發(fā)光效果。你需要在下面兩個情景下改變EdgeEffect的狀態(tài),然后在draw()方法中繪制EdgeEffect

  • 處理ACTION_MOVE時,如果發(fā)現(xiàn)y方向的滾動值超過了正常范圍的最小值時,你需要調(diào)用上邊界實例的onPull方法。如果是超過最大值,那么就是調(diào)用下邊界的onPull方法。
  • computeScroll函數(shù)中,也就是說Fling手勢執(zhí)行過程中,如果發(fā)現(xiàn)y方向的滾動值超過正常范圍時的最小值時,調(diào)用onAbsorb函數(shù)。

然后就是重載draw方法,讓EdgeEffect實例在畫布上繪制自己。你會發(fā)現(xiàn),你必須對畫布進行移動或者旋轉(zhuǎn)來讓EdgeEffect繪制出上邊界或者下邊界的發(fā)光的效果,因為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調(diào)用
            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());
                }
            }
        }
    }

后記

本篇文章是系列文章的第二篇,大家可能已經(jīng)知道如何實現(xiàn)各類手勢,但是對其中的機制和原理還不是很了解,之后的第三篇會講解從本篇代碼的視角講解一下android視圖繪制的原理和Scroller的機制,希望大家多多關(guān)注。

參考文章

http://stackoverflow.com/questions/22843671/android-swipe-vs-fling

https://www.google.com/design/spec/patterns/gestures.html#gestures-drag-swipe-or-fling-details

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2014/1212/2145.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容