Android事件分發(fā)|事件沖突處理

android的事件分發(fā)在面試時(shí)算是高頻問(wèn)題,工作中也能用到,這里將事件分發(fā)、事件沖突,和NestedScrolling中的事件傳遞整理哈。

Android事件分發(fā)

android 事件分發(fā).jpg

方法說(shuō)明
dispatchTouchEvent:事件分發(fā),Activity, ViewGroup, View都有該方法,Activity和ViewGroup分發(fā)給子View, View分發(fā)給自己
onInterceptTouchEvent:攔截事件,只有ViewGroup有該方法,用于事件,ViewGroup想要處理某個(gè)事件時(shí),可以隨時(shí)對(duì)子View, say no!我這個(gè)我要處理
onTouchEvent:事件消費(fèi),Activity,ViewGroup,View都有該方法

對(duì)于開發(fā)者來(lái)說(shuō),第一個(gè)接收到事件的地方就在dispatchTouchEvent中,如果想全局不允許點(diǎn)擊是,事件可以在這里直接返回,不進(jìn)行下一步的分發(fā)。上段源碼

Activity#dispatchTouchEvent

  public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
  1. 調(diào)用Window的superDispatchTouchEvent
  2. 沒(méi)有view消費(fèi),我自己調(diào)用onTouchEvent,返回消費(fèi)結(jié)果

PhoneWindow#superDispatchTouchEvent

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

這里就熟悉了,調(diào)用了mDecor的superDispatchTouchEvent
DecorView#superDispatchTouchEvent

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

DecorView繼承了FrameLayout,F(xiàn)rameLayout又繼承了ViewGroup,F(xiàn)rameLayout中并沒(méi)有重寫dispatchTouchEvent, 所以就調(diào)用到了ViewGroup的dispatchTouchEvent

ViewGroup#dispatchTouchEvent

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
         ...//此處省略數(shù)行
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                //重寫設(shè)置狀態(tài)
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            //mFirstTouchTarget 不為空和Down事件,所以有子view消費(fèi)的情況下,此處一直為真
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //是否禁止攔截
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //父view是否攔截
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
          //很重要的標(biāo)識(shí),當(dāng)前是否已經(jīng)分配給target,不至于被down事件被消費(fèi)兩次
           boolean alreadyDispatchedToNewTouchTarget = false;
            //如果取消和攔截都不查找子view
            if (!canceled && !intercepted) {
                      ...此處省略數(shù)行
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                           ...此處省略數(shù)行
                            //這里調(diào)dispatchTransformedTouchEvent, 最終調(diào)用了子View的onTouchEvent去確定該事件是否消費(fèi)
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                               ...此處省略數(shù)行
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }              
                        }
                    }
                }
            }
            // 沒(méi)有找到消費(fèi)的子view,那去看看自己消費(fèi)不,最終調(diào)用到了ViewGroup的onTouchEvent
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }else {
                //mFirstTouchTarget的后續(xù)事件,move/up都會(huì)走這里去下發(fā)
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    //down事件是 alreadyDispatchedToNewTouchTarget 已經(jīng)為true,所以down事件不會(huì)被消費(fèi)兩次
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                          target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
        ...此處省略數(shù)行
        return handled;
    }

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

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ...此處省略數(shù)行
        //父view調(diào)用是child==null,調(diào)用super.dispatchTouchEvent
        // Perform any necessary transformations and dispatch.
        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);
        }
        return handled;
    }

這里調(diào)用了子view的dispatchTouchEvent, 為了讓咋們的布局文件接收到分發(fā)事件,其實(shí)是頂層ViewGroup(DecorView)調(diào)用布局文件的dispatchTouchEvent,各個(gè)ViewGroup逐層分發(fā),直到有一個(gè)子View或者ViewGroup消費(fèi)了事件。對(duì)于上層ViewGroup而言,View或者ViewGroup,對(duì)于他們都是一樣處理。調(diào)用dispatchTouchEvent,ViewGroup調(diào)用dispatchTouchEvent就再次分發(fā),然后咋們看哈View的dispatchTouchEvent

View#dispatchTouchEvent

    public boolean dispatchTouchEvent(MotionEvent event) {
        ...此處省略數(shù)行
            if (!result && onTouchEvent(event)) {
                result = true;
            }
           ...此處省略數(shù)行
        return result;
    }

子view調(diào)用了onTouchEvent,如果消費(fèi)了就會(huì)返回true

總結(jié)

  • 事件從ViewGroup逐級(jí)往下分發(fā),直到找到消費(fèi)的view或者viewgroup
  • 子view一但消費(fèi)了down后續(xù)的move和up都會(huì)分發(fā)給它(一個(gè)前提,未被父view攔截),即使onTouchEvent返回了flase
  • 父view一但做了攔截,不管子view是否還想消費(fèi)事件,都會(huì)被父view消費(fèi)掉
  • 如果沒(méi)有子view消費(fèi),父view就會(huì)調(diào)用自己的onTouchEvent

關(guān)于第二點(diǎn)還要補(bǔ)充哈,為什么子view一但在down事件中返回了true,后續(xù)的事件都會(huì)分發(fā)給它,因?yàn)楦竀iew的mFirstTouchTarget 已經(jīng)不為空,父View的父級(jí)View中的mFirstTouchTarget 也不為,一層層的下來(lái)。事件每次都會(huì)分發(fā)給down時(shí)返回true的view。這就是為什么,有時(shí)候我們明明已經(jīng)移出控件外了,但是還是會(huì)接收到move和up事件。如果move和up事件返回false,事件最終就會(huì)調(diào)用activity的onTouchEvent

事件沖突處理

從上面的事件分發(fā)可知,ViewGroup擁有子view的絕對(duì)分配權(quán),父view攔截事件,就沒(méi)得子view啥事了。
在我們開發(fā)過(guò)程中可能遇到,在一個(gè)垂直滾動(dòng)的scrollview中前提一個(gè)橫向的列表,如果橫向滾動(dòng)列表,手指不會(huì)一直是一條直線,導(dǎo)致scrollview上下滾動(dòng),這樣體驗(yàn)就不好。這個(gè)就是需要解決的事件沖突,解決這種沖突有兩個(gè)方案。

Plan1:重寫父onTouchEvent,監(jiān)聽當(dāng)前頁(yè)面的列表,如果列表是當(dāng)前消費(fèi)事件,onTouchEvent就不消費(fèi)了

Plan2:在子View的down或者move事件中調(diào)用parent.requestDisallowInterceptTouchEvent()

為什么需要在子View的down和move中去調(diào)用?在父View的dispatchTouchEvent中,down事件是會(huì)去重置禁止攔截的標(biāo)識(shí),詳細(xì)查看ViewGroup.resetTouchState()

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容