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

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


事件分發(fā)機(jī)制面試也會(huì)經(jīng)常被提及,如果你能get到要領(lǐng),并跟面試官深入的靈魂交流一下,那么一定會(huì)讓面試官對(duì)你印象深刻,拋出愛的橄欖枝想想都有點(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:手指從屏幕上松開的瞬間產(chǎn)生該事件

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

正常情況下,無論你手指在屏幕上有多么騷的操作,最終呈現(xiàn)在MotionEvent上來講無外乎下面兩種。
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并重寫了onTouch方法,方法返回值默認(rèn)為false。如果這里我們返回true,那么你會(huì)發(fā)現(xiàn)onclick方法不執(zhí)行了?。?!What?
這些隨著我們的深入探討,結(jié)論就會(huì)浮出水面!針對(duì)MotionEvent,我們先說這么多。

MotionEvent事件分發(fā)


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

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

  • public boolean dispatchTouchEvent(MotionEvent event)
    通過方法名我們不難猜測(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均沒有消費(fèi)事件,將調(diào)用父View的onTouchEvent方法
  • public boolean onInterceptTouchEvent(MotionEvent ev)
    事件攔截,當(dāng)一個(gè)ViewGroup在接到MotionEvent事件序列時(shí)候,首先會(huì)調(diào)用此方法判斷是否需要攔截。特別注意,這是ViewGroup特有的方法,View并沒有攔截方法
    返回值:是否攔截事件傳遞,返回true表示攔截了事件,那么事件將不再向下分發(fā)而是調(diào)用View本身的onTouchEvent方法。返回false表示不做攔截,事件將向下分發(fā)到子View的dispatchTouchEvent方法。
  • public boolean onTouchEvent(MotionEvent ev)
    真正對(duì)MotionEvent進(jìn)行處理或者說消費(fèi)的方法。在dispatchTouchEvent進(jìn)行調(diào)用。
    返回值:返回true表示事件被消費(fèi),本次的事件終止。返回false表示事件沒有被消費(fèi),將調(diào)用父View的onTouchEvent方法

上面的三個(gè)方法可以用以下的偽代碼來表示其之間的關(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方法
    }

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


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

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

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

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

可以看出事件的傳遞過程都是從父View到子View。

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

  • 子View可以通過requestDisallowInterceptTouchEvent方法干預(yù)父View的事件分發(fā)過程(ACTION_DOWN事件除外),而這就是我們處理滑動(dòng)沖突常用的關(guān)鍵方法。關(guān)于處理滑動(dòng)沖突,我們下一篇文章會(huì)專門去分析,這里就不做過多解釋。
  • 對(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)說過提高的方法就是READ THE FUCKING CODE!那么我們就帶大家來看一下Android對(duì)事件分發(fā)的處理方式,看是否與我們上面說的結(jié)論一致!(為方便閱讀,以下都只給出了關(guān)鍵代碼并額外添加上一些簡(jiǎn)單注釋,全部代碼請(qǐng)自行閱讀源碼)


點(diǎn)擊事件產(chǎn)生最先傳遞到當(dāng)前的Activity,由Acivity的dispatchTouchEvent方法來對(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);//沒有View可以處理,調(diào)用Activity onTouchEvent方法
    }

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

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

Window類說明
/**
 * 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的源碼有說明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,我們通過setContentView設(shè)置的View是它的子View(Activity的setContentView,最終是調(diào)用PhoneWindow的setContentView,有興趣同學(xué)可以去閱讀,這塊不是我們討論重點(diǎn))

到這里事件已經(jīng)被傳遞到我們的頂級(jí)View中,一般是ViewGroup。
那么接下來重點(diǎn)將放到ViewGroup的dispatchTouchEvent方法中。我們之前說過,事件到達(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通過
            //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;
        }
        ...
    }

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

為什么ACTION_DOWN除外?通過上述代碼我們不難發(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)交給自己處理根本沒必要再調(diào)用onInterceptTouchEvent去判斷是否攔截。

結(jié)論:

當(dāng)ViewGroup決定攔截事件后,后續(xù)事件將默認(rèn)交給它處理并且不會(huì)再調(diào)用onInterceptTouchEvent方法來判斷是否攔截。子View可以通過設(shè)置FLAG_DISALLOW_INTERCEPT標(biāo)志位來不讓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可見并且沒有播放動(dòng)畫。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ā)過程首先需要遍歷ViewGroup的所有子View,可以接收點(diǎn)擊事件的View需要滿足下面條件。
1.如果View可見并且沒有播放動(dòng)畫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)。我們前面說過,當(dāng)子View處理了點(diǎn)擊事件那么mFirstTouchTarget就不為nulll。事實(shí)上此時(shí)我們還沒有將事件分發(fā)給子View,所以正常情況下我們的newTouchTarget 此時(shí)為null

接下來關(guān)鍵來了
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法。為方便我們將代碼再一次貼到后面來

        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;
    }

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

如果在遍歷完子View以后ViewGroup仍然沒有找到事件處理者即ViewGroup并沒有子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ā)過程全部結(jié)束!

結(jié)論:

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

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

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

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


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我們需要好幾段去拆開看的長(zhǎng)篇大論而言,它就簡(jiǎn)潔多了。很明顯View是單獨(dú)的一個(gè)元素,它沒有子View,所以也沒有分發(fā)的代碼。我們需要關(guān)注的也只是上面當(dāng)中的一部分代碼。

        //如果窗口沒有被遮蓋
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            //當(dāng)前監(jiān)聽事件
            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;
            }
        }

通過上面代碼我們可以看到View會(huì)先判斷是否設(shè)置了OnTouchListener,如果設(shè)置了OnTouchListener并且onTouch方法返回了true,那么onTouchEvent不會(huì)被調(diào)用。
當(dāng)沒有設(shè)置OnTouchListener或者設(shè)置了OnTouchListener但是onTouch方法返回false則會(huì)調(diào)用View自己的onTouchEvent方法。接下來看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)出??梢钥闯黾幢鉜iew是disabled狀態(tài),依然不會(huì)影響事件的消費(fèi),只是它看起來不可用。只要CLICKABLE和LONG_CLICKABLE有一個(gè)為true,就一定會(huì)消費(fèi)這個(gè)事件,就是onTouchEvent返回true。這點(diǎn)也印證了我們前面說的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。
(沒錯(cuò)這是復(fù)制前面的?。。。?/p>

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

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ì)一開始的例子已經(jīng)沒有什么疑惑了吧。


最后再?gòu)?qiáng)調(diào)一點(diǎn),我們剛說過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)擊,也就是說setClickable失效了。關(guān)于setClickable失效問題

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的感覺?


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

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

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


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

關(guān)于我


本人,Android開發(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)系作者
【社區(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)容