溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Android事件分發(fā)機(jī)制是什么

發(fā)布時間:2023-04-18 11:14:32 來源:億速云 閱讀:107 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容主要講解“Android事件分發(fā)機(jī)制是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“Android事件分發(fā)機(jī)制是什么”吧!

事件分發(fā)原因

Android 中頁面上的 View 是以樹型結(jié)構(gòu)顯示的,View 會重疊在一起,當(dāng)我們點(diǎn)擊的地方有多個 View 可以響應(yīng)的時候,這個點(diǎn)擊事件應(yīng)該給誰,為了解決這個問題就需要一個事件分發(fā)機(jī)制

事件分發(fā)對象

Touch 事件,即將每一個 Touch 事件(MotionEvent)傳遞給 View,至于最終這個事件有沒有處理看接收事件者的邏輯而定

當(dāng)用戶觸摸屏幕的時候,就會產(chǎn)生 Touch 事件(Touch 事件被封裝成 MotionEvent 對象),其主要分為如下幾種

  • MotionEvent.ACTION_DOWN:使用手指點(diǎn)擊屏幕這一瞬間,產(chǎn)生該事件,是所有事件的開始

  • MotionEvent.ACTION_MOVE:使用手指在屏幕滑動的時候產(chǎn)生該事件

  • MotionEvent.ACTION_CANCLE:非人為原因結(jié)束當(dāng)前事件

  • MotionEvent.ACTION_UP:手指離開屏幕一瞬間產(chǎn)生該事件

一次完整的 Touch 事件,是從用戶手指觸摸屏幕(伴隨著一次 ACTIONDOWN 事件)到用戶手指離開屏幕(伴隨著一次 ACTIONUP 事件)這一過程,整個過程如下

ACTIONDOWN(一次) --> ACTIONMOVE(N 次) --> ACTION_UP(一次)

事件分發(fā)方法

  • dispatchTouchEvent(MotionEvent ev) :從方法名也能看出它的作用是對事件進(jìn)行分發(fā);當(dāng)一個事件由底層驅(qū)動檢測到了之后,會進(jìn)行上報,最終會交由 Activity 的該方法處理,來決定是自己消費(fèi)還是繼續(xù)傳遞下去

  • onInterceptTouchEvent(MotionEvent ev) :當(dāng)一個事件分發(fā)到 ViewGroup 后,它可以決定是否對該事件進(jìn)行攔截,該方法只有 ViewGroup 擁有

  • onTouchEvent(MotionEvent event) :這是事件分發(fā)流程的最后一個方法了,即是否消費(fèi)該次事件

事件分發(fā)參與者

  • Activity:包含 ViewGroup 和 View

  • ViewGroup:包含 ViewGroup 和 View

  • View:并不包含其它 View,只有自己

事件分發(fā)流向一般是 Activity --> ViewGroup --> … --> View

注意:

  • 子 View 可以通過 requestDisallowInterceptTouchEvent 方法干預(yù)父 View 的事件分發(fā)過程(ACTION_DOWN 事件除外),而這就是我們處理滑動沖突常用的關(guān)鍵方法

  • 如果 View 設(shè)置了 onTouchListener,在重寫的 onTouch 方法中返回 true,那么它的 onTouchEvent 方法不會被調(diào)用,因?yàn)樵?View 的 dispatchTouchEvent 中 onTouch 優(yōu)先于 onTouchEvent 執(zhí)行;onClick 方法也不會被調(diào)用,因?yàn)?onClick 是在 onTouchEvent 中回調(diào)的

事件分發(fā)流程

  • 當(dāng)手指觸摸屏幕后,底層 Input 驅(qū)動從/dev/input/路徑下讀寫以 event[NUMBER]為名的硬件輸入設(shè)備節(jié)點(diǎn)獲取事件(可以通過 adb shell getevent 查看你的設(shè)備下的節(jié)點(diǎn),Android 也是從這些節(jié)點(diǎn)獲取這些原始數(shù)據(jù)再封裝后提供給開發(fā)者使用;如果做游戲開發(fā)可能就直接獲取這些原始數(shù)據(jù)自己處理了),經(jīng)過一系列調(diào)用后傳遞到了 DecorView 的 dispatchTouchEvent 方法

  • 在 DecorView 中,會通過 Window 的內(nèi)部接口 Callback,將事件繼續(xù)傳遞,因?yàn)?Activity 實(shí)現(xiàn)了該接口,故事件分發(fā)到 Activity;Activity 獲取到事件后,在 dispatchTouchEvent 方法中先將事件分發(fā)到該 Activity 所在的 window,實(shí)際類型是 PhoneWindow,這個 window 又將事件交給它的頂級 view 即 DecorView 處理

  • DecorView 是 FrameLayout 的子類,即 ViewGroup 的子類,自己沒有處理,只是繼續(xù)將事件交由 ViewGroup 處理;就這樣一個事件就從 Activity 轉(zhuǎn)到了 ViewGroup

  • ViewGroup 在 dispatchTouchEvent 方法進(jìn)行分發(fā),如果自己的 onInterceptTouchEvent 方法攔截此次事件,就把事件交給自身的 onTouchEvent 方法處理;反之遍歷自己的子 View,繼續(xù)將事件分發(fā)下去,只要有一個子 View 消費(fèi)了這個事件,那就停止遍歷

  • 事件會傳遞到子 View 的 dispatchTouchEvent 方法,如果給子 View 注冊了 OnTouchListener,且返回 true,那事件分發(fā)就到此結(jié)束;反之就會繼續(xù)將事件傳遞到子 View 的 onTouchEvent 方法

  • 子 View 會在 ACTION_UP 事件中回調(diào) View 的 onClick 監(jiān)聽,如果子 View 沒有消費(fèi)此次事件,就會按照分發(fā)流程反過來傳遞回去到 Activity;如果到了 Activity 還沒人消費(fèi)(包括 Activity 自己),那就會銷毀這個事件

事件分發(fā)源碼

以下源碼基于 API24

對應(yīng)上面的流程,當(dāng)有 Touch 事件后,步驟如下

DecorView.dispatchTouchEvent

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

此處的 cb 指的是 window 內(nèi)部的 Callback 接口,Activity 實(shí)現(xiàn)了這個接口,接下來進(jìn)入 Activity

Activity.dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

這個方法就是 Activity 用來處理觸摸屏事件,我們可以重寫這個方法,并返回 true/false,這樣在事件分發(fā)到 window 前就能進(jìn)行攔截,Activity 內(nèi)的 ViewGroup 或者 View 將收不到事件

一個觸摸屏事件都是以 ACTION_DOWN 開始,那就肯定會進(jìn)入 onUserInteraction()方法

public void onUserInteraction() {
}

這是一個空方法,它的調(diào)用時機(jī)如下: 當(dāng)一個按鍵事件,觸摸屏事件或者 trackball 事件分發(fā)到 Activity 的時候,它就會被調(diào)用;如果你希望在 Activity 正在運(yùn)行的時候了解用戶和設(shè)備用某種方式交互,可以重寫這個方法;不過需要注意的是這個方法只響應(yīng) touch-down 這種觸摸手勢,不會響應(yīng)接下來的 touch-move 和 touch-up

與這個方法相對應(yīng)的一個方法就是onUserLeaveHint,它同樣也是一個空方法,它的調(diào)用時機(jī)如下:

當(dāng)在用戶操作的情況下 Activity 進(jìn)入后臺,這個方法會作為 Activity 生命周期的一部分被調(diào)用;比如,用戶按下 home 鍵,當(dāng)前 Activity 就會進(jìn)入后臺,它就會被調(diào)用,并且是在 onPause 之前調(diào)用;但是比如有電話打進(jìn)來了導(dǎo)致 Activity 被動進(jìn)入后臺,這個方法就不會被調(diào)用

接下來進(jìn)入第二個 if 語句

getWindow().superDispatchTouchEvent

通過 getWindow()獲取到的是一個 Window 對象,但是它是在 Activity 的 attach 方法中進(jìn)行實(shí)例化,實(shí)際類型是 PhoneWindow,也是在這里實(shí)現(xiàn)了 Callback 接口

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
            ......
            mWindow = new PhoneWindow(this, window);
            mWindow.setCallback(this);
            ......
}

這里就轉(zhuǎn)到 PhoneWindow,如下

PhoneWindow.superDispatchTouchEvent

//這是窗口的頂層視圖
private DecorView mDecor
@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView .superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

DecorView 是 FrameLayout 的子類,F(xiàn)rameLayout 又是 ViewGroup 的子類,這里就會走到 ViewGroup

ViewGroup.dispatchTouchEvent

        @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //用于調(diào)試目的的一致性驗(yàn)證程序
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }
        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }
        //這個變量用于標(biāo)記事件是否被消費(fèi)
        boolean handled = false;
        //根據(jù)應(yīng)用安全策略過濾觸摸事件
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            // 處理 initial down 發(fā)生后的初始化操作
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 新的 ACTION_DOWN 事件來了,需要取消并清除之前的 touch Targets
                //清空掉 mFirstTouchTarget
                cancelAndClearTouchTargets(ev);
                //重置觸摸狀態(tài)
                resetTouchState();
            }
            //標(biāo)記是否攔截事件
            final boolean intercepted;
            // 當(dāng) ACTION_DOWN 來了或者已經(jīng)發(fā)生過 ACTION_DOWN,并且將 mFirstTouchTarget 賦值 就檢測 ViewGroup 是否需要攔截事件.
            //只有發(fā)生過 ACTION_DOWN 事件,mFirstTouchTarget != null
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //子 View 可以通過調(diào)用父 View 的 requestDisallowInterceptTouchEvent 方法設(shè)置 mGroupFlags 值
                //以此告訴父 View 是否攔截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //如果子 view 沒有告訴父 View 別攔截事件,那父 View 就判斷自己是否需要攔截事件
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // 重新恢復(fù) action  以防被改變了
                } else {
                        //這里表明子 View 告訴父 View 不要攔截事件
                    intercepted = false;
                }
            } else {
                //當(dāng) mFirstTouchTarget=null(沒有子 View 被分配處理),且不是 initial down 事件時(事件已經(jīng)初始化過了),ViewGroup 繼續(xù)攔截觸摸
                //繼續(xù)設(shè)置為 true
                intercepted = true;
            }
            // 如果當(dāng)前事件是 ACTION_CANCEL,或者 view.mPrivateFlags 被設(shè)置了 PFLAG_CANCEL_NEXT_UP_EVENT
            //那么當(dāng)前事件就取消了
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
            //split 表示當(dāng)前的 ViewGroup 是不是支持分割 MotionEvent 到不同的 View 當(dāng)中
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            //新的 TouchTarget
            TouchTarget newTouchTarget = null;
            //是否把事件分發(fā)給了新的 TouchTarget
            boolean alreadyDispatchedToNewTouchTarget = false;
            //不取消事件,同時不攔截事件才進(jìn)入該區(qū)域
            if (!canceled && !intercepted) {
                //把事件分發(fā)給所有的子視圖,尋找可以獲取焦點(diǎn)的視圖
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;
                //如果是這三種事件就得遍歷子 View
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;
                    // 對于這個 PointerId 清空更早的 touch targets 
                    removePointersFromTouchTargets(idBitsToAssign);
                    final int childrenCount = mChildrenCount;
                    //如果當(dāng)前 ViewGroup 有子 View 且 newTouchTarget=null
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // 在視圖里從前到后掃描一遍獲取可以接收事件的子 View
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //遍歷所有子 View,找到一個來接收事件
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            //如果當(dāng)前子 View 沒有獲取焦點(diǎn),則跳過這個子 View
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
                            //如果當(dāng)前子 View 不可見且沒有播放動畫 或者 不在觸摸點(diǎn)范圍內(nèi),跳過這個子 View
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            //如果在觸摸目標(biāo)列表找到了與該子 View 對應(yīng)的 TouchTarget,說明這個 view 正在接收事件,不需要再遍歷,直接退出
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                            resetCancelNextUpFlag(child);
                            //子 view 處于觸摸位置,就將事件分發(fā)給子 View,如果該子 View 返回 true,說明消費(fèi)了這個事件,就跳出遍歷
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // 獲取 TouchDown 的時間點(diǎn)
                                mLastTouchDownTime = ev.getDownTime();
                                // 獲取 TouchDown 的 Index
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                //獲取 TouchDown 的 x,y 坐標(biāo)
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //添加到觸摸目標(biāo)列表 同時給 mFirstTouchTarget 賦值
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // 到這里說明沒有子 View 接收事件,那就把最近一次的觸摸目標(biāo)賦值給 newTouchTarget
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }
            // mFirstTouchTarget 賦值是在通過 addTouchTarget 方法獲取的;
            // 只有處理 ACTION_DOWN 事件,才會進(jìn)入 addTouchTarget 方法。
            // 這也正是當(dāng) View 沒有消費(fèi) ACTION_DOWN 事件,則不會接收其他 MOVE,UP 等事件的原因
            if (mFirstTouchTarget == null) {
                // 那就只能 ViewGroup 自己處理事件了
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // 到這里就說明有子 View 接收了 ACTION_DOWN 事件,那后續(xù)的 move up 等事件就繼續(xù)分發(fā)給這個觸摸目標(biāo)
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        //如果 view.mPrivateFlags 被設(shè)置了 PFLAG_CANCEL_NEXT_UP_EVENT 或者事件被 ViewGroup 攔截了
                        //那子 View 需要取消事件
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //繼續(xù)分發(fā)事件給子 View
                        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;
                }
            }
            //當(dāng)發(fā)生抬起或取消事件,更新觸摸目標(biāo)列表
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                //如果是多點(diǎn)觸摸下的手指抬起事件,就要根據(jù) idBit 從 TouchTarget 中移除掉對應(yīng)的 Pointer(觸摸點(diǎn))
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }
        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

這個方法內(nèi)容有點(diǎn)多,需要拆分開分析

第一步:事件初始化

第一個進(jìn)來的是 ACTION_DOWN 事件,那需要做一些初始化:

第一件事就是清空所有的 TouchTarget,并將 mFirstTouchTarget 值為 null;mFirstTouchTarget 的類型也是 TouchTarget,是 ViewGroup 的一個內(nèi)部類,描述一個觸摸的視圖和它捕獲的指針的 id;mFirstTouchTarget 可以理解為如果事件由子 View 去處理時 mFirstTouchTarget 會被賦值并指向子 View

第二件事是重置狀態(tài)值,通過 FLAGDISALLOWINTERCEPT 重置 mGroupFlags 值

ViewGroup.cancelAndClearTouchTargets

/**
      * 取消和清空所有的 touch targets.
      */
    private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }
            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();
            if (syntheticEvent) {
                event.recycle();
            }
        }
    }
    /**
     * 清空所有的 touch targets.
     */
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
    /**
     * 重置所有觸摸狀態(tài)以準(zhǔn)備新周期.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

第二步:攔截判斷

接下來就需要判斷是否需要攔截事件:

首先看條件是

            //標(biāo)記是否攔截事件
            final boolean intercepted;
            // 當(dāng) ACTION_DOWN 來了或者已經(jīng)發(fā)生過 ACTION_DOWN,并且將 mFirstTouchTarget 賦值 就檢測 ViewGroup 是否需要攔截事件.
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //子 View 可以通過調(diào)用父 View 的 requestDisallowInterceptTouchEvent 方法設(shè)置 mGroupFlags 值
                //以此告訴父 View 是否攔截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //如果子 view 沒有告訴父 View 別攔截事件,那父 View 就判斷自己是否需要攔截事件
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // 重新恢復(fù) action  以防被改變了
                } else {
                    //這里表明子 View 告訴父 View 不要攔截事件
                    intercepted = false;
                }
            } else {
                //當(dāng) mFirstTouchTarget=null(沒有子 View 被分配處理),且不是 initial down 事件時(事件已經(jīng)初始化過了),ViewGroup 繼續(xù)攔截觸摸
                //繼續(xù)設(shè)置為 true
                intercepted = true;
            }
  • 當(dāng)事件是 ACTIONDOWN 或者 mFirstTouchTarget != null 才會去判斷要不要攔截,由第一步可知,當(dāng)事件是 ACTIONDOWN 的時候,mFirstTouchTarget 肯定為 null,所以這里只有兩種情況會進(jìn)入:ACTIONDOWN 事件來了需要判斷攔截;ACTIONDOWN 事件中如果有子 View 接收了事件(這樣 mFirstTouchTarget 就賦值了),那接下來的事件也需要判斷是否攔截事件

  • 上面條件的反向邏輯就是事件是 ACTIONDOWN 事件以后的事件(比如 move 或者 up)且 mFirstTouchTarget 為 null,說明在 ACTIONDOWN 事件中就判斷了需要攔截事件或者沒有子 View 處理事件,那接下來的事件就沒必要分發(fā)了,繼續(xù)攔截

第一個 if 語句里面是攔截判斷邏輯是

  • 先通過與運(yùn)算獲得 mGroupFlags 的值,子 view 可以通過調(diào)用父 view 的requestDisallowInterceptTouchEvent 方法設(shè)置 mGroupFlags 的值,告訴父 view 不要攔截事件

  • 如果 disallowIntercept 為 true,說明子 view 要求父 view 不要攔截,就將 intercepted 設(shè)置 false

  • 如果 disallowIntercept 為 false,表明子 view 沒有提出不要攔截請求,那就調(diào)用 onInterceptTouchEvent 看看自己是不是需要攔截事件

ViewGroup.requestDisallowInterceptTouchEvent

@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // 如果已經(jīng)設(shè)置過了,就返回
            return;
        }
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        // 依次告訴父 view
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

ViewGroup.onInterceptTouchEvent

 /**
     * ViewGroup 可在這個方法里攔截所有觸摸事件,默認(rèn)是不攔截事件,開發(fā)者可以重寫這個方法決定是否要攔截
     * 如下四個條件都成立,返回 true,攔截事件
     * 第一個:觸摸事件是否來自鼠標(biāo)指針設(shè)備
     * 第二個:觸摸事件是否是 ACTION_DOWN
     * 第三個:檢查是否按下了鼠標(biāo)或手寫筆按鈕(或按鈕組合),也就是說用戶必須實(shí)際按下
     * 第四個:觸摸點(diǎn)是否在滾動條上
     */
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

第三步:ACTION_DOWN 事件分發(fā)

接下來就需要遍歷子 View,然后將 ACTION_DOWN 事件分發(fā)給能接收事件的子 View

  • 如果當(dāng)前子 View 沒有獲取焦點(diǎn),則跳過這個子 View

  • 如果當(dāng)前子 View 不可見且沒有播放動畫 或者 不在觸摸點(diǎn)范圍內(nèi),跳過這個子 View

  • 如果在觸摸目標(biāo)列表找到了與該子 View 對應(yīng)的 TouchTarget,說明這個 view 正在接收事件,不需要再遍歷,直接退出

  • 如果子 view 處于觸摸位置,就調(diào)用 dispatchTransformedTouchEvent 方法將事件分發(fā)給子 View,如果該方法返回 true,說明子 View 消費(fèi)了這個事件,那就不需要再尋找子 view 接收事件了,跳出遍歷

ViewGroup.dispatchTransformedTouchEvent

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    // 發(fā)生取消操作時,不再執(zhí)行后續(xù)的任何操作
    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);
        }
        event.setAction(oldAction);
        return handled;
    }
    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    //由于某些原因,發(fā)生不一致的操作,那么將拋棄該事件
    if (newPointerIdBits == 0) {
        return false;
    }
    //分發(fā)的主要區(qū)域
    final MotionEvent transformedEvent;
    //判斷預(yù)期的 pointer id 與事件的 pointer id 是否相等
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                //不存在子視圖時,ViewGroup 調(diào)用 View.dispatchTouchEvent 分發(fā)事件,再調(diào)用 ViewGroup.onTouchEvent 來處理事件
                handled = super.dispatchTouchEvent(event); 
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);
                //將觸摸事件分發(fā)給子 ViewGroup 或 View;
                handled = child.dispatchTouchEvent(event);
                event.offsetLocation(-offsetX, -offsetY); //調(diào)整該事件的位置
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event); //拷貝該事件,來創(chuàng)建一個新的 MotionEvent
    } else {
        //分離事件,獲取包含 newPointerIdBits 的 MotionEvent
        transformedEvent = event.split(newPointerIdBits);
    }
    if (child == null) {
        //不存在子視圖時,ViewGroup 調(diào)用 View.dispatchTouchEvent 分發(fā)事件,再調(diào)用 ViewGroup.onTouchEvent 來處理事件
        handled = super.dispatchTouchEvent(transformedEvent); 
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            //將該視圖的矩陣進(jìn)行轉(zhuǎn)換
            transformedEvent.transform(child.getInverseMatrix());
        }
        //將觸摸事件分發(fā)給子 ViewGroup 或 View;
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    //回收 transformedEvent
    transformedEvent.recycle();
    return handled;
}

該方法是 ViewGroup 真正處理事件的地方,分發(fā)子 View 來消費(fèi)事件,過濾掉不相干的 pointer ids。當(dāng)子視圖為 null 時,MotionEvent 將會發(fā)送給該 ViewGroup;不為 null,最終調(diào)用 View.dispatchTouchEvent 方法來分發(fā)事件。

這個方法調(diào)用完畢,回到 ViewGroup.dispatchTouchEvent 會調(diào)用 addTouchTarget 方法

ViewGroup.addTouchTarget

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

可以看到在這里給 mFirstTouchTarget 賦值了

當(dāng)子控件消費(fèi)了事件,mFirstTouchTarget 不為空;當(dāng)子控件沒有消費(fèi)事件或者被攔截,mFirstTouchTarget 為空

第四步:ACTIONMOVE ACTIONUP 事件分發(fā)

在第三步過后,ViewGroup 可能會找到有子 View 消費(fèi)事件

  • 如果事件被攔截,mFirstTouchTarget==null,那接下來的事件最終調(diào)用 View.dispatchTouchEvent 方法來分發(fā)事件

  • 如果 ViewGroup 沒有子 View,mFirstTouchTarget==null,那接下來同上

  • 如果有子 View,但是子 View 沒消費(fèi)事件,mFirstTouchTarget==null,那接下來同上

  • 如果有子 View,且子 View 消費(fèi)了 ACTION_DOWN 事件,但是在 dispatchTouchEvent 返回了 false(即 dispatchTransformedTouchEvent 返回 false,那 addTouchTarget 就不會被調(diào)用),mFirstTouchTarget==null,那接下來的處理也同上

  • 接下來就是 mFirstTouchTarget 不為 null 了,那就需要將后續(xù)事件分發(fā)給消費(fèi) ACTION_DOWN 事件的 View 了

通過對 ViewGroup.dispatchTouchEvent 方法的分析,我們知道不管有沒有子 View 消費(fèi)事件,最終事件都會進(jìn)入 View.dispatchTouchEvent 方法,那我們繼續(xù)一探究竟

View.dispatchTouchEvent

/**
     * 將觸摸事件向下傳遞到目標(biāo)視圖,或者這個 View 是目標(biāo)視圖。
     *
     * @return 返回 true 表示消費(fèi)了事件,反之返回 false 
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
                ......
        boolean result = false;
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //在 Down 事件之前,如果存在滾動操作則停止。不存在則不進(jìn)行操作
            stopNestedScroll();
        }
        //過濾觸摸事件以應(yīng)用安全策略
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            ListenerInfo li = mListenerInfo;
            // 如果給 View 設(shè)置了 OnTouchListener
            //且該 view 沒有禁用的
            //且 OnTouchListener.onTouch 返回 true
            //那說明該 View 消費(fèi)了該事件,返回 true
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
           //如果 OnTouchListener.onTouch 沒有消費(fèi)事件且 View 的 onTouchEvent 方法返回 true,那返回 true
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        // 如果這是手勢的結(jié)束,則在嵌套滾動后清理;
        //如果我們嘗試了 ACTION_DOWN 但是我們不想要其余的手勢,也要取消它。
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }
        return result;
    }

這里有兩點(diǎn)比較重要

  • 如果開發(fā)者設(shè)置 OnTouchListener 監(jiān)聽,且在 onTouch 方法返回 true,說明 view 消費(fèi)了事件

  • 如果沒有設(shè)置監(jiān)聽,那就調(diào)用 View 的 onTouchEvent 方法去處理事件

可以看出 OnTouchListener.onTouch 是優(yōu)先于 onTouchEvent 執(zhí)行的,只要前者返回 true,那后者就不會執(zhí)行了,事件到此為止結(jié)束

接下來看看 onTouchEvent 的邏輯

View.onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //如果這個 view 是禁用的,可以通過 setEnabled()設(shè)置是否禁用
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // 即使設(shè)置了禁用,但是只要這個 view 滿足 CLICKABLE ,LONG_CLICKABLE ,CONTEXT_CLICKABLE 其中一種
            //任然算消費(fèi)該事件,只是沒有響應(yīng)而已
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
         //當(dāng) View 狀態(tài)為 ENABLED
        //且這個 view 滿足 CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE 其中一種,就消費(fèi)這個事件
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // 獲取焦點(diǎn)處于可觸摸模式
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            //這是 Tap 操作,移除長按回調(diào)方法
                            removeLongPressCallback();
                            // 如果處于按下狀態(tài)盡執(zhí)行點(diǎn)擊操作
                            if (!focusTaken) {
                                // 使用 Runnable 并發(fā)布而不是直接調(diào)用 performClick 
                                //這樣可以在單擊操作開始之前更新視圖的其他可視狀態(tài)
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                //調(diào)用 View.OnClickListener
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }
                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;
                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }
                    // 確定是否處于可滾動的視圖內(nèi)
                    boolean isInScrollingContainer = isInScrollingContainer();
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        //當(dāng)處于可滾動視圖內(nèi),則延遲 TAP_TIMEOUT,再反饋按壓狀態(tài),用來判斷用戶是否想要滾動。默認(rèn)延時為 100ms
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        //當(dāng)不再滾動視圖內(nèi),則立刻反饋按壓狀態(tài)
                        setPressed(true, x, y);
                        //檢測是否是長按,如果長按,回調(diào) OnLongClickListener.onLongClick
                        checkForLongClick(0, x, y);
                    }
                    break;
                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);
                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();
                            setPressed(false);
                        }
                    }
                    break;
            }
            return true;
        }
        return false;
    }

這里有幾點(diǎn)需要注意

  • 只要是這個 view 滿足 CLICKABLE ,LONGCLICKABLE ,CONTEXTCLICKABLE 其中一種,不管通過 setEnabled()設(shè)置禁用還是可用,都會返回 true,認(rèn)為消費(fèi)事件

  • View 的 longClickable 默認(rèn)為 false,clickable 需要區(qū)分情況,如 Button 的 clickable 默認(rèn)為 true,而 TextView 的 clickable 默認(rèn)為 false;但是 View 的 setOnClickListener 會默認(rèn)將 View 的 clickable 設(shè)置成 true,View 的 setOnLongClickListener 同樣會將 View 的 longClickable 設(shè)置成 true

  • 在 ACTION_DOWN 操作中,如果是長按,回調(diào) OnLongClickListener.onLongClick

  • 在 ACTION_UP 操作中,回調(diào) OnClickListener.onClick

Activity.OnTouchEvent

所有流程走完,假如沒有一個 View 消費(fèi)事件,那最終會回到 Activity.OnTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //循環(huán)判斷是否有 ViewGroup 或者 View 消費(fèi)事件,如果沒有,事件回到 activity
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }

事件分發(fā)流程圖

Android事件分發(fā)機(jī)制是什么

  • 觸摸事件由 Activity.dispatchTouchEvent 先處理;再一層層往下分發(fā),當(dāng)中間的 ViewGroup 都不消費(fèi)或者攔截時,進(jìn)入最底層的 View,開始由最底層的 OnTouchEvent 來處理,如果一直不消費(fèi),則最后返回到 Activity.OnTouchEvent

  • 只有 ViewGroup 有 onInterceptTouchEvent 攔截方法;在分發(fā)過程中,中間任何一層 ViewGroup 都可以直接攔截,則不再往下分發(fā),而是交由發(fā)生攔截操作的 ViewGroup 的 OnTouchEvent 來處理

  • 子 View 可調(diào)用父 ViewGroup 的 requestDisallowInterceptTouchEvent 方法,來設(shè)置 disallowIntercept=true,從而阻止父 ViewGroup 的 onInterceptTouchEvent 攔截操作

  • OnTouchEvent 由下往上冒泡時,當(dāng)中間任何一層的 OnTouchEvent 消費(fèi)該事件,則不再往上傳遞,表示事件已消費(fèi)

  • 如果 dispatchTouchEvent 在進(jìn)行事件分發(fā)的時候,View 沒有消費(fèi) ACTIONDOWN 事件,即返回 true,則之后的 ACTIONMOVE 等事件都將無法接收

  • 不管 View 是 DISABLED(禁用)的還是 ENABLED(可用)的,只要是 CLICKABLE (可點(diǎn)擊),LONG_CLICKABLE(可長按) ,都會消費(fèi)事件

  • View 的 setOnClickListener 會默認(rèn)將 View 的 clickable 設(shè)置成 true,View 的 setOnLongClickListener 同樣會將 View 的 longClickable 設(shè)置成 true;所有 View 的 setClickable 和 setLongClickable 最好在兩個監(jiān)聽方法后調(diào)用

  • onTouch 優(yōu)先于 onTouchEvent 執(zhí)行,onClick 和 onLongClick 在 onTouchEvent 中被調(diào)用,且 onLongClick 優(yōu)先于 onClick 被執(zhí)行;如果 onTouch 返回 true,就不會執(zhí)行 onTouchEvent;onTouch 只有 View 設(shè)置了 OnTouchListener,且是 enable 的才執(zhí)行該方法

到此,相信大家對“Android事件分發(fā)機(jī)制是什么”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI