Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android Scroll詳解(二):OverScroller實戰

Android Scroll詳解(二):OverScroller實戰

編輯:關於Android編程

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

對android滾動相關的知識還不太了解的同學可以先閱讀一下文章:

《Android-MotionEvent詳解》 《Android Scroll詳解(一):基礎知識》

為了節約你的時間,我特地將文章大致內容總結如下:

手勢Drag的實現和原理 手勢Fling的實現和原理 OverScroll效果和EdgeEffect效果的實現和原理。

詳細代碼請查看我的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為mSecondaryPointerIdACTION_MOVE事件發生時,我們除了根據第一觸摸點pointer的x,y值進行滾動外,也要更新mSecondayLastXmSecondaryLastYACTION_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的機制,希望大家多多關注。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved