Android應用開發三部曲 --- Touch事件分發

1、前言

Android應用開發中,經常會遇到touch事件分發的問題,甚至還會遇到滑動沖突問題,如果解決滑動沖突、理解touch事件分發原理等,很有必要。

應用開發三部曲系列文章,已經完成兩篇了

結合本文闡述的touch事件分發,一些常見的問題就能輕松解決了。

2、Touch事件分發原理

Touch事件分發涉及到三個重要的方法。

  • dispatchTouchEvent,touch事件分發的入口,決定把touch事件交給哪個view處理
  • onInterceptTouchEvent,負責touch事件攔截,如果返回為true,則調用onTouchEvent,如果返回為false,則將touch事件交給子view處理,調用子view的dispatchTouchEvent方法
  • onTouchEvent,處理touch事件,實現如滑屏等等

具體流程如下圖:

Paste_Image.png

如果子view的onInterceptTouchEvent也返回為false,此時會調用父view的onTouchEvent方法。

View中沒有onInterceptTouchEvent方法,只有ViewGroup才有。

在三級嵌套的頁面中,touch事件分發log為:

05-19 10:45:43.334: I/okunu(28182): root dispatchTouchEvent action = 0
05-19 10:45:43.334: I/okunu(28182): root onInterceptTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): viewgroup dispatchTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): viewgroup onInterceptTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): view dispatchTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): view onTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): viewgroup onTouchEvent action = 0
05-19 10:45:43.335: I/okunu(28182): root onTouchEvent action = 0

結合log與上圖,touch事件的分發流程就很清楚了。

3、Touch事件分發源碼走讀

查看ViewGroup.java的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onFilterTouchEventForSecurity(ev)) {
        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            //是否允許攔截,如果不允許攔截,則不調用onInterceptTouchEvent方法
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //調用onInterceptTouchEvent,是否攔截此touch事件
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }
        //不攔截,將touch事件交給子view處理
        if (!canceled && !intercepted) {
            if (newTouchTarget == null && childrenCount != 0) {
                //遍歷子view,找出合適的子view處理此touch事件
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final View child = (preorderedList == null)
                            ? children[childIndex] : preorderedList.get(childIndex);
                            //將touch事件交給子view處理
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {}
                }
            }
        }
        //如果攔截此touch事件
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            while (target != null) {
                final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;
                //如果攔截此touch事件或者子view不處理此事件時,事件由父view處理
                if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
                }
            }
        }
    }
}

為了使代碼更簡明,更容易看懂,本人刪減了一些東西,只關注脈絡性內容

查看上述代碼,如果父view攔截touch事件,則調用dispatchTransformedTouchEvent方法,如果父view不攔截touch事件,也要調用此方法。dispatchTransformedTouchEvent方法負責將touch事件最終分發

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    。。。。。。
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    。。。。。。
}

dispatchTransformedTouchEvent方法邏輯比較簡單,和源碼相比上述代碼省略了很多,但其實意思一樣,根據各種條件運算,最后如果傳過來的child為空,則父view處理,如果不為空,則child處理。

可能有細心的同學會問,onTouchEvent方法在哪里調用的呢?查看View.java的dispatchTouchEvent方法,onTouchEvent的此被調用。

4、touch事件處理源碼走讀

touch事件是一個很廣義的范疇,點擊事件、長按事件也會產生touch事件,那View是如何區分touch事件、點擊事件和長按事件的呢?

public boolean onTouchEvent(MotionEvent event) {
    switch (action) {
    case MotionEvent.ACTION_UP:
        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
            //如果mHasPerformedLongPress為false,沒有按住一定時間,則將長按消息remove
            //remove長按消息,就不會再觸發長按事件了
            removeLongPressCallback();
            //檢測一定條件,如果條件滿足,則觸發單擊事件
            if (!focusTaken) {
                if (!post(mPerformClick)) {
                    performClick();
                }
            }
        }
        break;
    case MotionEvent.ACTION_DOWN:
        //傳來down事件時,將mHasPerformedLongPress設置為false
        //mHasPerformedLongPress,表征長按事件是否發生
        mHasPerformedLongPress = false;
        setPressed(true, x, y);
        //發送處理長按事件的消息,該消息將在一定時間后響應
        checkForLongClick(0);
        break;
    }
}

如果LongClick事件執行了,那么mHasPerformedLongPress值為true。

可以查看下systemui中的虛擬按鍵的onTouchEvent方法,和上述代碼非常相似,這樣就能做到點擊、長按和滑動的區分了。

5、滑動沖突

常見的滑動沖突如網易新聞,可以橫向滑動也可以縱向滑動,需要明確區分兩種滑動事件,并且將touch事件交由正確的view處理。

目前有兩種常見的滑動沖突處理方法:

  • 外部攔截法,由父View根據條件進行攔截
  • 內部攔截法,由子View設置父View的標志位,當子View不需要處理touch事件時將事件交由父view處理

5.1 外部攔截法

本節使用示例說明,示例中父View有三個子View,listView,橫向滑動時切換listview,縱向滑動時,listview響應用戶。

外部攔截法的思想,由父view根據情況攔截touch事件,本例中,則是在move事件下發時,如果橫向滑動距離超過縱向滑動距離,那么則由父view攔截,否則由子view攔截。

父View的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:
            mInterceptX = x;
            mInterceptY = y;
            intercepted = false;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                intercepted = true;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            int deltax = Math.abs(mInterceptX - x);
            int deltay = Math.abs(mInterceptY - y);
            if (deltax > deltay) {
                intercepted = true;
            }else {
                intercepted = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
    }
    Util.log("intercepted = " + intercepted + "  action = " + ev.getAction());
    return intercepted;
}

本例中的橫向滑動事件處理具有代表性意義,滑動一屏,這種場景非常多見,可參考本例中代碼

public boolean onTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    mTracker.addMovement(event);
    ViewConfiguration configuration = ViewConfiguration.get(getContext());
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mLastX = x;
            mLastY = y;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            break;
        case MotionEvent.ACTION_MOVE:
            if (mFirstTouch) {
                mLastX = x;
                mLastY = y;
                mFirstTouch = false;
            }
            int deltax = x - mLastX;
            scrollBy(-deltax, 0);
            break;
        case MotionEvent.ACTION_UP:
            int scrollx = getScrollX();
            mTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());
            float xV = mTracker.getXVelocity();
            if (Math.abs(xV) > configuration.getScaledMinimumFlingVelocity()) {
                mChildIndex = xV < 0 ? mChildIndex + 1 : mChildIndex - 1;
            }else {
                mChildIndex = (scrollx + mContentWidth/2)/mContentWidth;
            }
            mChildIndex = Math.min(getChildCount() - 1, Math.max(mChildIndex, 0));
            smoothScrolly(mContentWidth * mChildIndex - scrollx);
            mTracker.clear();
            mFirstTouch = true;
            break;
    }
    mLastX = x;
    mLastY = y;
    return true;
}

private void smoothScrolly(int dx){
    mScroller.startScroll(getScrollX(), getScrollY(), dx, 0, 500);
    invalidate();
}

@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        invalidate();
    }
}

5.2、內部攔截法

內部攔截法,主要基于設置標志位的思想。requestDisallowInterceptTouchEvent方法,將給view設置標志位,view無法攔截touch事件了。

如果父view在down事件時,被調用此方法,父view無法再攔截touch事件,所有touch事件均會直接讓子view處理。子view如果發現對某touch事件不關心,再重新調用上述方法,關閉此標志位,父view將重新處理touch事件。

父view的onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    Util.log("group2 onInterceptTouchEvent action = " + ev.getAction());
    //父view在down事件時,不攔截,子view處理down事件時將父view設置標志位,禁止父view攔截touch事件
    //父view其它事件均返回為true,這是為了時刻準備著,如果子view不需要處理此事件,則父view獲得機會,將攔截此事件
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
            return true;
        }
        return false;
    }else {
        return true;
    }
}

子view的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent ev) {
    Util.log("ListViewEx dispatchTouchEvent action = " + ev.getAction());
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
        mHorizontalEx2.requestDisallowInterceptTouchEvent(true);
        break;
    case MotionEvent.ACTION_MOVE:
        int deltax = x - mLastX;
        int deltay = y - mLastY;
        //當橫向移動距離大于縱向移動距離時,給父view解禁,讓父view處理此touch事件
        //綜合來說,就是當子view對這種touch事件不關心時,就扔給父view處理
        if (Math.abs(deltax) > Math.abs(deltay)) {
            mHorizontalEx2.requestDisallowInterceptTouchEvent(false);
        }
        break;
    default:
        break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(ev);
}

注,所有代碼均已上傳至本人的github

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容