一文讀懂Android View事件分發(fā)機(jī)制

Android View 雖然不是四大組件贞瞒,但其并不比四大組件的地位低。而View的核心知識(shí)點(diǎn)事件分發(fā)機(jī)制則是不少剛?cè)腴T同學(xué)的攔路虎趁曼。ScrollView嵌套R(shí)ecyclerView(或者ListView)的滑動(dòng)沖突這種老大難的問(wèn)題的理論基礎(chǔ)就是事件分發(fā)機(jī)制军浆。


事件分發(fā)機(jī)制面試也會(huì)經(jīng)常被提及,如果你能get到要領(lǐng)挡闰,并跟面試官深入的靈魂交流一下乒融,那么一定會(huì)讓面試官對(duì)你印象深刻,拋出愛(ài)的橄欖枝想想都有點(diǎn)小激動(dòng)呢摄悯。那么就讓我們從淺入深赞季,由表及里的去看事件分發(fā)機(jī)制,全方位奢驯,立體式申钩,去弄懂這個(gè)神秘的事件分發(fā)機(jī)制吧。

MotionEvent事件初探


我們對(duì)屏幕的點(diǎn)擊瘪阁,滑動(dòng)撒遣,抬起等一系的動(dòng)作都是由一個(gè)一個(gè)MotionEvent對(duì)象組成的。根據(jù)不同動(dòng)作管跺,主要有以下三種事件類型:
1.ACTION_DOWN:手指剛接觸屏幕义黎,按下去的那一瞬間產(chǎn)生該事件
2.ACTION_MOVE:手指在屏幕上移動(dòng)時(shí)候產(chǎn)生該事件
3.ACTION_UP:手指從屏幕上松開(kāi)的瞬間產(chǎn)生該事件

從ACTION_DOWN開(kāi)始到ACTION_UP結(jié)束我們稱為一個(gè)事件序列

正常情況下,無(wú)論你手指在屏幕上有多么騷的操作伙菜,最終呈現(xiàn)在MotionEvent上來(lái)講無(wú)外乎下面兩種轩缤。
1.點(diǎn)擊后抬起,也就是單擊操作:ACTION_DOWN -> ACTION_UP
2.點(diǎn)擊后再風(fēng)騷的滑動(dòng)一段距離,再抬起:ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP

public class MotionEventActivity extends BaseActivity {
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_motion_event);
        mButton = (Button) findViewById(R.id.button);
        mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        e("MotionEvent: ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        e("MotionEvent: ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        e("MotionEvent: ACTION_UP");
                        break;
                }
                return false;
            }
        });
    }

    public void click(View v) {
        e("點(diǎn)擊了按鈕");
    }
}

注:e("xxx")是BaseActivity封裝的Log顯示方法火的,具體請(qǐng)看BaseProject

當(dāng)我們單擊按鈕:



當(dāng)我們?cè)诎粹o上風(fēng)騷走位(滑動(dòng)):


細(xì)心的同學(xué)一定發(fā)現(xiàn)了我們常用的按鈕的onclick事件都是在ACTION_UP以后才被調(diào)用的壶愤。這和View的事件分發(fā)機(jī)制是不是有某種不可告人的關(guān)系呢?馏鹤!


上面代碼我們給button設(shè)置了OnTouchListener并重寫(xiě)了onTouch方法征椒,方法返回值默認(rèn)為false。如果這里我們返回true湃累,那么你會(huì)發(fā)現(xiàn)onclick方法不執(zhí)行了2取!治力!What蒙秒?
這些隨著我們的深入探討,結(jié)論就會(huì)浮出水面宵统!針對(duì)MotionEvent晕讲,我們先說(shuō)這么多。

MotionEvent事件分發(fā)


當(dāng)一個(gè)MotionEvent產(chǎn)生了以后马澈,就是你的手指在屏幕上做一系列動(dòng)作的時(shí)候瓢省,系統(tǒng)需要把這一系列的MotionEvent分發(fā)給一個(gè)具體的View。我們重點(diǎn)需要了解這個(gè)分發(fā)的過(guò)程痊班,那么系統(tǒng)是如何去判斷這個(gè)事件要給哪個(gè)View勤婚,也就是說(shuō)是如何進(jìn)行分發(fā)的呢?

事件分發(fā)需要View的三個(gè)重要方法來(lái)共同完成:

  • public boolean dispatchTouchEvent(MotionEvent event)
    通過(guò)方法名我們不難猜測(cè)涤伐,它就是事件分發(fā)的重要方法馒胆。那么很明顯,如果一個(gè)MotionEvent傳遞給了View废亭,那么dispatchTouchEvent方法一定會(huì)被調(diào)用国章!
    返回值:表示是否消費(fèi)了當(dāng)前事件《勾澹可能是View本身的onTouchEvent方法消費(fèi)液兽,也可能是子View的dispatchTouchEvent方法中消費(fèi)。返回true表示事件被消費(fèi)掌动,本次的事件終止四啰。返回false表示View以及子View均沒(méi)有消費(fèi)事件,將調(diào)用父View的onTouchEvent方法
  • public boolean onInterceptTouchEvent(MotionEvent ev)
    事件攔截粗恢,當(dāng)一個(gè)ViewGroup在接到MotionEvent事件序列時(shí)候柑晒,首先會(huì)調(diào)用此方法判斷是否需要攔截。特別注意眷射,這是ViewGroup特有的方法匙赞,View并沒(méi)有攔截方法
    返回值:是否攔截事件傳遞佛掖,返回true表示攔截了事件,那么事件將不再向下分發(fā)而是調(diào)用View本身的onTouchEvent方法涌庭。返回false表示不做攔截芥被,事件將向下分發(fā)到子View的dispatchTouchEvent方法。
  • public boolean onTouchEvent(MotionEvent ev)
    真正對(duì)MotionEvent進(jìn)行處理或者說(shuō)消費(fèi)的方法坐榆。在dispatchTouchEvent進(jìn)行調(diào)用拴魄。
    返回值:返回true表示事件被消費(fèi),本次的事件終止席镀。返回false表示事件沒(méi)有被消費(fèi)匹中,將調(diào)用父View的onTouchEvent方法

上面的三個(gè)方法可以用以下的偽代碼來(lái)表示其之間的關(guān)系。

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;//事件是否被消費(fèi)
        if (onInterceptTouchEvent(ev)){//調(diào)用onInterceptTouchEvent判斷是否攔截事件
            consume = onTouchEvent(ev);//如果攔截則調(diào)用自身的onTouchEvent方法
        }else{
            consume = child.dispatchTouchEvent(ev);//不攔截調(diào)用子View的dispatchTouchEvent方法
        }
        return consume;//返回值表示事件是否被消費(fèi)豪诲,true事件終止顶捷,false調(diào)用父View的onTouchEvent方法
    }

通過(guò)上面的介紹相信我們已經(jīng)初步了解了View事件分發(fā)的機(jī)制


接下來(lái)我們來(lái)看一下View 和ViewGroup 在事件分發(fā)的時(shí)候有什么不一樣的地方

ViewGroup是View的子類,也就是說(shuō)ViewGroup本身就是一個(gè)View屎篱,但是它可以包含子View(當(dāng)然子View也可能是一個(gè)ViewGroup)焊切,所以不難理解,上面所展示的偽代碼表示的是ViewGroup 處理事件分發(fā)的流程芳室。而View本身是不存在分發(fā),所以也沒(méi)有攔截方法(onInterceptTouchEvent)刹勃,它只能在onTouchEvent方法中進(jìn)行處理消費(fèi)或者不消費(fèi)堪侯。

上面結(jié)論先簡(jiǎn)單的理解一下,通過(guò)下面的流程圖荔仁,會(huì)更加清晰的幫助我們梳理事件分發(fā)機(jī)制

View結(jié)構(gòu)圖
View事件分發(fā)流程圖

可以看出事件的傳遞過(guò)程都是從父View到子View伍宦。

但是這里有三點(diǎn)需要特別強(qiáng)調(diào)一下

  • 子View可以通過(guò)requestDisallowInterceptTouchEvent方法干預(yù)父View的事件分發(fā)過(guò)程(ACTION_DOWN事件除外)窄俏,而這就是我們處理滑動(dòng)沖突常用的關(guān)鍵方法侨核。關(guān)于處理滑動(dòng)沖突,我們下一篇文章會(huì)專門去分析辛萍,這里就不做過(guò)多解釋遇骑。
  • 對(duì)于View(注意卖毁!ViewGroup也是View)而言,如果設(shè)置了onTouchListener落萎,那么OnTouchListener方法中的onTouch方法會(huì)被回調(diào)亥啦。onTouch方法返回true,則onTouchEvent方法不會(huì)被調(diào)用(onClick事件是在onTouchEvent中調(diào)用)所以三者優(yōu)先級(jí)是onTouch->onTouchEvent->onClick
  • View 的onTouchEvent 方法默認(rèn)都會(huì)消費(fèi)掉事件(返回true)练链,除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)翔脱,View的longClickable默認(rèn)為false,clickable需要區(qū)分情況媒鼓,如Button的clickable默認(rèn)為true届吁,而TextView的clickable默認(rèn)為false错妖。

View事件分發(fā)源碼


作為程序猿,最不想看的但是也不得不去看的就是源碼疚沐!所謂知其然也要知其所以然暂氯,神秘的大佬曾經(jīng)說(shuō)過(guò)提高的方法就是READ THE FUCKING CODE!那么我們就帶大家來(lái)看一下Android對(duì)事件分發(fā)的處理方式濒旦,看是否與我們上面說(shuō)的結(jié)論一致株旷!(為方便閱讀,以下都只給出了關(guān)鍵代碼并額外添加上一些簡(jiǎn)單注釋尔邓,全部代碼請(qǐng)自行閱讀源碼)


點(diǎn)擊事件產(chǎn)生最先傳遞到當(dāng)前的Activity晾剖,由Acivity的dispatchTouchEvent方法來(lái)對(duì)事件進(jìn)行分發(fā)。那么很明顯我們先看Activity的dispatchTouchEvent方法

Class Activity:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {//事件分發(fā)并返回結(jié)果
            return true;//事件被消費(fèi)
        }
        return onTouchEvent(ev);//沒(méi)有View可以處理梯嗽,調(diào)用Activity onTouchEvent方法
    }

通過(guò)上面的代碼我們可以發(fā)現(xiàn)齿尽,事件會(huì)給Activity附屬的Window進(jìn)行分發(fā)。如果返回true灯节,那么事件被消費(fèi)循头。如果返回false表示事件發(fā)下去卻沒(méi)有View可以進(jìn)行處理,則最后return Activity自己的onTouchEvent方法炎疆。

跟進(jìn)getWindow().superDispatchTouchEvent(ev)方法發(fā)現(xiàn)是Window類當(dāng)中的一個(gè)抽象方法

Window類說(shuō)明
/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
Class Window:
//抽象方法卡骂,需要看PhoneWindow的實(shí)現(xiàn)
public abstract boolean superDispatchTouchEvent(MotionEvent event);

Window的源碼有說(shuō)明The only existing implementation of this abstract class is
android.view.PhoneWindow
,Window的唯一實(shí)現(xiàn)類是PhoneWindow形入。那么去看PhoneWindow對(duì)應(yīng)的代碼全跨。

class PhoneWindow
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

PhoneWindow又調(diào)用了DecorView的superDispatchTouchEvent方法。而這個(gè)DecorView就是Window的頂級(jí)View亿遂,我們通過(guò)setContentView設(shè)置的View是它的子View(Activity的setContentView浓若,最終是調(diào)用PhoneWindow的setContentView,有興趣同學(xué)可以去閱讀蛇数,這塊不是我們討論重點(diǎn))

到這里事件已經(jīng)被傳遞到我們的頂級(jí)View中挪钓,一般是ViewGroup。
那么接下來(lái)重點(diǎn)將放到ViewGroup的dispatchTouchEvent方法中耳舅。我們之前說(shuō)過(guò)碌上,事件到達(dá)View會(huì)調(diào)用dispatchTouchEvent方法,如果View是ViewGroup那么會(huì)先判斷是否攔截該事件浦徊。

class ViewGroup:
    public boolean dispatchTouchEvent(MotionEvent 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);
            //清除FLAG_DISALLOW_INTERCEPT設(shè)置并且mFirstTouchTarget 設(shè)置為null
            resetTouchState();
        }
        // Check for interception.
        final boolean intercepted;//是否攔截事件
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT是子View通過(guò)
            //requestDisallowInterceptTouchEvent方法進(jìn)行設(shè)置的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //調(diào)用onInterceptTouchEvent方法判斷是否需要攔截
                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;
        }
        ...
    }

我們前面說(shuō)過(guò)子View可以通過(guò)requestDisallowInterceptTouchEvent方法干預(yù)父View的事件分發(fā)過(guò)程(ACTION_DOWN事件除外)

為什么ACTION_DOWN除外绍赛?通過(guò)上述代碼我們不難發(fā)現(xiàn)。如果事件是ACTION_DOWN辑畦,那么ViewGroup會(huì)重置FLAG_DISALLOW_INTERCEPT標(biāo)志位并且將mFirstTouchTarget 設(shè)置為null吗蚌。對(duì)于mFirstTouchTarget 我們可以先這么理解,如果事件由子View去處理時(shí)mFirstTouchTarget 會(huì)被賦值并指向子View纯出。

所以當(dāng)事件為ACTION_DOWN 或者 mFirstTouchTarget 蚯妇!=null(即事件由子View處理)時(shí)會(huì)進(jìn)行攔截判斷敷燎。具體規(guī)則是如果子View設(shè)置了FLAG_DISALLOW_INTERCEPT標(biāo)志位,那么intercepted =false箩言。否則調(diào)用onInterceptTouchEvent方法硬贯。

如果事件不為ACTION_DOWN 且事件為ViewGroup本身處理(即mFirstTouchTarget ==null)那么intercepted = true,很顯然事件已經(jīng)交給自己處理根本沒(méi)必要再調(diào)用onInterceptTouchEvent去判斷是否攔截陨收。

結(jié)論:

當(dāng)ViewGroup決定攔截事件后饭豹,后續(xù)事件將默認(rèn)交給它處理并且不會(huì)再調(diào)用onInterceptTouchEvent方法來(lái)判斷是否攔截。子View可以通過(guò)設(shè)置FLAG_DISALLOW_INTERCEPT標(biāo)志位來(lái)不讓ViewGroup攔截除ACTION_DOWN以外的事件务漩。

所以我們知道了onInterceptTouchEvent并非每次都會(huì)被調(diào)用拄衰。如果要處理所有的點(diǎn)擊事件那么需要選擇dispatchTouchEvent方法
而FLAG_DISALLOW_INTERCEPT標(biāo)志位可以幫助我們?nèi)ビ行У奶幚砘瑒?dòng)沖突

當(dāng)ViewGroup不攔截事件,那么事件將下發(fā)給子View進(jìn)行處理饵骨。

class ViewGroup:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final View[] children = mChildren;
        //對(duì)子View進(jìn)行遍歷
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(
                    childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(
                    preorderedList, children, childIndex);

            // If there is a view that has accessibility focus we want it
            // to get the event first and if not handled we will perform a
            // normal dispatch. We may do a double iteration but this is
            // safer given the timeframe.
            if (childWithAccessibilityFocus != null) {
                if (childWithAccessibilityFocus != child) {
                    continue;
                }
                childWithAccessibilityFocus = null;
                i = childrenCount - 1;
            }

            //判斷1翘悉,View可見(jiàn)并且沒(méi)有播放動(dòng)畫(huà)。2居触,點(diǎn)擊事件的坐標(biāo)落在View的范圍內(nèi)
            //如果上述兩個(gè)條件有一項(xiàng)不滿足則continue繼續(xù)循環(huán)下一個(gè)View
            if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }

            newTouchTarget = getTouchTarget(child);
            //如果有子View處理即newTouchTarget 不為null則跳出循環(huán)妖混。
            if (newTouchTarget != null) {
                // Child is already receiving touch within its bounds.
                // Give it the new pointer in addition to the ones it is handling.
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }

            resetCancelNextUpFlag(child);
            //dispatchTransformedTouchEvent第三個(gè)參數(shù)child這里不為null
            //實(shí)際調(diào)用的是child的dispatchTouchEvent方法
            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();
                //當(dāng)child處理了點(diǎn)擊事件,那么會(huì)設(shè)置mFirstTouchTarget 在addTouchTarget被賦值
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                //子View處理了事件轮洋,然后就跳出了for循環(huán)
                break;
            }
        }
    }

上面代碼是將事件分發(fā)給子View的關(guān)鍵代碼制市,需要關(guān)注的地方都加了注釋。分發(fā)過(guò)程首先需要遍歷ViewGroup的所有子View弊予,可以接收點(diǎn)擊事件的View需要滿足下面條件息堂。
1.如果View可見(jiàn)并且沒(méi)有播放動(dòng)畫(huà)canViewReceivePointerEvents方法判斷

    /**
     * Returns true if a child view can receive pointer events.
     * @hide
     */
    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }

2.點(diǎn)擊事件的坐標(biāo)落在View的范圍內(nèi)isTransformedTouchPointInView方法判斷

    /**
     * Returns true if a child view contains the specified point when transformed
     * into its coordinate space.
     * Child must not be null.
     * @hide
     */
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        //調(diào)用View的pointInView方法進(jìn)行判斷坐標(biāo)點(diǎn)是否在View內(nèi)
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

如果滿足上面兩個(gè)條件,接著我們看后面的代碼newTouchTarget = getTouchTarget(child);

    /**
     * Gets the touch target for specified child view.
     * Returns null if not found.
     */
    private TouchTarget getTouchTarget(@NonNull View child) {
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            if (target.child == child) {
                return target;
            }
        }
        return null;
    }

可以看到當(dāng)mFirstTouchTarget不為null的時(shí)候并且target.child就為我們當(dāng)前遍歷的child的時(shí)候块促,那么返回的newTouchTarget 就不為null,則跳出循環(huán)床未。我們前面說(shuō)過(guò)竭翠,當(dāng)子View處理了點(diǎn)擊事件那么mFirstTouchTarget就不為nulll。事實(shí)上此時(shí)我們還沒(méi)有將事件分發(fā)給子View薇搁,所以正常情況下我們的newTouchTarget 此時(shí)為null

接下來(lái)關(guān)鍵來(lái)了
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法斋扰。為方便我們將代碼再一次貼到后面來(lá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();
            //當(dāng)child處理了點(diǎn)擊事件,那么會(huì)設(shè)置mFirstTouchTarget 在addTouchTarget被賦值
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            //子View處理了事件啃洋,然后就跳出了for循環(huán)
            break;
        }

可以看到它被最后一個(gè)if包圍传货,如果它返回為true,那么就break跳出循環(huán)宏娄,如果返回為false則繼續(xù)遍歷下一個(gè)子View问裕。
我們跟進(jìn)dispatchTransformedTouchEvent方法可以看到這樣的關(guān)鍵邏輯

        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }

這里child是我們遍歷傳入的子View此時(shí)不為null,則調(diào)用了child.dispatchTouchEvent(event);
我們子View的dispatchTouchEvent方法返回true孵坚,表示子View處理了事件粮宛,那么我們一直提到的窥淆,mFirstTouchTarget 會(huì)被賦值,是在哪里完成的呢巍杈?
再回頭看dispatchTransformedTouchEvent則為true進(jìn)入最后一個(gè)if語(yǔ)句忧饭,有這么一句newTouchTarget = addTouchTarget(child, idBitsToAssign);

    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

沒(méi)錯(cuò),mFirstTouchTarget 就是在addTouchTarget中被賦值筷畦!到此子View遍歷結(jié)束

如果在遍歷完子View以后ViewGroup仍然沒(méi)有找到事件處理者即ViewGroup并沒(méi)有子View或者子View處理了事件词裤,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup會(huì)去處理這個(gè)事件。
從代碼上看就是我們遍歷的dispatchTransformedTouchEvent方法返回了false鳖宾。那么mFirstTouchTarget 必然為null吼砂;
在ViewGroup的dispatchTouchEvent遍歷完子View后有下面的處理。

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        }

上面的dispatchTransformedTouchEvent方法第三個(gè)child參數(shù)傳null
我們剛看了這個(gè)方法攘滩。當(dāng)child為null時(shí)帅刊,handled = super.dispatchTouchEvent(event);所以此時(shí)將調(diào)用View的dispatchTouchEvent方法,點(diǎn)擊事件給了View漂问。到此事件分發(fā)過(guò)程全部結(jié)束赖瞒!

結(jié)論:

ViewGroup會(huì)遍歷所有子View去尋找能夠處理點(diǎn)擊事件的子View(可見(jiàn),沒(méi)有播放動(dòng)畫(huà)蚤假,點(diǎn)擊事件坐標(biāo)落在子View內(nèi)部)最終調(diào)用子View的dispatchTouchEvent方法處理事件

當(dāng)子View處理了事件則mFirstTouchTarget 被賦值栏饮,并終止子View的遍歷。

如果ViewGroup并沒(méi)有子View或者子View處理了事件磷仰,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup會(huì)去處理這個(gè)事件(本質(zhì)調(diào)用View的dispatchTouchEvent去處理)

通過(guò)ViewGroup對(duì)事件的分發(fā)袍嬉,我們知道事件最終是調(diào)用View的dispatchTouchEvent來(lái)處理


View最終是怎么去處理事件的


class View:
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //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;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

上面是View的dispatchTouchEvent方法的全部代碼。相比ViewGroup我們需要好幾段去拆開(kāi)看的長(zhǎng)篇大論而言灶平,它就簡(jiǎn)潔多了伺通。很明顯View是單獨(dú)的一個(gè)元素,它沒(méi)有子View逢享,所以也沒(méi)有分發(fā)的代碼罐监。我們需要關(guān)注的也只是上面當(dāng)中的一部分代碼。

        //如果窗口沒(méi)有被遮蓋
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            //當(dāng)前監(jiān)聽(tīng)事件
            ListenerInfo li = mListenerInfo;
            //需要特別注意這個(gè)判斷當(dāng)中的li.mOnTouchListener.onTouch(this, event)條件
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //result為false調(diào)用自己的onTouchEvent方法處理
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

通過(guò)上面代碼我們可以看到View會(huì)先判斷是否設(shè)置了OnTouchListener瞒爬,如果設(shè)置了OnTouchListener并且onTouch方法返回了true弓柱,那么onTouchEvent不會(huì)被調(diào)用。
當(dāng)沒(méi)有設(shè)置OnTouchListener或者設(shè)置了OnTouchListener但是onTouch方法返回false則會(huì)調(diào)用View自己的onTouchEvent方法侧但。接下來(lái)看onTouchEvent方法:

class View:
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //1.如果View是設(shè)置成不可用的(DISABLED)仍然會(huì)消費(fèi)點(diǎn)擊事件
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        ...
        //2.CLICKABLE 和LONG_CLICKABLE只要有一個(gè)為true就消費(fèi)這個(gè)事件
        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) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        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) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    //3.在ACTION_UP方法發(fā)生時(shí)會(huì)觸發(fā)performClick()方法
                                    performClick();
                                }
                            }
                        }
                        ...
                    break;
            }
            ...
            return true;
        }
        return false;
    }

上述代碼有三個(gè)關(guān)鍵點(diǎn)分別在注釋處標(biāo)出矢空。可以看出即便View是disabled狀態(tài)禀横,依然不會(huì)影響事件的消費(fèi)屁药,只是它看起來(lái)不可用。只要CLICKABLE和LONG_CLICKABLE有一個(gè)為true柏锄,就一定會(huì)消費(fèi)這個(gè)事件者祖,就是onTouchEvent返回true立莉。這點(diǎn)也印證了我們前面說(shuō)的View 的onTouchEvent 方法默認(rèn)都會(huì)消費(fèi)掉事件(返回true),除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false)七问,View的longClickable默認(rèn)為false蜓耻,clickable需要區(qū)分情況,如Button的clickable默認(rèn)為true械巡,而TextView的clickable默認(rèn)為false刹淌。
(沒(méi)錯(cuò)這是復(fù)制前面的!<ズ摹S泄础)

ACTION_UP方法中有performClick();接下來(lái)看一下它:

class View:
    /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     */
    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

很明顯古程,如果View設(shè)置了OnClickListener蔼卡,那么會(huì)回調(diào)onClick方法。到這里相信大家對(duì)一開(kāi)始的例子已經(jīng)沒(méi)有什么疑惑了吧挣磨。


最后再?gòu)?qiáng)調(diào)一點(diǎn)雇逞,我們剛說(shuō)過(guò)View的longClickable默認(rèn)為false,clickable需要區(qū)分情況茁裙,如Button的clickable默認(rèn)為true塘砸,而TextView的clickable默認(rèn)為false。
這是默認(rèn)情況晤锥,我們可以單獨(dú)給View設(shè)置clickable屬性掉蔬,但有時(shí)候會(huì)發(fā)現(xiàn)View的setClickable方法失效了。假如我們想讓View默認(rèn)不可點(diǎn)擊矾瘾,將View的clickable設(shè)置成false女轿,在合適的時(shí)候需要可點(diǎn)擊所以我們又給View設(shè)置了OnClickListener,那么你會(huì)發(fā)現(xiàn)View默認(rèn)依然可以點(diǎn)擊壕翩,也就是說(shuō)setClickable失效了蛉迹。關(guān)于setClickable失效問(wèn)題

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

    public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

View的setOnClickListener會(huì)默認(rèn)將View的clickable設(shè)置成true。
View的setOnLongClickListener同樣會(huì)將View的longClickable設(shè)置成true戈泼。

至此,MotionEvent事件分發(fā)機(jī)制與源碼的分析已經(jīng)搞定赏僧,大家是否有g(shù)et到技能+1的感覺(jué)大猛?


接下來(lái)一篇文章將講述如何解決View滑動(dòng)沖突。

歡迎轉(zhuǎn)發(fā)淀零,請(qǐng)附帶原文鏈接

喜歡就點(diǎn)個(gè)贊吧~


如果這篇文章對(duì)你有幫助挽绩,就刷個(gè)飛機(jī)游艇,點(diǎn)個(gè)喜歡關(guān)注雙擊一波666吧驾中。
任何疑問(wèn)都?xì)g迎在下方評(píng)論區(qū)討論唉堪。

關(guān)于我


本人模聋,Android開(kāi)發(fā)蕓蕓眾生當(dāng)中的一個(gè)新手,正在學(xué)習(xí)的路上不斷爬坑唠亚!
不論文章還是代碼链方,當(dāng)然肯定有很多不夠好的地方,希望各位大神不吝賜教灶搜,我一定虛心學(xué)習(xí)祟蚀,可以評(píng)論或者在github上提issue,歡迎關(guān)注轉(zhuǎn)發(fā)割卖!轉(zhuǎn)發(fā)請(qǐng)帶上原文鏈接前酿!謝謝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鹏溯,隨后出現(xiàn)的幾起案子罢维,更是在濱河造成了極大的恐慌,老刑警劉巖丙挽,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肺孵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡取试,警方通過(guò)查閱死者的電腦和手機(jī)悬槽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)瞬浓,“玉大人初婆,你說(shuō)我怎么就攤上這事≡趁蓿” “怎么了磅叛?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)萨赁。 經(jīng)常有香客問(wèn)我弊琴,道長(zhǎng),這世上最難降的妖魔是什么杖爽? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任敲董,我火速辦了婚禮,結(jié)果婚禮上慰安,老公的妹妹穿的比我還像新娘腋寨。我一直安慰自己,他們只是感情好化焕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布萄窜。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪查刻。 梳的紋絲不亂的頭發(fā)上键兜,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音穗泵,去河邊找鬼普气。 笑死,一個(gè)胖子當(dāng)著我的面吹牛火欧,可吹牛的內(nèi)容都是我干的棋电。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼苇侵,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼赶盔!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起榆浓,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤于未,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后陡鹃,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體烘浦,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年萍鲸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了闷叉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡脊阴,死狀恐怖握侧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嘿期,我是刑警寧澤品擎,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站备徐,受9級(jí)特大地震影響萄传,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蜜猾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一秀菱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蹭睡,春花似錦衍菱、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)禽绪。三九已至蓖救,卻和暖如春洪规,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背循捺。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工斩例, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人从橘。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓念赶,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親恰力。 傳聞我的和親對(duì)象是個(gè)殘疾皇子叉谜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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