Android View事件分發(fā)機(jī)制

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

事件分化機(jī)制的對(duì)象: onTouchEvent

MotionEvent事件初探:

我們對(duì)屏幕的點(diǎn)擊郊酒,滑動(dòng)遇绞,抬起等一系的動(dòng)作都是由一個(gè)一個(gè)MotionEvent對(duì)象組成的。根據(jù)不同動(dòng)作燎窘,主要有以下三種事件類(lèi)型:
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é)束我們稱(chēng)為一個(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

舉個(gè)例子:

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)擊了按鈕");
    }
}

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


點(diǎn)擊事件

當(dāng)我們?cè)诎粹o上滑動(dòng)時(shí):


滑動(dòng)時(shí)

可以發(fā)現(xiàn)了我們常用的按鈕的onclick事件都是在ACTION_UP以后才被調(diào)用的。這和View的事件分發(fā)機(jī)制是不是有某種不可告人的關(guān)系呢舵匾?俊抵! 當(dāng)我們給button設(shè)置了OnTouchListener并重寫(xiě)了onTouch方法,方法返回值默認(rèn)為false坐梯。如果這里我們返回true徽诲,那么你會(huì)發(fā)現(xiàn)onclick方法不執(zhí)行了!這些隨著我們的深入探討吵血,結(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)共同完成:

1.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方法

2.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方法。

3.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方法
    }

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

ViewGroup是View的子類(lèi)妙同,也就是說(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ī)制.

流程圖

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

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

1.子View可以通過(guò)requestDisallowInterceptTouchEvent方法干預(yù)父View的事件分發(fā)過(guò)程(ACTION_DOWN事件除外)羊始,而這就是我們處理滑動(dòng)沖突常用的關(guān)鍵方法旱幼。關(guān)于處理滑動(dòng)沖突,我們下一篇文章會(huì)專(zhuān)門(mén)去分析突委,這里就不做過(guò)多解釋柏卤。

2.對(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ā)源碼分析

點(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類(lèi)當(dāng)中的一個(gè)抽象方法

Window類(lèi)說(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)類(lèi)是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 =false,很顯然事件已經(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)行處理盐固。

當(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é)論:

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

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

3辽狈、如果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)處理


image.png

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ù)制前面的B橄鳌U舯浴4好帧)

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)有什么疑惑了吧榛鼎。

最后再強(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)沖突蔫敲。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市炭玫,隨后出現(xiàn)的幾起案子奈嘿,更是在濱河造成了極大的恐慌,老刑警劉巖吞加,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裙犹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡衔憨,警方通過(guò)查閱死者的電腦和手機(jī)叶圃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)践图,“玉大人掺冠,你說(shuō)我怎么就攤上這事÷氲常” “怎么了德崭?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵斥黑,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我眉厨,道長(zhǎng)锌奴,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任憾股,我火速辦了婚禮鹿蜀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘服球。我一直安慰自己茴恰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布有咨。 她就那樣靜靜地躺著琐簇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪座享。 梳的紋絲不亂的頭發(fā)上婉商,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音渣叛,去河邊找鬼丈秩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛淳衙,可吹牛的內(nèi)容都是我干的蘑秽。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼箫攀,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼肠牲!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起靴跛,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤缀雳,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后梢睛,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體肥印,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年绝葡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了深碱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡藏畅,死狀恐怖敷硅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤竞膳,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布航瞭,位于F島的核電站诫硕,受9級(jí)特大地震影響坦辟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜章办,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一锉走、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧藕届,春花似錦挪蹭、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至踏兜,卻和暖如春词顾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背碱妆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工肉盹, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人疹尾。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓上忍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親纳本。 傳聞我的和親對(duì)象是個(gè)殘疾皇子窍蓝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

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