Android事件分發機制——Touch事件


1. 簡介

    先來看一個表:

Touch事件相關方法

功能

Activity

ViewGroup

View

public boolean dispatchTouchEvent(MotionEvent ev)

事件分發

Yes

Yes

Yes

public boolean onInterceptTouchEvent(MotionEvent ev)

事件攔截

No

Yes

No

public boolean onTouchEvent(MotionEvent ev)

事件響應

Yes

Yes

Yes

     從表中可以看出,Activity、ViewGroup、View都關心Touch事件,其中ViewGroup的關心的事件有三個:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent。Activity和View關心的事件只有兩個:dispatchTouchEvent、onTouchEvent。也就是說只有ViewGroup可以對事件進行攔截。

    Android的UI結構是一個樹形結構,樹的最頂層(根節點)是Activity內包含的一個ViewGroup,下面有若干個ViewGroup節點,每個節點下面又有若干個ViewGroup節點或者View節點,以此類推。Touch事件的傳遞和處理與這種結構密切相關。一次完整的觸摸事件由一個ACTION_DOWN、零個或一個或多個ACTION_MOVE、一個ACTION_UP組成。

在分析源碼之前,先簡單了解下這三個函數。

(1) 事件分發: public boolean dispatchTouchEvent(MotionEvent ev)

    當Touch事件發生時,Activity的dispatchTouchEvent()方法會以隧道方式(從根節點依次往下傳遞直到最內層子節點,或在中間某一節點中由於某一條件停止傳遞)將事件傳遞給最外層View的dispatchTouchEvent()方法,並由該View的dispatchTouchEvent()方法對事件進行分發。

dispatchTouchEvent 的事件分發邏輯如下:

  • return true :事件會分發給當前View並由dispatchTouchEvent()方法進行消費,同時事件會停止向下傳遞。
  • return false :將事件返還給當前View的上一級的onTouchEvent()進行消費。(這個上一級可能是Activity,也可能是父View)
  • return super.dispatchTouchEvent(ev) :事件會自動的分發給當前View的onInterceptTouchEvent方法。

    注意,View響應dispatchTouchEvent()和onInterceptTouchEvent()的前提是可以向該View中添加子View,也就是說該View有子節點才談得上能分發和攔截。

(2) 事件攔截public boolean onInterceptTouchEvent(MotionEvent ev) 

    在外層View的dispatchTouchEvent()方法返回super.dispatchTouchEvent(ev)時,事件會自動的分發給當前View的onInterceptTouchEvent()方法。

onInterceptTouchEvent 的事件攔截邏輯如下:

  • return true :將對事件進行攔截,並將攔截到的事件交由當前View的onTouchEvent()進行處理。
  • return false :將對事件進行放行,當前View上的事件會被傳遞到子View 上,再由子View的dispatchTouchEvent()來繼續對這個事件進行分發。
  • return super.onInterceptTouchEvent(ev) :事件默認會被攔截,並將攔截到的事件交由當前View的onTouchEvent()進行處理。

(3) 事件響應public boolean onTouchEvent(MotionEvent ev)

    由(1)和(2)可以知道onTouchEvent()被調用的條件。

onTouchEvent 的事件響應邏輯如下:

  • return false :事件將會從當前View向上傳遞,並且都是由上層View的onTouchEvent()來接收,如果傳遞到上層的onTouchEvent()也返回false,那么這                       個事件就會"消失",而且接收不到下一次事件。
  • return true :接收並消費掉該事件。
  • return super.onTouchEvent(ev) :默認處理事件的邏輯和return false相同。

下面看下源碼。

1.1 Activity對Touch事件的處理

      當Touch事件發生生,最先被觸發的是Activity的dispatchTouchEvent()函數,再由這個函數觸發根節點的dispatchTouchEvent()。如果想讓Activity不響應觸摸事件,可以直接重寫這個函數。

dispatchTouchEvent()@Activity.java

// 處理觸摸屏事件。可以重寫這個函數來攔截所有的觸摸屏事件,不讓它們分發到window。為了應該被正常處理的觸摸屏幕事件,一定要確保調用這個實現。
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {  // 從ACTION_DOWN事件開始本次觸摸事件
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {  // 會調用window中的根ViewGroup進行事件分發
        return true;   // 如果子View消費了這個事件,返回true.
    }
    return onTouchEvent(ev);    // 沒有View處理本次事件,調用Activity的onTouchEvent。
}

 onTouchEvent()@Activity.java

// 當一個觸摸屏事件沒有被它下面的任意view處理的時候,調用這個函數。這個處理對於觸摸事件超出window范圍的時候很有用,這個時候沒有view接收到事件。
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}

1.2 View對Touch事件的處理

dispatchTouchEvent()@View.java

// 將屏幕觸摸事件向下傳遞給目標view.
public boolean dispatchTouchEvent(MotionEvent event) {
    ......
    if (onFilterTouchEventForSecurity(event)) {
        // ListenerInfo類保存了設給這個view的各種listener,如OnClickListener,OnTouchListener等。
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                       && li.mOnTouchListener.onTouch(this, event)) {
            // 當mOnTouchListener不為null,當前控件是enable的,且mOnTouchListener.onTouch返回true, 那么dispatchTouchEvent返回true.
            // 從這里可以看到onTouch執行要先於onTouchEvent及onClick
            return true;
        }
        // 上面的條件都不成立,才會執行onTouchEvent()
        if (onTouchEvent(event)) {       // 如果一個控件是可點擊的,那么onTouchEvent必定返回true, 因此dispatchTouchEvent也返回true.
            return true;
        }
    }
    ......
    return false;
} 

    從View的dispatchTouchEvent()的實現知道,如果給一個view設置了OnTouchListener,那么onTouch函數就在dispatchTouchEvent()函數中被回調,只有onTouch()返回false時,才會繼續執行onTouchEvent()。

onTouchEvent()@View.java

// 實現這個方法處理屏幕觸摸手勢事件
// 如果這個方法被用於檢測點擊動作,那么推薦通過調用performClick來實現。這樣做可以確保系統行為一致,包括服從點擊聲音喜好,調度OnClickListener調用,當輔助 // 功能啟用的時候處理ACTION_CLICK ACTION_CLICK。
public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);    // 設置按下UI狀態
        }
        // 一個可點擊的但是disabled的view仍然消費touch事件,只是不作出回應。
        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:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // 如果我們沒有擁有它,我們應該獲取焦點,而且應該在Touch模式下。
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }
    
                    if (prepressed) {
                        // 按鈕在我們實際顯示它按下之前被釋放。現在使它顯示按下狀態(在調度click之前),以確保用戶看到它。
                        setPressed(true);
                    }

                    if (!mHasPerformedLongPress) {
                        // 這是一個閥門,所以移除長按的檢查
                        removeLongPressCallback();
                        // 只有在按下狀態中時才執行click動作。
                        if (!focusTaken) {
                            // 使用Runnable和post執行performClick,而不是直接執行。這樣可以讓這個view的其它可見狀態在click操作開始前更新。
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();              // 注意,在MotionEvent.ACTION_UP時執行click。
                            }
                        }
                    }
                    if (mUnsetPressedState == null) {
                        mUnsetPressedState = new UnsetPressedState();
                    }
                    if (prepressed) {
                        postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration());
                    } else if (!post(mUnsetPressedState)) {
                        mUnsetPressedState.run();
                    }
                    removeTapCallback();
                }
                break;
    
            case MotionEvent.ACTION_DOWN:
                mHasPerformedLongPress = false;
                if (performButtonActionOnTouchDown(event)) {
                    break;
                }
    
                // 沿層次結構走,以確定我們是否在一個滾動的容器內。
                boolean isInScrollingContainer = isInScrollingContainer();
                // 對於在一個滾動容器內的views,延遲一會按下的反饋,以防這是一個滾動。
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // 如果不是在一個正在滾動的容器內,那么馬上顯示反饋。
                    setPressed(true);
                    checkForLongClick(0);
                }
                break;
    
            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                final int x = (int) event.getX();
                final int y = (int) event.getY();
                // 按鈕之外的移動要容忍。
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // 移除后面長按/閥門的檢查
                        removeLongPressCallback();
                        setPressed(false);
                    }
                }
            break;
        }
        return true; // 如果一個控件是可點擊的,那么點擊該控件時,onTouchEvent的返回值必定是true
    } 
    return false;
} 

performClick()@View.java

// 如果定義了OnClickListener,則調用。執行所有與click關聯的動作:報告可訪問性事件,播放按鍵聲音。
public boolean performClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        return true;
    }
    return false;
}

 setOnClickListener()@View.java

public void setOnClickListener(OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);  // 如果這個view不是clickable,將它設置為clickable。
    }
    getListenerInfo().mOnClickListener = l;
}

     如果給一個View設置了OnClickListener,那么onClick()事件是在onTouchEvent()函數中被調用的。

1.3 ViewGroup對Touch事件的處理

onInterceptTouchEvent()@ViewGroup.java

// 實現這個方法來攔截所有觸摸屏移動事件。這可以讓你在事件被分配給孩子的時候看到事件,並在任意時刻獲取當前手勢的所有權。
// 使用這個功能需要特別注意,因為它與View.onTouchEvent有着相當復雜的相互作用,並且使用它還需要以正確的方式實現這個方法。
// 事件將會以下面順序被接收:
// (1) ViewGroup會在這里接收到DOWN事件,這個DOWN事件會被這個ViewGroup的一個子View處理掉,或者給這個ViewGroup自己的onTouchEvent函數進行處理。那就意味着ViewGroup應該實現onTouchEvent()函數並且返回true, 這樣你就會繼續看到手勢的后續事件(而不是尋找父View來處理)。
// (2) 通過從ViewGroup自己的onTouchEvent()返回true, ViewGroup的onInterceptTouchEvent()中將不會再收到任何后續事件,即后續事件不用再經過onInterceptTouchEvent()了,所有觸摸的處理都像正常情況一樣發生在onTouchEvent里了。
// (3) 只要ViewGroup從這個函數返回false,接下來的每個事件(包括最后的UP)都會首先傳遞到這里,然后再傳遞給目標View的onTouchEvent()。
// (4) 如果ViewGroup從這個函數返回true,說明從子view攔截了事件,並將它們通過onTouchEvent()傳遞給這個ViewGroup。當前目標View將接收到相同事件,但是Action為ACTION_CANCEL,並且沒有進一步消息在這里傳遞。

public boolean onInterceptTouchEvent(MotionEvent ev) {  
    return false;  
}

dispatchTouchEvent()@ViewGroup.java

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }
    
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // 處理最初的down事件
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 開始一個新的觸摸手勢時,丟掉所有之前的狀態。framework可能由於app切換,ANR或者其它狀態變化而放棄前一個手勢的up或者cancel事件。
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 檢查攔截
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;       // 是否禁用事件攔截功能,默認為false.
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);// 根據onInterceptTouchEvent返回值決定是否攔截,返回false表示不攔截,返回true表示攔截。
                ev.setAction(action); // 重新保存action,以防它發生變化。
            } else {
                intercepted = false;
            }
        } else {
            // 沒有觸摸目標且action不是最初的DOWN事件,那么這個ViewGroup繼續攔截觸摸事件。
            intercepted = true;
        }

        // 檢查取消
        final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;

        // 如果需要,更新觸摸目標列表給向下的指針。
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) { // 如果不是CANCEL,而且不攔截事件,則往下走。intercepted的值在前面被賦值。intercepted為true,則事件不再往下傳。
            if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex();  // DOWN事件這個值通常為0
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
    
                // 為該指針id除早期的觸摸目標,以防它們已經不同步了。
                removePointersFromTouchTargets(idBitsToAssign);
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // 從前往后掃描,找一個可以接收事件的子view。
                    final View[] children = mChildren;
                    final boolean customOrder = isChildrenDrawingOrderEnabled();
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
                        final View child = children[childIndex];
                        if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
                            continue;
                        }
    
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // 子view已經在它的范圍內接收觸摸事件了。
                            // 給它新指針,除了它正在處理的指針。
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;      // 找到了接收事件的target,跳出循環。
                        }
    
                        resetCancelNextUpFlag(child);
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {   // dispatchTransformedTouchEvent函數中調用子view的dispatchTouchEvent。
                            // 子view在它的范圍內接收觸摸事件。
                            mLastTouchDownTime = ev.getDownTime();
                            mLastTouchDownIndex = childIndex;
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }
    
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // 沒有找到一個子view來接收事件,將指針指向最近添加的目標。
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }
        // 分發給觸摸目標
        if (mFirstTouchTarget == null) {
            // 沒有觸摸目標,那么把它當作一個普通的view。
            handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
        } else {
            // 分發給觸摸目標,如果我們已經分發給它那么就排除新的觸摸目標。如有必要取消觸摸目標。
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
    
        // Update list of touch targets for pointer up or cancel, if needed.
        // 如果需要,為指針UP或者CANCEL更新觸摸目標列表。
        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);
        }
    }
    
    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

 dispatchTransformedTouchEvent()@ViewGroup.java

// 將手勢事件轉換成特定子view的坐標空間,過濾無關的指針ID,如有必要覆蓋它的action。如果child為null,則取而代之的是假定手勢事件會發送給該ViewGroup。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
    final boolean handled;
 
    // 取消手勢動作是一種特殊情況。不需要執行任何轉換或者過濾操作。重點關注的是action,而不是內容。
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);     // 調用child的dispatchTouchEvent。
        }
        event.setAction(oldAction);
        return handled;
    }

    // 結算要傳遞的指針數量。
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // 如果因為某種原因,我們在一個不符的狀態中結束了,在這個狀態中看起來我們可能會產生一個不帶指針的手勢事件,那么就放棄這個事件。
    if (newPointerIdBits == 0) {
        return false;
    }

    // 如果指針的數量是相等的,那么我們就不需要執行任何代價高的不可逆轉的轉換,那么只要我們小心恢復我們做的任何修改,我們就可以在這次分發中重用手勢事件。
    // 否則,我們就需要復制一個手勢事件。
    final MotionEvent transformedEvent;
    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);
    }

    // 執行任何需要的轉換和分發。
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
        handled = child.dispatchTouchEvent(transformedEvent); //若一個控件是可點擊的,則點擊該控件時,dispatchTouchEvent的返回值必定是true
    }
    // 完成
    transformedEvent.recycle();
    return handled;
}

 




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM