Android-輸入事件一擼到底之View接盤俠(3)

前言

1、Android 輸入事件一擼到底之源頭活水(1)
2、Android 輸入事件一擼到底之DecorView攔路虎(2)
3、Android 輸入事件一擼到底之View接盤俠(3

image.png

前兩篇文章分別分析了輸入事件分發(fā)到App層以及DecorView對(duì)輸入事件的處理,最終交給ViewTree處理。我們平時(shí)對(duì)事件的處理大部分集中在對(duì)ViewTree的處理上,網(wǎng)上絕大部分的文章也是針對(duì)此分析,為了將輸入事件連貫起來,從總體看局部,由局部推總體,接下來分析ViewTree的事件分發(fā)。
通過本篇文章,你將了解到:

1、View/ViewGroup/ViewTree 易混淆之處
2、ViewGroup 事件分發(fā)
3、View 事件分發(fā)
4、ViewTree 事件分發(fā)
5、事件分發(fā)系列總結(jié)

一個(gè)小比喻

對(duì)代碼調(diào)用流程比較疑惑的話,我們做個(gè)簡(jiǎn)單的比喻:
一個(gè)學(xué)校有3個(gè)年級(jí),每個(gè)年級(jí)有1個(gè)年級(jí)主任、3個(gè)班,每個(gè)班有個(gè)班主任、有若干個(gè)學(xué)生,其中一個(gè)學(xué)生叫小明

有一天,校長(zhǎng)接到了個(gè)任務(wù):有個(gè)日本的女學(xué)生:石原里美要來學(xué)校做交流。
校長(zhǎng)將這個(gè)任務(wù)指派下去,先找到1年級(jí)主任問你們年級(jí)要接收這個(gè)學(xué)生不?年級(jí)主任想到手底下還有幾個(gè)班,就先問1班班主任你們能接收嗎,1班主任想這么多學(xué)生我問問誰能帶帶這個(gè)石原里美,就先問小明。這個(gè)過程叫做dispatchTouchEvent。
小明接到任務(wù),發(fā)現(xiàn)手底下沒人了(自己是View,班主任、年級(jí)主任、校長(zhǎng)是ViewGroup),就只能自己硬著頭皮看看這石原里美資料,這時(shí)候他有兩個(gè)選擇:接受/拒絕
小明看了資料發(fā)現(xiàn)石原里美是個(gè)小美女,于是開心選擇了接受,那么就答應(yīng)班主任,班主任將結(jié)果告訴年級(jí)主任,年級(jí)主任告訴校長(zhǎng),校長(zhǎng)長(zhǎng)嘆一口氣,終于將包袱甩掉了。。這個(gè)過程就是dispatchTouchEvent 回傳結(jié)果。
某天石原里美的同班男同學(xué):松井 聽說石原里美在中國(guó)玩得挺開心的,自己也想來。校長(zhǎng)架不住,只能答應(yīng)。校長(zhǎng)知道石原里美在1年級(jí)里做交流,想想松井和石原里美是同學(xué),在一起更好交流,于是直接就將這個(gè)任務(wù)交給了1年級(jí)主任,1年級(jí)主任知道石原里美在1班,于是直接交給了1班,1班主任知道小明接待過石原里美,就讓小明陪松井(石原里美是Down事件,松井是后續(xù)的Move、Up事件),小明心里一萬只草泥馬,誰叫自己沖動(dòng)了呢(自己xxx,跪著也要完成)。這個(gè)過程就是某布局一旦處理了Down事件,那么后續(xù)的事件都會(huì)通知給它。
時(shí)光回到過去,小明是個(gè)剛正不阿的學(xué)生,表示自己學(xué)習(xí)很忙,書中自有黃金屋,書中自有顏如玉,沒時(shí)間陪伴石原里美,這時(shí)他告訴班主任他不接這個(gè)任務(wù),班主任一看,班里沒人愿意接這活兒了,幸好還有B計(jì)劃,就先看看自己能完成這個(gè)任務(wù)不。如果不能完成,還是交給領(lǐng)導(dǎo)來做吧,交給了年級(jí)主任,年級(jí)主任暗自慶幸自己也有B計(jì)劃,發(fā)現(xiàn)自己B計(jì)劃能完成,于是就親自帶石原里美了。校長(zhǎng)知道年級(jí)主任接收任務(wù)了,很開心,自己的B計(jì)劃終于沒有擺上臺(tái)面。這個(gè)B計(jì)劃就是onTouchEvent
當(dāng)然,松井同學(xué)最后也交給了年級(jí)主任帶。。
中間有個(gè)插曲,班主任看了石原里美資料,心里有個(gè)大膽的想法:自己的兒子和她差不多年級(jí),多交流一下提高兒子的外語水平,多好啊。于是當(dāng)他從年級(jí)主任那收到這個(gè)任務(wù)的時(shí)候,就不把這個(gè)任務(wù)發(fā)下去了,告訴年級(jí)主任自己可以處理。這個(gè)過程就是 onInterceptTouchEvent
當(dāng)然小明也不是沒機(jī)會(huì)陪伴石原里美,學(xué)校之前制定了規(guī)矩:任何人都可以禁止上級(jí)不將任務(wù)派給自己(領(lǐng)導(dǎo)不能私自將任務(wù)攔截了,至少得通知下級(jí)),小明使用了這條規(guī)定,班主任的大膽想法碎了一地。。當(dāng)然這條規(guī)定一視同仁,包括年級(jí)主任、校長(zhǎng)都要遵守。
這個(gè)過程就是 requestDisallowInterceptTouchEvent

View/ViewGroup/ViewTree 易混淆之處

父類/子類、父布局/子布局

網(wǎng)上一些文章在畫關(guān)系圖的時(shí)候沒有將兩者區(qū)分開,容易讓剛接觸此內(nèi)容的人混淆。先看看View、ViewGroup定義:

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource {}

public abstract class ViewGroup extends View implements ViewParent, ViewManager {}

可以看出ViewGroup 繼承自View,也即是ViewGroup是View的子類,View是ViewGroup的父類。
父類/子類關(guān)系是語言范疇的關(guān)系:
子類訪問父類的方法:

super.doMethod(xx)

再來看看常見的布局文件內(nèi)容:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="@color/colorGreen"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    tools:context=".MainActivity">
    <View
        android:background="@color/colorAccent"
        android:layout_width="100dp"
        android:layout_height="100dp">
    </View>
</FrameLayout>

FrameLayout是ViewGroup子類,該布局文件里,F(xiàn)rameLayout是父布局,View是子布局。從ViewGroup的命名可以看出,ViewGroup是View的集合,當(dāng)然ViewGroup也可以是ViewGroup的集合(嵌套)。
父布局/子布局關(guān)系是ViewGroup/View 里定義的。
子布局尋找父布局

    public final ViewParent getParent() {
        return mParent;
    }
  • ViewParent 是個(gè)接口,ViewGroup、ViewRootImpl 都實(shí)現(xiàn)了它
    獲取到mParent后需要強(qiáng)轉(zhuǎn)為對(duì)應(yīng)類型
  • 每當(dāng)將子布局添加到父布局里的時(shí)候,就給子布局指定其父布局,也就是給mParent賦值 (assignParent(xx))
  • RootView(如DecorView)的mParent指向ViewRootImpl (setView(xx) 里指定)

父布局尋找子布局
父布局通過addView(xx)方法將子布局添加到一維數(shù)組里,因此父布局尋找其子布局也即是訪問該組數(shù)的過程:

    public View getChildAt(int index) {
        if (index < 0 || index >= mChildrenCount) {
            return null;
        }
        //private View[] mChildren;
        return mChildren[index];
    }

ViewTree 建立

了解父布局、子布局關(guān)系,將子布局添加到父布局里,這是最簡(jiǎn)單的ViewTree結(jié)構(gòu)。再將父布局作為另一個(gè)父布局的子布局添加,那么ViewTree又增加了一層。如此反復(fù),最終形成的ViewTree 如下:


image.png

注意:ViewTree并不是一個(gè)類,僅僅只是為了方便描述View/ViewGroup構(gòu)成的布局層次而命名的。

由此可知:

  • View是ViewGroup父類
  • View只能作為子布局,不能作為父布局
  • ViewGroup既可做父布局,也可做子布局

厘清了上述概念,接下來進(jìn)入正題。

ViewGroup 事件分發(fā)

回顧上篇文章內(nèi)容:

1、DecorView 將事件傳遞給Activity處理,如果Activity沒有處理,那么傳遞到Window進(jìn)而傳遞到DecorView
2、DecorView 調(diào)用父類的dispatchTouchEvent(xx)方法繼續(xù)分發(fā)事件

可以看出,此處重點(diǎn)是父類的dispatchTouchEvent(xx)方法。
DecorView繼承自FrameLayout,在FrameLayout里尋找該方法,發(fā)現(xiàn)FrameLayout并沒有重寫該方法。而FrameLayout繼承自ViewGroup,在ViewGroup里找到了dispatchTouchEvent(xx)。因此DecorView最終調(diào)用的是ViewGroup的dispatchTouchEvent(xx)方法。

一個(gè)小例子

image.png

如上圖所示,ViewGroup里4個(gè)子布局,添加順序如下:

addView(View1)
addView(View2)
addView(View3)
addView(View4)

View1、View2、View3相交于 "1"的位置
View3、View4相交于"2"的位置
當(dāng)分別點(diǎn)擊"1"、"2" 位置時(shí),事件時(shí)怎么傳遞的呢?
先說結(jié)論:
當(dāng)點(diǎn)擊"1" 位置時(shí):

1、ViewGroup 首先收到事件,并查找1位置是否落在某個(gè)子布局之內(nèi)
2、ViewGroup 有4個(gè)子布局,倒序遍歷尋找,也就是View4->View1的順序?qū)ふ?br> 3、先判斷View4,"1"不在View4內(nèi),繼續(xù)尋找
3、然后找到View3,判斷View3是否想處理該事件,如果處理,那么事件不再傳遞給View1、View2;如果不接收,繼續(xù)判斷View2;
4、View2不處理,繼續(xù)判斷View1。
5、當(dāng)View3、View2、View1 都不處理事件,那么只能交給他們的父布局ViewGroup。
6、當(dāng)ViewGroup自身也不想處理,那么退回給它的父布局,其父布局的操作和ViewGroup對(duì)事件的分發(fā)一樣的原理。

當(dāng)點(diǎn)擊"2" 位置時(shí),與點(diǎn)擊"1" 位置類似,不再贅述。
實(shí)際上上述事件分發(fā)的流程就是由ViewGroup dispatchTouchEvent(xx)方法完成,來看看它的源碼:

ViewGroup dispatchTouchEvent(MotionEvent ev)

ViewGroup.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        //標(biāo)記該事件是否已處理
        boolean handled = false;
        //如果該View沒有被遮擋,那么可以接收事件
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                //如果是Down事件,表明是一次事件序列的開始
                //清空之前的狀態(tài)
                //清空touchTarget鏈表
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            //標(biāo)記是否攔截該事件
            final boolean intercepted;
            //兩個(gè)條件滿足一個(gè)即可
            //1、是Down事件 2、mFirstTouchTarget != null 表示有子布局處理了Down事件,也就是子布局在收到Down事件時(shí)返回了true
            //mFirstTouchTarget -> 指向鏈表頭
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //查看標(biāo)記位:自己能否被允許攔截事件 disallowIntercept=true 表示不被允許攔截事件------(1)
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //能夠攔截事件,調(diào)用ViewGroup onInterceptTouchEvent 進(jìn)行攔截------(2)
                    //返回值表示是否已處理該事件
                    intercepted = onInterceptTouchEvent(ev);
                    //恢復(fù)事件防止之前中途修改過
                    ev.setAction(action);
                } else {
                    //不允許攔截,則肯定未處理
                    intercepted = false;
                }
            } else {
                //如果不是Down事件且也沒有任何子布局處理過Down事件,則表示ViewGroup已經(jīng)攔截處理該事件
                intercepted = true;
            }
            ...
            //記錄處理了Down事件的鏈表
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //事件未取消且沒被攔截處理
            if (!canceled && !intercepted) {
                ...
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex();
                    //單指、多指Down事件,鼠標(biāo)事件
                    //ViewGroup 直接子布局個(gè)數(shù)
                    final int childrenCount = mChildrenCount;
                    //有子布局且還未有任何子布局處理過事件
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        //根據(jù)繪制順序生成接收事件的子布局列表,一般都忽略
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //倒序?qū)ふ易硬季郑琣ddView(xx1) addView(xx2)是正序
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //先確定待檢測(cè)的子布局
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            ...
                            //child.canReceivePointerEvents() -> 能否接收事件,也就是子布局是否可見 Visible 不可見無法接收事件
                            //isTransformedTouchPointInView() -> 檢測(cè)當(dāng)前點(diǎn)擊的點(diǎn)是否落在子布局內(nèi),檢測(cè)時(shí)候考慮了子布局的padding/scroll/matrix 對(duì)位置的影響
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                //不滿足條件,則跳過該子布局,繼續(xù)尋找另一個(gè)布局
                                continue;
                            }
                            //上述條件滿足了,檢測(cè)該子布局是否已經(jīng)處理過該Down事件
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                //處理過直接跳出循環(huán),不用再找下一個(gè)子布局了
                                break;
                            }
                            //該子布局還未處理過Down事件
                            //將事件分發(fā)給子布局->child ------(3)
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                //返回true->子布局已經(jīng)處理了該Down事件
                                mLastTouchDownTime = ev.getDownTime();
                                ...
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //將子布局構(gòu)成的TouchTarget掛到鏈表頭(鏈表節(jié)點(diǎn)表示處理了Down事件的子布局)
                                //mFirstTouchTarget 指向當(dāng)前鏈表頭(也即是mFirstTouchTarget有值了,重要!)
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //標(biāo)記Down事件已經(jīng)找到處理者了
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            ...
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    ...
                }
            }

            //沒找到任何處理了Down事件的子布局
            if (mFirstTouchTarget == null) {
                //因?yàn)闆]有找到任何處理了Down事件的子布局,因此事件分發(fā)是目標(biāo)布局填:null ------(4)
                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) {
                        //Down事件,之前已經(jīng)處理過了,這里直接標(biāo)記位已吹李
                        handled = true;
                    } else {
                        //Down之外的其它事件,如Move Up等
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //target.child 為目標(biāo)子布局,分發(fā)給它處理 ------(5)
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            //如返回true,標(biāo)記為已處理
                            handled = true;
                        }
                        if (cancelChild) {
                            //如已取消,則跳過當(dāng)前繼續(xù)尋找下一個(gè)節(jié)點(diǎn)
                        }
                    }
                    //繼續(xù)尋找下一個(gè)需要處理的節(jié)點(diǎn) 一般來說該鏈表通常只有一個(gè)節(jié)點(diǎn)
                    predecessor = target;
                    target = next;
                }
            }
            ...
        }
        ...
        //最終返回dispatchTouchEvent(xx)該方法對(duì)事件處理結(jié)果
        //true->已處理 false->未處理 ------(6)
        return handled;
    }

注意,為了理解方便,上面以子布局代替子View闡述,子布局可以是View也可以是ViewGroup
上邊注釋比較比較清晰了,列出了(1)~(6)比較重要的點(diǎn):
(1)
禁止攔截標(biāo)記:

ViewGroup.java
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            //已設(shè)置過了,無需再次設(shè)置
            return;
        }
        //設(shè)置標(biāo)記位
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        if (mParent != null) {
            //遞歸調(diào)用父類,直至ViewRootImpl
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

該方法為ViewGroup獨(dú)有,若某個(gè)子布局不想讓其父布局?jǐn)r截其事件序列,那么調(diào)用getParent().requestDisallowInterceptTouchEvent(true)即可。該方法一直往上追溯設(shè)置父布局,也就是子布局之上的所有層次的父布局不攔截事件

(2)
只有ViewGroup 有onInterceptTouchEvent(xx)方法,View沒有。該方法是為了在事件分發(fā)給子布局之前進(jìn)行攔截操作。

ViewGroup.java
    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())) {
            //一般不會(huì)走到這
            return true;
        }
        return false;
    }

如果不重寫onInterceptTouchEvent(xx)方法,默認(rèn)不攔截事件。當(dāng)重寫該方法進(jìn)行攔截的時(shí)候需要注意:

onInterceptTouchEvent(xx)攔截到Down事件后,如果此時(shí)沒有任何子布局處理Down事件,那么后續(xù)的Move、Up等事件onInterceptTouchEvent(xx) 將不會(huì)收到,也就是onInterceptTouchEvent不執(zhí)行
一般很少重寫ViewGroup dispatchTouchEvent(xx),處理事件使用onInterceptTouchEvent(xx) + onTouchEvent(xx) 處理

(3)
將事件分發(fā)給子布局

ViewGroup.java
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
        //child 指的是要接收事件的子布局
        final boolean handled;
        ...
        if (child == null) {
            //如果沒有子布局接收Down事件,那么Down/Move/Up事件直接調(diào)用父類處理方法
            //ViewGroup 父類就是View 因此調(diào)用的是View的dispatchTouchEvent(xx)方法
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            //有子布局接收Down事件
            //計(jì)算子布局位置偏移
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            //將Event 位置偏移,使它落在子布局內(nèi)
            transformedEvent.offsetLocation(offsetX, offsetY);
            //考慮matrix 對(duì)位置的影響
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }
            //Event位置調(diào)整后,它的位置是基于當(dāng)前子布局的左上角為原點(diǎn)偏移的
            //繼續(xù)分發(fā)給子布局 child可能為View也可能為ViewGroup,如果是ViewGroup那么又重新走到了其父布局的分發(fā)邏輯
            //繼續(xù)遞歸分發(fā),直至返回
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        //處理結(jié)果
        return handled;
    }

1、該方法遞歸派發(fā)事件
2、MotionEvent修改的是AXIS_X、AXIS_Y值,也就是Event.getX()、Event.getY()取得的值,這也就是為什么這兩個(gè)值是距離當(dāng)前View左上角的原因

(4)
此處方法

handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
傳入的是null,因此交給View dispatchTouchEvent(xx)處理

(5)
當(dāng)有子布局處理了Down事件,那么后續(xù)的Move、Up等事件傳遞過來后,直接派發(fā)給當(dāng)初處理了Down事件的子布局。
由此可以看出:

1、Down事件是事件序列(Down->Move->Up)的開始,如果某個(gè)布局沒有處理Down事件,那么后續(xù)的事件將收不到
2、如果某個(gè)布局處理了Down事件,那么即使父布局?jǐn)r截(onInterceptTouchEvent(xx))了事件,子布局依然能夠收到完整的事件序列

(6)
最終事件的處理結(jié)果體現(xiàn)在一個(gè)布爾值上。
true -> 表示該事件已處理
false -> 表示該事件未處理
需要注意的是:

不管是否對(duì)事件"真正處理",只要返回true,就告訴調(diào)用者該事件已處理。即使對(duì)事件做了"很多處理",返回false,就告訴調(diào)用者該事件未處理。

上面分析了一堆可能比較混淆,用圖表示ViewGroup 分發(fā)事件的過程:


image.png

ViewGroup 事件分發(fā)常用方法

image.png

以上,分析了ViewGroup分發(fā)事件的邏輯,接下來看看事件分發(fā)到View時(shí)如何處理。

View 事件分發(fā)

從ViewGroup分發(fā)邏輯可以看出:ViewGroup分發(fā)事件的過程就是遞歸查找子布局并分發(fā)。那么遞歸結(jié)束的條件是什么呢?

1、有子布局處理了該事件,最終調(diào)用View.dispatchTouchEvent(xx)
2、沒有子布局處理該事件,只能自己接收(不一定處理),此時(shí)調(diào)用super.dispatchTouchEvent(xx),也就是View.dispatchTouchEvent(xx)

由此可知,ViewGroup分發(fā)事件最終需要交給View.dispatchTouchEvent(xx)處理。

View dispatchTouchEvent(MotionEvent ev)

    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //停止滑動(dòng)
            stopNestedScroll();
        }
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //如果注冊(cè)了onTouch回調(diào),則執(zhí)行onTouch方法
            //如果該方法返回true,則認(rèn)為該事件已經(jīng)處理了
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //事件未處理,則調(diào)用onTouchEvent處理
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ...
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }
        return result;
    }

onTouch通過以下方法注冊(cè):

    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

如果onTouch(xx)處理了事件,那么onTouchEvent(xx)就不會(huì)調(diào)用

onTouchEvent(MotionEvent event)

    public boolean onTouchEvent(MotionEvent event) {
        //獲取點(diǎn)擊坐標(biāo)
        final float x = event.getX();
        final float y = event.getY();
        //View控制標(biāo)記,根據(jù)標(biāo)記內(nèi)容控制View的屬性
        final int viewFlags = mViewFlags;
        //點(diǎn)擊動(dòng)作->Down/Move/Up等
        final int action = event.getAction();
        //如果該View設(shè)置了:可以單擊、可以長(zhǎng)按、鼠標(biāo)右鍵彈出(很少用)中的一個(gè)
        //那么認(rèn)為該View可以點(diǎn)擊-------(1)
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            //View默認(rèn)是ENABLED,若是DISABLED,則返回clickable
            return clickable;
        }
        if (mTouchDelegate != null) {
            //用在子布局?jǐn)U大其點(diǎn)擊區(qū)域使用,當(dāng)點(diǎn)擊坐標(biāo)位于子布局之外時(shí),通過該方法判斷點(diǎn)擊坐標(biāo)是否位于子布局的"擴(kuò)大區(qū)域內(nèi)"
            //若是則將事件交給子布局處理---------(2)
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        //View可點(diǎn)擊或者鼠標(biāo)移動(dòng)懸浮顯示(很少用)
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    //在Down事件里已經(jīng)處在按下狀態(tài)
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ...
                        //如果處在焦點(diǎn)獲取狀態(tài)但又未獲得焦點(diǎn),則主動(dòng)申請(qǐng)焦點(diǎn)
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        //mHasPerformedLongPress 表示長(zhǎng)按事件是否已經(jīng)處理了事件
                        //如果已經(jīng)處理了,則單擊事件不會(huì)執(zhí)行
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            //長(zhǎng)按事件還沒來得及處理,此處將長(zhǎng)按事件移除
                            removeLongPressCallback();
                            //如果上一步是請(qǐng)求獲取焦點(diǎn)并成功了,則這一次不處理后續(xù)事件
                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    //實(shí)現(xiàn)Runnable接口的類,用于回調(diào)
                                    mPerformClick = new PerformClick();
                                }
                                //為了給View更新其他狀態(tài)留夠時(shí)間,此處是通過Handler發(fā)送到主線程執(zhí)行------(3)
                                if (!post(mPerformClick)) {
                                    //如果不成功,則直接調(diào)用
                                    performClickInternal();
                                }
                            }
                        }
                        ...
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_DOWN:
                    ...
                    boolean isInScrollingContainer = isInScrollingContainer();
                    if (isInScrollingContainer) {
                        ...
                        //在可滾動(dòng)的容器內(nèi),為了容錯(cuò),延遲點(diǎn)擊
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        //設(shè)置按下的狀態(tài),多用于按下時(shí)背景/前景等變化
                        setPressed(true, x, y);
                        //開啟一個(gè)長(zhǎng)按延時(shí)事件,當(dāng)延時(shí)事件到了就執(zhí)行該事件(長(zhǎng)按事件)
                        //ViewConfiguration.getLongPressTimeout() 就是長(zhǎng)按的時(shí)間閾值。不同系統(tǒng)可能不一樣
                        //我手機(jī)上是400ms,缺省值是500ms----------(4)
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;
                case MotionEvent.ACTION_MOVE:
                    ...
                    break;
            }
            //只要是clickable=true 則認(rèn)為已經(jīng)處理了該事件
            return true;
        }
        return false;
    }

和ViewGroup分析類似,上邊注釋比較比較清晰了,列出了(1)~(4)比較重要的點(diǎn):
(1)
CLICKABLE/LONG_CLICKABLE 相關(guān)
賦值操作:

View.java
    public void setClickable(boolean clickable) {
        setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
    }
    public void setLongClickable(boolean longClickable) {
        setFlags(longClickable ? LONG_CLICKABLE : 0, LONG_CLICKABLE);
    }

當(dāng)前也可以在XML里指定屬性。你可能比較疑惑,一般我們都不用設(shè)置上面的屬性,View.onTouchEvent(xx)依然能夠執(zhí)行,咋回事呢?這得要分兩種情況:

1、View CLICKABLE/LONG_CLICKABLE 屬性默認(rèn)是沒有設(shè)置的,比如TextView就沒設(shè)置CLICKABLE,但是Button在其默認(rèn)屬性了設(shè)置了CLICKABLE
2、不管有沒有設(shè)置上述屬性,只要調(diào)用了View.setOnClickListener(xx)/View.setOnLongClickListener,這倆方法內(nèi)部分別調(diào)用了View.setClickable(xx)/View.setLongClickable(xx)方法

(2)
通常來說,內(nèi)容區(qū)域不變,擴(kuò)大View的點(diǎn)擊區(qū)域有兩種方法:

1、設(shè)置padding
2、設(shè)置TouchDelegate

簡(jiǎn)單說說TouchDelegate,顧名思義,Touch事件代理。使用方法如下:

        //view為待擴(kuò)大的點(diǎn)擊區(qū)域
        view.post(new Runnable() {
            @Override
            public void run() {
                Rect areaRect = new Rect();
                //獲取原本的區(qū)域
                view.getHitRect(areaRect);
                //將區(qū)域擴(kuò)大
                areaRect.left -= 100;
                areaRect.top -= 100;
                areaRect.right += 100;
                areaRect.bottom += 100;
                View parentView = (View)view.getParent();
                //設(shè)置代理,當(dāng)點(diǎn)擊坐標(biāo)落在areaRect之內(nèi)時(shí),事件優(yōu)先交給view處理
                TouchDelegate touchDelegate = new TouchDelegate(areaRect, view);
                //給父布局設(shè)置代理,事件流轉(zhuǎn):子布局的onTouchEvent->父布局的onTouchEvent->落在擴(kuò)大的區(qū)域內(nèi)->將坐標(biāo)值更改->子布局的dispatchTouchEvent->子布局onTouchEvent
                parentView.setTouchDelegate(touchDelegate);
            }
        });

(3)
PerformClick
最終調(diào)用到

    public boolean performClick() {
        ...
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            //聲音反饋
            playSoundEffect(SoundEffectConstants.CLICK);
            //熟知的onClick
            li.mOnClickListener.onClick(this);
            //只要執(zhí)行了onClick,就認(rèn)為已經(jīng)處理了事件
            result = true;
        } else {
            result = false;
        }
        ...
        return result;
    }

我們平時(shí)給View設(shè)置的點(diǎn)擊事件:

 public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

就是此時(shí)回調(diào)的。
(4)
來看看長(zhǎng)按事件
最終調(diào)用CheckForLongPress里的Run方法

    public void run() {
        if ((mOriginalPressedState == isPressed()) && (mParent != null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
            recordGestureClassification(mClassification);
            if (performLongClick(mX, mY)) {
                //記錄長(zhǎng)按事件已經(jīng)處理了
                mHasPerformedLongPress = true;
            }
        }
    }
    private boolean performLongClickInternal(float x, float y) {
       sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            //執(zhí)行 setOnLongClickListener(xx) 注冊(cè)的回調(diào)
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        ...
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }

長(zhǎng)按和短按的區(qū)別

  • 重寫onLongClick(View v),其返回值決定是否執(zhí)行onClick(View v)方法,若是返回true表示已經(jīng)處理長(zhǎng)按事件,短按無需處理了
  • 重寫onClick(View v),該方法沒有返回值,執(zhí)行了該方法就不會(huì)執(zhí)行onLongClick(View v)

用圖表示View事件分發(fā)流程:


image.png

View 事件分發(fā)常用方法

image.png

值得注意的是onTouch(xx)回調(diào)和短按操作區(qū)別:

短按是在收到Up事件后觸發(fā)的,而onTouch(xx)則是只要收到事件就會(huì)觸發(fā)

ViewTree 事件分發(fā)

以上分別分析了ViewGroup、View的事件分發(fā)流程,而眾多ViewGroup、View組成了ViewTree結(jié)構(gòu)。將父、子布局的dispatchTouchEvent、onTouchEvent關(guān)聯(lián)起來,如圖:


image.png

從中可以看出:

  • 若是父布局dispatchTouchEvent處理了事件,那么子布局的dispatchTouchEvent將收不到事件
  • 若是子布局的onTouchEvent處理了事件,那么父布局的onTouchEvent將收不到事件

ViewGroup/View 事件分發(fā)難點(diǎn)在:

弄清楚子布局/父布局、View/ViewGroup繼承關(guān)系

事件分發(fā)系列總結(jié)

Android事件分發(fā)系列文章分了三篇來講述
Android 輸入事件一擼到底之源頭活水(1)
分析了App層從底層收到事件后ViewRootImpl.java的處理

Android 輸入事件一擼到底之DecorView攔路虎(2)
分析了DecorView對(duì)事件的處理

Android 輸入事件一擼到底之View接盤俠(3)
前面事件沒有處理,流轉(zhuǎn)到此處進(jìn)行處理后就完成了使命,這也就是為什么本篇文章叫做"View接盤俠的原因"

網(wǎng)上很多文章將DecorView與View/ViewGroup事件處理一起講解,沒有明確指出兩者之間的差異。通過本系列文章,我們知道DecorView對(duì)事件的處理并不是必須的,只有使用了DecorView作為RootView才特殊處理(比如Activity、Dialog等)。
將三者串聯(lián)起來:


image.png

一般來說,我們常接觸到的就是第二部分、第三部分,尤其是第三部分常用。
建議將這三部分對(duì)應(yīng)的文章結(jié)合起來看,局部---->整體---->局部,這樣對(duì)整個(gè)事件分發(fā)有個(gè)更清晰的認(rèn)識(shí)。
本文基于 Android 10.0 源碼

如果您喜歡,請(qǐng)點(diǎn)贊,您的鼓勵(lì)是我前進(jìn)的動(dòng)力。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容