從果推因 ---- Android的事件的分發(fā)與攔截

緣由

偶然看到了下面這幾篇“逆視角”分析思考的文章磕蛇,覺(jué)得還是挺有意思的蒂誉,距離上次好好看事件分發(fā)源碼也有幾年了,想著也換個(gè)角度重新思考梳理下對(duì)Andrroid視圖層級(jí)事件處理的理解冠骄。
反思|Android 事件分發(fā)機(jī)制的設(shè)計(jì)與實(shí)現(xiàn)
反思|Android 事件攔截機(jī)制的設(shè)計(jì)與實(shí)現(xiàn)

首先帶幾個(gè)問(wèn)題

ViewTree

如上圖攒读,Android的視圖結(jié)構(gòu)可以本質(zhì)上構(gòu)成了一顆N叉樹(shù),每個(gè)節(jié)點(diǎn)都是View的子類(lèi)(View or ViewGroup)
ViewGroup節(jié)點(diǎn)持有View[] mChildren記錄該節(jié)點(diǎn)的所有子節(jié)點(diǎn)寨腔,整體構(gòu)成了一個(gè)N叉樹(shù)速侈。

  1. 為何選擇遞歸實(shí)現(xiàn)事件分發(fā)?
  2. FrameLayout內(nèi)放置兩個(gè)重疊的Button點(diǎn)擊如何響應(yīng)迫卢?Why倚搬?
  3. DOWN事件作為事件序列的開(kāi)始,既然首次遞歸遍歷找到了消費(fèi)DOWN事件的View乾蛤,為何不直接記錄該View的引用每界,后續(xù)直接將后續(xù)事件直接將事件直送該View?

比如ViewG接收了DOWN事件家卖,若是記錄ViewG那么下次可以直接從Activity或rootView直接下發(fā)MOVE/UP給ViewG眨层;而不需要再次rootView -> A ->D ->viewG

  1. 為何自定義View一般不修改dispatchTouchEvent函數(shù)?
  2. 為何解決滑動(dòng)沖突自定義View不應(yīng)攔截Down事件上荡?
  3. Cancel事件用處趴樱?

為何選擇遞歸

首先ViewTree這種天然的樹(shù)形結(jié)構(gòu)是非常符合遞歸算法,在樹(shù)形結(jié)構(gòu)上使用遞歸非常簡(jiǎn)單簡(jiǎn)潔(二叉樹(shù)的前中后序的遞歸遍歷酪捡,三五行代碼搞定)叁征;
其次符合實(shí)際情況,View嵌套層級(jí)越深逛薇,即ViewTree越下層的View反而顯示在屏幕的上層捺疼,越接近用戶(hù)所見(jiàn),遞歸的深度優(yōu)先DFS滿(mǎn)足這種讓底層的View可以先獲得事件的處理機(jī)會(huì)永罚,達(dá)到用戶(hù)“所觸即所得”的效果啤呼。

dispatchTouchEvent遞歸大概流程

ViewGroup dispatchTouchEvent:

    //省略大部分代碼议薪,看看關(guān)鍵步驟
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            //綜合requestDisallowInterceptTouchEvent及自身的onInterceptTouchEvent判斷是否攔截事件
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //注意這里的判斷,只有Down或者mFirstTouchTarget不為空才會(huì)進(jìn)行攔截判斷媳友;
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev); //調(diào)用了onInterceptTouchEvent ---> 函數(shù)return true代表攔截事件
                    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;
            }
            .......
            ......
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                       
                        final View[] children = mChildren;
                        //根據(jù)children index倒敘遍歷所有child view
                        //尋找真正需要消費(fèi)事件的child view
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            //根據(jù)touchEvent的坐標(biāo)x,y篩掉不在觸點(diǎn)范圍內(nèi)的childView
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                        ....... 
                        .......
                        //調(diào)用dispatchTransformedTouchEvent會(huì)觸發(fā)調(diào)用child的dispatchTouchEvent(這里的返回值代表了該child子樹(shù)內(nèi)是否有View消費(fèi)了事件)
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                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;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign); //注意這個(gè)函數(shù)斯议,記錄了該child是消費(fèi)事件的子child
                                alreadyDispatchedToNewTouchTarget = true;
                                break; //如果該child包含目標(biāo)view直接退出遍歷循環(huán)
                            }
                            
        //通過(guò)該函數(shù)實(shí)現(xiàn)遞歸調(diào)用所有觸點(diǎn)內(nèi)child的dispatchTouchEvent
       private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        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);
        }

View dispatchTouchEvent:

          //noinspection SimplifiableIfStatement
            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;
            }

忽略掉細(xì)節(jié)核心邏輯:ViewGroup會(huì)遍歷它的所有直接children(根據(jù)touch坐標(biāo)過(guò)濾掉不包含觸點(diǎn)坐標(biāo)的child),轉(zhuǎn)換坐標(biāo)之后繼續(xù)遞歸調(diào)用child的dispatchTouchEvent函數(shù)醇锚;最終遞歸到targetView的dispatchTouchEvent哼御,然后返回默認(rèn)false或者mOnTouchListener、onTouchEvent的返回值焊唬。

稍微畫(huà)個(gè)圖看下dispatchTouchEvent調(diào)用的順序:

image.png

通過(guò)幾個(gè)核心的邏輯恋昼,可以看出整個(gè)遞歸調(diào)其實(shí)相當(dāng)于偽廣度優(yōu)先BFS 和 深度優(yōu)先DFS結(jié)合來(lái)遍歷了這顆ViewTree(根據(jù)touch坐標(biāo)值優(yōu)化了不需要范圍的節(jié)點(diǎn));若是視圖層級(jí)很深赶促,布局復(fù)雜會(huì)導(dǎo)致遍歷開(kāi)銷(xiāo)的大幅增加

優(yōu)化遞歸的開(kāi)銷(xiāo)

通過(guò)上述分析液肌,這個(gè)遞歸調(diào)用鏈遍歷尋找targetView的過(guò)程開(kāi)銷(xiāo)比較大,所以從這里入手可以考慮優(yōu)化:

把事件分成DOWN鸥滨、MOVE嗦哆、UP&CANCEL序列,DOWN作為完整Touch事件的開(kāi)始節(jié)點(diǎn)婿滓,只要跟蹤到Down事件分發(fā)到targetView的路線(xiàn)老速,后面的MOVE/UP就不需要遍歷直達(dá)target。

       //addTouchTarget這個(gè)函數(shù)凸主,記錄了該child是消費(fèi)事件的子child
       newTouchTarget = addTouchTarget(child, idBitsToAssign); 
  
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

在遍歷ViewGroup尋找消費(fèi)事件的子View過(guò)程中橘券,Android是設(shè)計(jì)了一個(gè)TouchTarget的數(shù)據(jù)結(jié)構(gòu)一個(gè)單向鏈表,每次遍歷ViewGroup的所有children卿吐,若是該child(或者child的子view)消費(fèi)了事件那么會(huì)將該child存入mFirstTouchTarget鏈表旁舰。

為啥mFirstTouchTarget只存ViewGroup的直接childView

回到問(wèn)題3:rootView -> A ->D ->viewG這條路徑:

rootView 的 mFirstTouchTarget指向的是A
A的 mFirstTouchTarget指向的是D
D的 mFirstTouchTarget指向的是ViewG

是不是有點(diǎn)迷糊,如果rootView的mFirstTouchTarget直接指向ViewG不是更省事嗡官?并且明顯這個(gè)mFirstTouchTarget只會(huì)存在一個(gè)值箭窜,搞個(gè)鏈表(循環(huán)在找到第一個(gè)消費(fèi)事件的child時(shí)就break跳出了)?

  1. 這里若是直接從rootView->ViewG會(huì)廢掉onInterceptTouchEvent機(jī)制谨湘;
  2. 頂層rootView直接持有最底層的View绽快,打破了樹(shù)形結(jié)構(gòu)父節(jié)點(diǎn)僅依賴(lài)直接子節(jié)點(diǎn)的設(shè)計(jì)芥丧,ViewGroup的設(shè)計(jì)原則也被破壞紧阔。
  3. 設(shè)計(jì)成鏈表應(yīng)該是為了多指觸控考慮的

mFirstTouchTarget機(jī)制使得僅Down事件花費(fèi)較大循環(huán)+遞歸尋找targetView,后續(xù)依靠每個(gè)ViewGroup的mFirstTouchTarget值省掉了viewTree的橫向循環(huán)续担,但是遞歸函數(shù)調(diào)用的深度依然是樹(shù)形結(jié)構(gòu)的層數(shù)擅耽。

    //直接通過(guò)mFirstTouchTarget分發(fā)事件
   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;
          }
      }

FrameLayout內(nèi)放置兩個(gè)重疊的Button點(diǎn)擊如何響應(yīng)?Why物遇?

上面分析過(guò)乖仇,在ViewGroup橫向遍歷子View時(shí)找到第一個(gè)消費(fèi)事件的child就會(huì)記錄在mFirstTouchTarget然后跳出循環(huán)憾儒。所以這種情況肯定只有一個(gè)button會(huì)響應(yīng)。

考慮下面這么個(gè)布局乃沙,點(diǎn)擊事件直覺(jué)應(yīng)該是只有button1響應(yīng):

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <com.android.test.testapplication.lib.CustomFrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button"
            android:onClick="onClick"
            android:layout_gravity="center"/>
    </com.android.test.testapplication.lib.CustomFrameLayout>
    <com.android.test.testapplication.lib.CustomFrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button1"
            android:onClick="onClick"
            android:layout_gravity="center"/>
    </com.android.test.testapplication.lib.CustomFrameLayout>
    <com.android.test.testapplication.lib.CustomFrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </com.android.test.testapplication.lib.CustomFrameLayout>
</FrameLayout>
image.png

可以看到結(jié)果確實(shí)也是只有Button1響應(yīng)起趾,而且看輸出也印證了橫向循環(huán)是依據(jù)childView添加index倒敘來(lái)判斷是否消費(fèi)事件,并且由于childAt 1消費(fèi)了事件警儒,childAt 0的View一點(diǎn)機(jī)會(huì)都沒(méi)有训裆。

那么同樣的布局,有沒(méi)有什么方法讓下方的Button獲得響應(yīng)機(jī)會(huì)蜀铲?

  //三個(gè)自定義FrameLayout的父view也換成自定義View边琉,開(kāi)啟isChildrenDrawingOrderEnabled & 重寫(xiě)getChildDrawingOrder
  override fun onFinishInflate() {
        isChildrenDrawingOrderEnabled = true
        super.onFinishInflate()
    }
    override fun getChildDrawingOrder(childCount: Int, i: Int): Int {
        return childCount-1-i
    }

image.png

這下看到直接從childAt 0 開(kāi)始遍歷分發(fā)事件,其實(shí)是利用了ChildDrawingOrder记劝,查看橫向遍歷源碼可知道如果開(kāi)啟了isChildrenDrawingOrderEnabled = true变姨;遍歷時(shí)會(huì)做一個(gè)映射把實(shí)際index通過(guò)getChildDrawingOrder-->為自定義的順序;childCount-1-i //強(qiáng)行把末尾的index換成了childAt 0的View

為何自定義View一般不修改dispatchTouchEvent函數(shù)厌丑?

其實(shí)整個(gè)遞歸過(guò)程核心就是dispatchTouchEvent函數(shù)定欧,onInterceptTouchEventonTouchEvent 用于輔助。

onInterceptTouchEvent提供事件攔截機(jī)制怒竿,是滑動(dòng)沖突的解決方案忧额;
onTouchEvent通常情況是真正的事件消費(fèi)者,返回值會(huì)被dispatchTouchEvent調(diào)用棧層層上報(bào)一直到頂層View

默認(rèn)情況整個(gè)事件分發(fā)的V字形結(jié)構(gòu):ViewGroup 遞歸調(diào)用child的dispatchTouchEvent愧口,函數(shù)入棧過(guò)程形成了事件往底層View分發(fā)的路線(xiàn)睦番;最終最底層的targetView onTouchEvent的返回值,dispatchTouchEvent函數(shù)層層返回出棧:

遞歸函數(shù)調(diào)用棧

其中targetViewG的onTouchEvent返回值影響函數(shù)棧內(nèi)各個(gè)dispatchTouchEvent后續(xù)行為耍属,返回true則其直系parentView的dispatchTouchEvent函數(shù)將會(huì)給mFirstTouchTarget賦值托嚣,后續(xù)也逐層上報(bào)true,使得完整消費(fèi)路徑上mFirstTouchTarget都得到賦值厚骗。

返回false導(dǎo)致直系parentView的mFirstTouchTarget==null示启,觸發(fā)直系parentView調(diào)用super.dispatchTouchEvent也就是View的dispatchTouchEvent(其實(shí)幾乎就相當(dāng)于觸發(fā)parentView自己的onTouchEvent)

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                //下面這個(gè)函數(shù)傳入null值會(huì)觸發(fā)super.dispatchTouchEvent
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 

如果修改了dispatchTouchEvent的實(shí)現(xiàn)相當(dāng)于打斷了這條遞歸調(diào)用鏈,你得自己接上后續(xù)的調(diào)用(可能工作包括:轉(zhuǎn)換坐標(biāo)調(diào)用child的dispatch领舰、自行調(diào)用onIntercept和onTouchEvent函數(shù)夫嗓,甚至還需要維護(hù)對(duì)子view cancel事件的分發(fā)等)

最直觀(guān)的修改,自定義一個(gè)Button冲秽,直接把dispatchTouchEvent return true你的Button將不會(huì)響應(yīng)任何點(diǎn)擊事件包括點(diǎn)擊背景變化效果都沒(méi)了舍咖!

為什么滑動(dòng)沖突一般不應(yīng)攔截Down事件?

前面分析了一個(gè)事件完整序列是以Down事件開(kāi)始锉桑,為了減少消耗根據(jù)Down事件來(lái)追蹤事件的消費(fèi)路徑排霉,后續(xù)的Move、Cancel事件才有完整的路徑依據(jù)用于分發(fā)民轴;如果一開(kāi)始就攔截down事件攻柠,其(mFirstTouchTarget == null直接調(diào)用super.dispatch)導(dǎo)致該ViewGroup所有的childView什么事件都收不到球订。除非本意就是為了disable掉所有的子View接受事件。

為什么需要Action Cancel

上面提到滑動(dòng)沖突方案里面瑰钮,需要放開(kāi)Down事件的通行冒滩,那么子View接受Down事件響應(yīng)了pressed狀態(tài),而后續(xù)Move浪谴、Up事件被parentView攔截消費(fèi)掉了旦部;那么就沒(méi)有機(jī)會(huì)取消這個(gè)pressed狀態(tài)。
所以需要在事件被攔截之后或者說(shuō)改變事件消費(fèi)主體之后较店,應(yīng)該要給之前的消費(fèi)者一個(gè)Cancel通知士八;有始有終才不是“渣TouchEvent”。
通過(guò)Cancel事件可以作為清理標(biāo)志梁呈,用于清理恢復(fù)狀態(tài)婚度,比如把之前設(shè)置的touchTarget置為null等。

事件序列中onInterceptTouchEvent會(huì)執(zhí)行幾次官卡?

完整事件總是從Down開(kāi)始蝗茁,之前也是利用僅在Down事件的去遍歷尋找初始mFirstTouchTarget;以便優(yōu)化后續(xù)事件不需要再次遍歷寻咒。
同樣的理由是否存在于onInterceptTouchEvent哮翘?

因?yàn)閂iewGroup攔截事件之后,肯定是交由super.dispatchToucheEvent -->onTouchEvent處理毛秘;不會(huì)再去到其子View饭寺,故而可以在攔截事件之后將mFirstTouchTarget置為null; 那么后續(xù)事件(非Down)即可跳過(guò)onInterceptTouchEvent判斷。

            // Check for interception. 
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //判斷onInterceptTouchEvent條件:down 或者 touchTarget不為空
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    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;
            }
            
            //mFirstTouchTarget == null直接進(jìn)入super.dispatch
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    //首次進(jìn)入攔截邏輯touchTarget != null
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted; // intercepted=true --> cancelChild ==true
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            // 分發(fā)Action Cancel事件到touchTarget
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                //最終action cancel分發(fā)循環(huán)完成 --> mFirstTouchTarget=null
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

通過(guò)代碼印證首次攔截事件叫挟,touchTarget != null艰匙,遍歷touchTarget鏈表分發(fā)ActionCancel事件,最終將mFirstTouchTarget=null;后續(xù)的事件就不會(huì)再觸發(fā)interceptTouchEvent函數(shù)抹恳。從而也可知道员凝,interceptTouchEvent函數(shù)在一次事件序列中只會(huì)觸發(fā)一次。

父View攔截Move返回true之后本次序列不再觸發(fā)

childView調(diào)用禁止父View攔截不撤銷(xiāo)奋献,為何沒(méi)影響健霹?

Android提供了攔截機(jī)制用以解決滑動(dòng)沖突,相當(dāng)于給父View開(kāi)了特權(quán)攔截子View的事件瓶蚂;那么同樣的子View理應(yīng)也有拒絕父View攔截的權(quán)利:

攔截事件機(jī)制提供了滾動(dòng)父View和子View點(diǎn)擊事件的沖突解決糖埋;
那么ViewPager和里面的進(jìn)度條,滑動(dòng)和滑動(dòng)的沖突如何解決扬跋?
viewParent.requestDisallowInterceptTouchEvent(true)允許子View申請(qǐng)禁止父View攔截事件阶捆。

問(wèn)題:如果子View忘記requestDisallowInterceptTouchEvent(false),那么其父View攔截事件如何解除禁令钦听?
這里也是利用的事件序列的起點(diǎn):DOWN事件洒试,在dispatch函數(shù)起始就判斷收到DOWN事件就做了全面狀態(tài)重置,解除了禁令朴上。

            // Handle an initial down.
            //在ViewGroup的dispatch函數(shù)垒棋,一開(kāi)始就判斷了收到down事件做了全面狀態(tài)清理
            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);
                resetTouchState();
            }

      private void resetTouchState() {
          clearTouchTargets();
          resetCancelNextUpFlag(this);
          mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
          mNestedScrollAxes = SCROLL_AXIS_NONE;
      }

onTouchListener & onTouchEvent

這個(gè)看一眼View dispatchTouchEvent就可以知道: touchListener先于onTouchEvent調(diào)用,并且touchListener的返回值決定了是否執(zhí)行onTouchEvent痪宰。
touchListener優(yōu)先于touchEvent函數(shù)叼架,touchListener消費(fèi)事件返回true消費(fèi)事件之后,onTouchEvent將不觸發(fā)衣撬。而onClick乖订、onLongClick的響應(yīng)會(huì)受到影響,click事件都是在onTouchEvent處理的具练。

            //先調(diào)用了touchListener回調(diào)然后根據(jù)其返回值決定是否調(diào)用onTouchEvent
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //短路特性乍构,如果result=true, onTouchEvent將不會(huì)調(diào)用
            if (!result && onTouchEvent(event)) {
                result = true;
            }

onClick & onLongClick主要依據(jù)是在onTouchEvent里面down和up事件判斷扛点。
Down事件是肯定都需要的哥遮,click僅需要判斷up事件到來(lái)即可滿(mǎn)足click條件;onLongClick則需要超過(guò)500ms未接受Up or Cancel事件才滿(mǎn)足longClick陵究。 同時(shí)onLongClick的返回值代表響應(yīng)longClick之后是否還需要響應(yīng)click眠饮,長(zhǎng)按之后還是會(huì)觸發(fā)一個(gè)up事件的!

擴(kuò)展思考

前面的結(jié)論好似都指向了一個(gè)TouchEvent僅能被一個(gè)View消費(fèi),那么現(xiàn)在的嵌套滑動(dòng)怎么玩铜邮?

參考

https://juejin.im/post/5d3140c951882565dd5a66ef

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末仪召,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子松蒜,更是在濱河造成了極大的恐慌返咱,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件牍鞠,死亡現(xiàn)場(chǎng)離奇詭異咖摹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)难述,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)萤晴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人胁后,你說(shuō)我怎么就攤上這事店读。” “怎么了攀芯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵屯断,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng)殖演,這世上最難降的妖魔是什么氧秘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮趴久,結(jié)果婚禮上丸相,老公的妹妹穿的比我還像新娘。我一直安慰自己彼棍,他們只是感情好灭忠,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著座硕,像睡著了一般弛作。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上华匾,一...
    開(kāi)封第一講書(shū)人閱讀 49,785評(píng)論 1 290
  • 那天映琳,我揣著相機(jī)與錄音,去河邊找鬼瘦真。 笑死刊头,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诸尽。 我是一名探鬼主播原杂,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼您机!你這毒婦竟也來(lái)了穿肄?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤际看,失蹤者是張志新(化名)和其女友劉穎咸产,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體仲闽,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡脑溢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了赖欣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屑彻。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖顶吮,靈堂內(nèi)的尸體忽然破棺而出社牲,到底是詐尸還是另有隱情,我是刑警寧澤悴了,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布搏恤,位于F島的核電站违寿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏熟空。R本人自食惡果不足惜藤巢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望痛阻。 院中可真熱鬧菌瘪,春花似錦腮敌、人聲如沸阱当。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)弊添。三九已至,卻和暖如春捌木,著一層夾襖步出監(jiān)牢的瞬間油坝,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工刨裆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留澈圈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓帆啃,卻偏偏與公主長(zhǎng)得像瞬女,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子努潘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348