ScrollView 觸控事件

貓尾巴發表於2018-06-26

ScrollView 觸控事件一覽

ScrollView 繼承於 FrameLayout,屬於 ViewGroup 控制元件。View 樹的觸控事件是從 ViewGroup 的 dispatchTouchEvent 開始分發的。先判斷 ViewGroup 的 onInterceptTouchEvent 是否攔截,同時這裡也可以呼叫 ViewGroup 的 requestDisallowInterceptTouchEvent 讓 ViewGroup 不呼叫 onInterceptTouchEvent,如果事件被攔截,則呼叫 ViewGroup 的超類即 View 的 dispatchTouchEvent,反之,則呼叫子檢視的 dispatchTouchEvent 。

ScrollView 觸控事件
上圖針對的是 ACTION_DOWN 事件。

  • Activity 接收事件後,由 dispatchTouchEvent進行分發。Activity 的 dispatchTouchEvent 如果不呼叫 super (無論返回 true or false)則事件不會向下分發。所以一般 activity 的 dispatchTouchEvent 需要呼叫 super 才能向下分發。
  • ViewGroup 的 dispatchTouchEvent,用來向下分發事件。如果此方法內不呼叫super,直接返回 true 則代表直接消費終止。返回 false 代表不在分發直接交給復層處理。呼叫 super 則會執行 onInterceptTouchEvent 方法。
  • onInterceptTouchEvent 方法用來判斷當前 ViewGroup 是否需要攔截此事件。如果攔截返回 true,則直接呼叫當前 ViewGroup 的 onTouchEvent 自己處理。不需要攔截返回 false 或者直接呼叫 super 即可。
  • 最下次 View 的 dispatchTouchEvent 接受到事件後,true 代表消費終止,false 則直接呼叫自己的 onTouchEvent處理事件。注意 View 沒有 onInterceptTouchEvent,此方法只有 ViewGroup 有。
  • down 事件在哪個View 消費了,那麼 move 和 up 事件就只會從上向下傳遞到這個 view,不會繼續向下傳遞。

View 之 dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;
    ...
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    ...
    return result;
}
複製程式碼

根據原始碼可知:

  • 首先執行 dispatchTouchEvent 方法。
  • 在 dispatchTouchEvent 方法中先執行 onTouch 方法,後執行 onClick 方法(onClick方法在onTouchEvent中執行)。
  • 如果 onTouch 返回false或者 mOnTouchListener 為null(控制元件沒有設定 setOnTouchListener 方法)或者控制元件不是enable的情況下會調運 onTouchEvent , dispatchTouchEvent 返回值與 onTouchEvent 返回一樣。
  • 如果不是enable的設定了 onTouch 方法也不會執行,只能通過重寫控制元件的 onTouchEvent 方法處理, dispatchTouchEvent 返回值與 onTouchEvent 返回一樣。
  • 如果是enable且 onTouch 返回true情況下, dispatchTouchEvent 直接返回true,不會呼叫 onTouchEvent 方法。

View 之 onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
    ...
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        return (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
    }
    ...
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                ...
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClick();
                }
                ...
                break;
                
            case MotionEvent.ACTION_DOWN:
                ...
                break;

            case MotionEvent.ACTION_CANCEL:
                ...
                break;

            case MotionEvent.ACTION_MOVE:
                ...
                break;
        }
        return true;
    }
    return false;
}

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    return result;
}
複製程式碼

根據原始碼可知:

  • onTouchEvent 方法中會在ACTION_UP分支中觸發 onClick 的監聽。
  • 當 dispatchTouchEvent 在進行事件分發的時候,只有前一個action返回true,才會觸發下一個action(也就是說dispatchTouchEvent返回true才會進行下一次action派發)。

ViewGroup 之 dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action);
        } else {
            intercepted = false;
        }
    } else {
        intercepted = true;
    }
    ...
    // Dispatch to touch targets.
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } else {
        ...
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
            predecessor = target;
            target = next;
        }
    }
    if (canceled
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        resetTouchState();
    } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
        final int actionIndex = ev.getActionIndex();
        final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
        removePointersFromTouchTargets(idBitsToRemove);
    }
}
複製程式碼

根據原始碼可知:

  • 使用變數intercepted來標記ViewGroup是否攔截Touch事件的傳遞,mGroupFlags可以根據 requestDisallowInterceptTouchEvent 方法來設定是否攔截的標誌 FLAG_DISALLOW_INTERCEPT 。
  • FLAG_DISALLOW_INTERCEPT一旦設定之後,ViewGroup將無法攔截除ACTION_DOWN以外的其他點選事件。ViewGroup會在ACTION_DOWN事件到來時做重置狀態的操作。在resetTouchState方法中重置FLAG_DISALLOW_INTERCEPT標記位。因此,子View呼叫requestDisallowInterceptTouchEvent方法並不能影響ViewGroup對ACTION_DOWN事件的處理。當ViewGroup決定攔截事件後,那麼後續的點選事件將預設交給它處理並且不再呼叫它的onInterceptTouchEvent方法。FLAG_DISALLOW_INTERCEPT標記位的作用是讓ViewGroup不再攔截事件,前提是ViewGroup不攔截ACTION_DOWN事件。
  • dispatchTransformedTouchEvent 將Touch事件傳遞給特定的子View。在該方法中為一個遞迴呼叫,會遞迴呼叫 dispatchTouchEvent 方法。在 dispatchTouchEvent 中如果子View為ViewGroup並且Touch沒有被攔截那麼遞迴呼叫 dispatchTouchEvent ,如果子View為View那麼就會呼叫其 onTouchEvent 。 dispatchTransformedTouchEvent 方法如果返回true則表示子View消費掉該事件,同時進入該if判斷。
  • dispatchTransformedTouchEvent 方法返回false,即子View的 onTouchEvent 返回false(即Touch事件未被消費)。那麼該子View就無法繼續處理ACTION_MOVE事件和ACTION_UP事件。
  • dispatchTransformedTouchEvent 會呼叫遞迴呼叫 dispatchTouchEvent 和 onTouchEvent ,所以 dispatchTransformedTouchEvent 的返回值實際上是由 onTouchEvent 決定的。

ViewGroup 之 dispatchTransformedTouchEvent

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
    ...
    if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }
    }
    ...
}
複製程式碼

根據原始碼可知:

  • 當child == null時會將Touch事件傳遞給該ViewGroup自身的dispatchTouchEvent()處理,即super.dispatchTouchEvent(event)(也就是View的這個方法,因為ViewGroup的父類是View);當child != null時會呼叫該子view(當然該view可能是一個View也可能是一個ViewGroup)的dispatchTouchEvent(event)處理,即child.dispatchTouchEvent(event)。
  • Android事件派發是先傳遞到最頂級的ViewGroup,再由ViewGroup遞迴傳遞到View的。
  • 在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true代表不允許事件繼續向子View傳遞,返回false代表不對事件進行攔截,預設返回false。
  • 子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件。

常見滑動衝突場景

外部滑動和內部滑動方向不一致

  1. ViewPager和Fragment配合使用組成的頁面滑動效果。這種衝突的解決方式,一般都是根據水平滑動還是豎直滑動(滑動的距離差)來判斷到底是由誰來攔截事件。
  2. 外部滑動和內部滑動方向一致。內外兩層同時能上下滑動或者能同時左右滑動。這種一般都是根據業務來進行區分。
  3. 以上兩種場景的巢狀

滑動衝突的解決方式

外部攔截法

外部攔截法,就是所有事件都先經過父容器的攔截處理,由父容器來決定是否攔截。這種方式需要重寫父容器的onInterceptTouchEvent方法,虛擬碼如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if (父容器需要當前點選事件) {
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted=false;
            break;
        default:
            break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}
複製程式碼
  • 不攔截ACTION_DOWN事件。一旦父容器攔截ACTION_DOWN,則後續的ACTION_MOVE和ACTION_UP事件都會直接交由父容器處理,無法傳遞給子元素。
  • ACTION_MOVE事件根據具體需求來決定是否攔截。
  • ACTION_UP事件必須返回false,ACTION_UP事件本身沒什麼意義,但如果父容器在ACTION_UP返回true會導致子元素無法接收ACTION_UP事件,無法響應onClick事件。

內部攔截法

內部攔截法是指父容器不攔截任何事件,所有事件都傳遞給子元素。內部攔截法需要配合requestDisallowInterceptTouchEvent方法才能正常工作。這種方式需要重寫子元素的dispatchTouchEvent方法,虛擬碼如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (父容器需要當前點選事件) {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(ev);
}
複製程式碼

父元素需要預設攔截除ACTION_DOWN事件以外的其他事件,父元素修改如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction()==MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}
複製程式碼

ACTION_DOWN事件並不受FLAG_DISALLOW_INTERCEPT這個標記位的控制。一旦父容器攔截ACTION_DOWN事件,那麼所有的事件都無法傳遞到子元素中去。

ScrollView 觸控事件流程

onInterceptTouchEvent

onInterceptTouchEvent 所進行的處理,即在 ACTION_DOWN 資源初始化,ACTION_MOVE 判斷是否開始拖動手勢,ACTION_CANCEL && ACTION_UP 中進行資源釋放。這裡涉及了多指觸控的處理。

預處理

if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
    return true;
}
複製程式碼

用 mIsBeingDragged 變數來儲存當前是否已經開始進行拖動手勢,這個後面會講到,同時當前分發事件型別為 ACTION_MOVE,那麼直接返回 true,即攔截事件向子檢視進行分發。

ACTION_MOVE

if (!inChild((int) ev.getX(), (int) y)) {
    mIsBeingDragged = false;
    recycleVelocityTracker();
    break;
}
複製程式碼

如果觸控事件沒有作用於子檢視範圍內,那麼就不處理,同時釋放速度跟蹤器(一般用於 fling 手勢的判定)。

mLastMotionY = y;
mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);

mScroller.computeScrollOffset();
mIsBeingDragged = !mScroller.isFinished();

startNestedScroll(SCROLL_AXIS_VERTICAL);
複製程式碼

mLastMotionY 記錄按下時的座標資訊,mActivePointerId 記錄當前分發觸控事件的手指 id,這個一般用於多指的處理,initOrResetVelocityTracker 初始化速度跟蹤器,同時使用 addMovement 記錄當前觸控事件資訊,mScroller 是一般用於 fling 手勢處理,這裡的作用是處理上一次的 fling,startNestedScroll 則是巢狀滾動機制的知識了。

ACTION_MOVE

final int activePointerId = mActivePointerId;
if (activePointerId == INVALID_POINTER) {
    break;
}
final int pointerIndex = ev.findPointerIndex(activePointerId);
if (pointerIndex == -1) {
    break;
}
final int y = (int) ev.getY(pointerIndex);
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
    mIsBeingDragged = true;
    mLastMotionY = y;
    initVelocityTrackerIfNotExists();
    mVelocityTracker.addMovement(ev);
    final ViewParent parent = getParent();
    if (parent != null) {
        parent.requestDisallowInterceptTouchEvent(true);
    }
}
複製程式碼

對 mActivePointerId 進行是否為有效的判斷,如果有效,則通過 findPointerIndex 獲取作用手指 id 的下標,記錄為 pointerIndex ,為什麼要獲取這個值,我們知道現在的手機螢幕都是支援多指觸控的,所以我們需要根據某個按下的手指的觸控資訊來進行處理。yDiff 是滑動的距離,mTouchSlop 則是 SDK 定義的可作為判定是否開始進行拖動的距離常量,可以通過 ViewConfiguration 的 getScaledTouchSlop 獲取,如果大於這個值,我們可以認為開始了拖動的手勢。 getNestedScrollAxes 這個同樣是用於巢狀滾動機制的。如果開始了拖動手勢,mIsBeingDragged 標記為 true,同樣使用速度跟蹤器記錄資訊,這裡還會呼叫 ViewParent 的 requestDisallowInterceptTouchEvent,防止父檢視攔截了事件,即 onInterceptTouchEvent。

ACTION_CANCEL && ACTION_UP

mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
recycleVelocityTracker();
stopNestedScroll();
複製程式碼

進行一些釋放資源的操作,比如 mIsBeingDragged 設定為 false,釋放速度跟蹤器等等。

ACTION_UP 是所有的手指(多指觸控)抬起時分發的事件,而 ACTION_CANCEL 則是觸控取消事件型別,一般什麼時候會分發這個事件呢?舉個例子,如果某個子檢視已經消費了 ACTION_DOWN,即在這個事件分發時,向父檢視傳遞了 true 的返回值,那麼一般情況下,父檢視不會再攔截接下來的事件,比如 ACTION_MOVE 等,但是如果父檢視在這種情況下,還攔截了事件傳遞,即在 onInterceptTouch 中返回了 true,那麼在 ViewGroup 的 dispatchTouchEvent 中會給已經確認消費事件的子檢視分發一個 TOUCH_CANCEL 的事件。
複製程式碼

ACTION_POINTER_UP

多指觸控時,某個手指抬起時分發的事件。

final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
    mLastMotionY = (int) ev.getY(newPointerIndex);
    mActivePointerId = ev.getPointerId(newPointerIndex);
    if (mVelocityTracker != null) {
        mVelocityTracker.clear();
    }
}
複製程式碼

當某個手指抬起時,而這個手指剛好是我們當前使用的,則重新初始化資源。

onTouchEvent

onTouchEvent 和 onInterceptTouchEvent 處理有些相似,主要是在 TOUCH_MOVE 中在判定為拖動手勢後進行真正的業務邏輯處理,同時在 ACTION_UP 中根據速度跟蹤器的獲取的速度,判定是否符合 fling 手勢,如果符合,則使用 Scroller 進行計算。

ACTION_DOWN

if (getChildCount() == 0) {
    return false;
}
if ((mIsBeingDragged = !mScroller.isFinished())) {
    final ViewParent parent = getParent();
    if (parent != null) {
        parent.requestDisallowInterceptTouchEvent(true);
    }
}

if (!mScroller.isFinished()) {
    mScroller.abortAnimation();
}

mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
startNestedScroll(SCROLL_AXIS_VERTICAL);
複製程式碼

onTouchEvent 在 ACTION_DOWN 事件分發中,主要是進行資源初始化,同時也處理上一次的 fling 任務,比如呼叫 Scroller 的 abortAnimation,如果 Scroller 還沒結束 fling 計算,則中止處理。

ACTION_MOVE

final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
if (activePointerIndex == -1) {
    break;
}
final int y = (int) ev.getY(activePointerIndex);
int deltaY = mLastMotionY - y;
if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
    // 巢狀滾動處理
    deltaY -= mScrollConsumed[1];
    vtev.offsetLocation(0, mScrollOffset[1]);
    mNestedYOffset += mScrollOffset[1];
}
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
    final ViewParent parent = getParent();
    if (parent != null) {
        parent.requestDisallowInterceptTouchEvent(true);
    }
    mIsBeingDragged = true;
    if (deltaY > 0) {
        deltaY -= mTouchSlop;
    } else {
        deltaY += mTouchSlop;
    }
}

if (mIsBeingDragged) {
    /// 業務邏輯
}
複製程式碼

進行多指處理,獲取指定手指的觸控事件資訊。mIsBeingDragged 為 false,同時會再進行一次拖動手勢的判定,判定邏輯和 onInterceptTouchEvent 中類似,如果 mIsBeingDragged 為 true,則開始進行真正的邏輯處理。

if (mIsBeingDragged) {
    mLastMotionY = y - mScrollOffset[1];

    final int oldY = mScrollY;
    final int range = getScrollRange();
    final int overscrollMode = getOverScrollMode();
    boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

    if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
            && !hasNestedScrollingParent()) {
        mVelocityTracker.clear();
    }

    final int scrolledDeltaY = mScrollY - oldY;
    final int unconsumedY = deltaY - scrolledDeltaY;
    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
        mLastMotionY -= mScrollOffset[1];
        vtev.offsetLocation(0, mScrollOffset[1]);
        mNestedYOffset += mScrollOffset[1];
    } else if (canOverscroll) {
        final int pulledToY = oldY + deltaY;
        if (pulledToY < 0) {
            mEdgeGlowTop.onPull((float) deltaY / getHeight(),
                    ev.getX(activePointerIndex) / getWidth());
            if (!mEdgeGlowBottom.isFinished()) {
                mEdgeGlowBottom.onRelease();
            }
        } else if (pulledToY > range) {
            mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
                    1.f - ev.getX(activePointerIndex) / getWidth());
            if (!mEdgeGlowTop.isFinished()) {
                mEdgeGlowTop.onRelease();
            }
        }
        if (mEdgeGlowTop != null
                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
            postInvalidateOnAnimation();
        }
    }
}
複製程式碼

EdgeEffect 是用於拖動時,邊緣的陰影效果。

ACTION_UP

if (mIsBeingDragged) {
    final VelocityTracker velocityTracker = mVelocityTracker;
	velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
    
    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
    	flingWithNestedDispatch(-initialVelocity);
    }
    
    mActivePointerId = INVALID_POINTER;
    endDrag();
}
複製程式碼

當手指全部抬起時,可以使用速度跟蹤器進行 fling 手勢的判定,同時釋放資源。通過 getYVelocity 獲取速度,在判斷是否可以作為 fling 手勢處理,mMaximumVelocity 是處理的最大速度,mMinimumVelocity 是處理的最小速度,這兩個值同樣可以通過 ViewConfiguration 的 getScaledMaximumFlingVelocity 和 getScaledMinimumFlingVelocity 獲取。一般情況對 fling 的處理是通過 Scroller 進行處理的,因為這裡涉及複雜的數學知識,而 Scroller 可以幫我們簡化這裡的操作,使用如下:

int height = getHeight() - mPaddingBottom - mPaddingTop;
int bottom = getChildAt(0).getHeight();

mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
                    Math.max(0, bottom - height), 0, height/2);

postInvalidateOnAnimation();
複製程式碼

通過傳遞當前拖動手勢速度值來呼叫 fling 進行處理,然後在 computeScrollOffset 方法中,進行真正的滾動處理:

public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      	int oldX = mScrollX;
        int oldY = mScrollY;
        int x = mScroller.getCurrX();
        int y = mScroller.getCurrY();

        if (oldX != x || oldY != y) {
            final int range = getScrollRange();
            final int overscrollMode = getOverScrollMode();
            final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
                    (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);

            overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
                    0, mOverflingDistance, false);
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);

            if (canOverscroll) {
                if (y < 0 && oldY >= 0) {
                    mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
                } else if (y > range && oldY <= range) {
                    mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
                }
            }
        }
        postInvalidateOnAnimation();
    }
}
複製程式碼

Scroller 並不會為我們進行滾動處理,它只是提供了計算的模型,通過呼叫 computeScrollOffset 進行計算,如果返回 true,表示計算還沒結束,然後通過 getCurrX 或 getCurrY 獲取計算後的值,最後進行真正的滾動處理,比如呼叫 scrollTo 等等,這裡需要注意的是,需要呼叫 invalidate 來確保進行下一次的 computeScroll 呼叫,這裡使用的 postInvalidateOnAnimation 其作用是類似的。

ACTION_CANCEL

if (mIsBeingDragged && getChildCount() > 0) {
    if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
        postInvalidateOnAnimation();
    }
    mActivePointerId = INVALID_POINTER;
    endDrag();
}
複製程式碼

釋放資源。

ACTION_POINTER_DOWN

當有新的手指按下時分發的事件。

final int index = ev.getActionIndex();
mLastMotionY = (int) ev.getY(index);
mActivePointerId = ev.getPointerId(index);
複製程式碼

以新按下的手指的資訊重新計算。

ACTION_POINTER_UP

處理和 onInterceptTouch 一致。

SDK 工具類

系統已經提供 GestureDetector 來進行手勢的判定,我們只需要在相應的手勢回撥方法中進行我們的業務邏輯即可。還有更強大的 ViewDragHelper。

相關文章