【Android源碼】View的事件分發(fā)

目錄:

QWQIX7REKZF%38DNA$G%(GP.png

一、 setContentView

Activity中setContentView的源碼玖像,主要作用就是:

生成DecorView,并把通過Activity的theme和Java代碼里面設(shè)置的Feature匹配得到的layoutResource與DecorView綁定,然后再把我們傳進去的layoutResId添加到DecorView上捐寥,最后再添加一個onContentChange()的回調(diào)

二笤昨、 View的事件分發(fā)機制

用戶的點擊事件會被系統(tǒng)封裝成一個類:MotionEvent,當(dāng)MotionEvent產(chǎn)生后握恳,就會在各層View之間傳遞瞒窒,這個傳遞過程就是點擊事件分發(fā)

其中,事件分發(fā)其主要作用的是三個方法:

  • dispatchTouchEvent() —— 進行事件分發(fā)
  • onInterceptTouchEvent() —— 進行事件攔截乡洼,在dispatchTouchEvent()中調(diào)用崇裁,但View沒有提供這個方法(因為View一般就是最后一層,此時不必再對事件進行攔截束昵,事件不會再繼續(xù)傳遞下去)
  • onTouchEvent() —— 用來處理點擊事件拔稳,在dispatchTouchEvent()中進行調(diào)用

1. View的事件分發(fā)機制

產(chǎn)生點擊事件后,事件會從Activity ==> PhoneWindow ==> DecorView => ViewGroup ==> (...ViewGroup...) ==> View

事件就這樣一層一層的從上往下傳遞锹雏,我們直接從ViewGroup的dispatchTouchEvent()開始看巴比。

ViewGroup.java

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    //...
    // 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);
        //初始化之前的狀態(tài)
        resetTouchState();
    }

    // Check for interception.
    final boolean intercepted;
    //這里使用了mFirstTouchTarget這個屬性
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
    //...
    return handled;
}

在dispatchTouchEvent()方法里,會先事件進行判斷礁遵,看看是否為DOWN轻绞,如果是,則調(diào)用resetTouchState()方法佣耐。因為ACTION_DOWN事件是新事件的開始铲球,所以需要調(diào)用resetTouchState()方法初始化之前的狀態(tài)

private void resetTouchState() {
    clearTouchTargets();
    //...
}

private void clearTouchTargets() {
    TouchTarget target = mFirstTouchTarget;
    if (target != null) {
        do {
            TouchTarget next = target.next;
            target.recycle();
            target = next;
        } while (target != null);
        mFirstTouchTarget = null;
    }
}

這里會對mFirstTouchTarget進行賦null值的操作。而mFirstTouchTarget是用來標(biāo)記當(dāng)前ViewGroup是否攔截了事件晰赞,如果攔截:mFirstTouchTarget=null稼病,如果不攔截并交給子View來處理:mFirstTouchTarget!=null

而mFirstTouchTarget在dispatchTouchEvent()方法中繼續(xù)用來作為判斷,==假設(shè)此時事件被攔截了==掖鱼,那么mFirstTouchTarget != null為false然走,如果當(dāng)前點擊事件為ACTION_DOWN,那么就會就會調(diào)用onInterceptTouchEvent()方法戏挡,如果當(dāng)前事件是ACTION_UPACTION_MOVE芍瑞,那么就會直接執(zhí)行intercepted = true;,此后的事件都將交由這個ViewGroup處理

//當(dāng)事件被攔截時褐墅,即 mFirstTouchTarget != null 為false拆檬,此時只有ACTION_DOWN事件,才會去調(diào)用onInterceptTouchEvent()方法
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    //FLAG_DISALLOW_INTERCEPT:留給子View去禁止ViewGroup攔截
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent()(ev);
        ev.setAction(action); // restore action in case it was changed
    } else {
        intercepted = false;
    }
}  else {
    //事件被攔截時妥凳,ACTION_UP和ACTION_MOVE都將會進到這里
    intercepted = true;
}

 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())) {
         return true;
     }
     //默認(rèn)返回false
     return false;
 }

除了ViewGroup自己去攔截ACTION_UPACTION_MOVE事件外竟贯,子View還可以通過FLAG_DISALLOW_INTERCEPT標(biāo)志位來禁止ViewGroup攔截ACTION_UPACTION_MOVE事件,途徑就是通過子View調(diào)用requestDisallowInterceptTouchEvent()這個方法來改變標(biāo)記位

onInterceptTouchEvent()方法默認(rèn)返回false逝钥,不攔截事件屑那,如果想要攔截,可以自定義ViewGroup重寫這個方法

我們繼續(xù)回到ViewGroup的dispatchTouchEvent()上面來,看看事件是如何被分發(fā)給子View的

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    //...
    final View[] children = mChildren;
    //遍歷子元素(倒序遍歷)持际,從最上層的子View往內(nèi)層遍歷
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);

        //判斷子View是否能獲取到點擊事件
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                //子View能接收到點擊事件點擊事件沃琅,交由子View處理
                continue;
            }
            childWithAccessibilityFocus = null;
            //雙重迭代
            i = childrenCount - 1;
        }

        //判斷觸摸點位置是否在子View的范圍內(nèi),或者子View是否在播放動畫
        if (!child.canReceivePointerEvents()
            || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            //該子View不符合上面兩個條件蜘欲,開始遍歷下一個
            continue;
        }
    }
    //...

    resetCancelNextUpFlag(child);
    //dispatchTransformedTouchEvent()方法里面會對View的事件分發(fā)進行處理
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
        //...
    }
    ev.setTargetAccessibilityFocus(false);
    //...
    return handled;
}


private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        //如果有子View益眉,就去調(diào)用子View的dispatchTouchEvent()方法,如果沒有子View姥份,就去調(diào)用ViewGroup的父類——View的dispatchTouchEvent方法
        if (child == null) {
            //調(diào)用父類——View里面的dispatchTouchEvent()方法
            handled = super.dispatchTouchEvent(event);
        } else {
            //調(diào)用子View里面的dispatchTouchEvent()方法
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    //...
}

ViewGroup的dispatchTouchEvent()在遍歷完children之后呜叫,就開始對時間進行分發(fā),dispatchTransformedTouchEvent()里面就是對事件分發(fā)的處理殿衰,他會先去查看自己(ViewGroup)有沒有子View朱庆,有就調(diào)用自己的子View去處理,沒有就交給自己的父類——View處理闷祥,所以最終都是調(diào)用到了View去處理dispatchTouchEvent()

1)View的dispatchTouchEvent():

我們來看View.java里面的這個方法

public boolean dispatchTouchEvent(MotionEvent event) {
    
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        //把所有的監(jiān)聽事件封裝成了一個對象娱颊,這里面存放了View所有Listener信息,如onTouchListener
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED  //是否是enable,如果View設(shè)置了android:enabled="false",就都不能執(zhí)行了
                && li.mOnTouchListener.onTouch(this, event)) {  //如果你mOnTouchListener返回的是false凯砍,那么result就為false
            result = true;
        }
        //這里的onTouchEvent()能不能執(zhí)行箱硕,完全取決于onTouch()方法的返回值,所以onTouch()方法的優(yōu)先級大于onTouchEvent()
        if (!result && onTouchEvent(event)) {//如果result = false悟衩,就會執(zhí)行onTouchEvent剧罩,如果result = true,就不會執(zhí)行就會執(zhí)行onTouchEvent
            result = true;
        }
    }
    //返回
    return result;
}

boolean isAccessibilityFocusedViewOrHost() {
    return isAccessibilityFocused() || (getViewRootImpl() != null && getViewRootImpl()
            .getAccessibilityFocusedHost() == this);
}

static class ListenerInfo {
    protected OnFocusChangeListener mOnFocusChangeListener;
    private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;
    protected OnScrollChangeListener mOnScrollChangeListener;
    private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;
    public OnClickListener mOnClickListener;
}

這個方法里面座泳,其實就是利用 ==短路與== 的特性:當(dāng)前面的條件不符合時惠昔,就不再判斷后面的條件了,所以就通過這種方式挑势,讓enable屬性控制mOnTouchListener方法的執(zhí)行镇防,和讓result變量控制onTouchEvent的執(zhí)行

onTouch()方法之前,會先判斷View的enable屬性潮饱,如果enable被設(shè)置了android:enabled="false"来氧,那么后面的onTouch()、onTouxhEvent()香拉、onClick()都不會得到執(zhí)行啦扬。

2)View的onTouchEvent():

我們看源碼:

public boolean onTouchEvent(MotionEvent event) {
    //只要View的CLICKABLE和LONG_CLICKABLE有一個為true,clickable就會為true凫碌,那么onTouchEvent就會返回true扑毡,消耗這個事件
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();
    
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                if (mPerformClick == null) {
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
                    performClickInternal();
                }
                break;
        }
         return true;
    }
    return false;
}

private boolean performClickInternal() {
    // Must notify autofill manager before performing the click actions to avoid scenarios where
    // the app has a click listener that changes the state of views the autofill service might
    // be interested on.
    notifyAutofillManagerOnClick();

    return performClick();
}

public boolean performClick() {
    //如果view設(shè)置了click事件,那么onClick()方法就會被執(zhí)行
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        //調(diào)用點擊事件
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
    return result;
}

View的CLICKABLELONG_CLICKABLE可以通過setClickable()证鸥、setLongClickable()方法來設(shè)置僚楞,也可以通過View的setOnClickListener()勤晚、setOnLongClickListener()來設(shè)置枉层,他們會自動把View設(shè)置為CLICKABLELONG_CLICKABLE泉褐。

這里其實就解釋了,為什么我們OnTouchListener里面返回false的時候鸟蜡,因為View的onClickListener是在OnTouch.UP后面才調(diào)用的

3)覆寫onTouchEvent()

當(dāng)onTouchEvent返回true后膜赃,這個方法就沒有去調(diào)用super.onTouchEvent()方法,View內(nèi)部的onTouchEvent()方法就不能得到執(zhí)行揉忘,就不能去調(diào)用performClick()方法跳座,那么li.mOnClickListener.onClick(this);就不能執(zhí)行。所以onClickListener就不能得到執(zhí)行

所以View內(nèi)部的優(yōu)先級:enable > onTouch > onTouchEvent > onClick

2. View事件分發(fā)的傳遞規(guī)則

上面的一連串過程泣矛,我們可以歸納為幾行偽代碼

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    //onInterceptTouchEvent默認(rèn)返回false疲眷,如果我們自定義ViewGroup時,重寫了這個方法您朽,讓他返回true狂丝,那么我們就能攔截點擊事件
    if(onInterceptTouchEvent(ev)){
        //攔截點擊事件后,開始在自己內(nèi)部分發(fā)點擊事件
        result = onTouchEvent();
    }else{
        //不攔截點擊事件哗总,交由子View去處理,這個步驟重復(fù)下去几颜,最終會調(diào)用最底層的View,View是沒有子View的讯屈,所以最后就會調(diào)用View的dispatchTouchEvent()方法蛋哭,一般情況下,最終會調(diào)用最底層View的onTouchEvent()方法
        result = child.dispatchTouchEvent(ev);
    }
    return result;
}

事件從Activity ==> PhoneWindow => DecorView => ViewGroup ==> .... ==> View

如果最頂層的ViewGroup開始涮母,一直不攔截事件谆趾,事件最終會傳遞到最底層的View上面去,由于底層View叛本,沒有子View了棺妓,所以會調(diào)用View的onTouch方法。這就是事件由上向下傳遞

如果底層的View不處理這個事件炮赦,就會讓自己的onTouchEvent返回false怜跑,從這里開始,事件開始向上傳遞吠勘,如果中途的ViewGroup也不處理這個事件性芬,最終就會傳遞到頂層的ViewGroup,由頂層的ViewGroup去處理這個事件剧防。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末植锉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子峭拘,更是在濱河造成了極大的恐慌俊庇,老刑警劉巖狮暑,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異辉饱,居然都是意外死亡搬男,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門彭沼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缔逛,“玉大人,你說我怎么就攤上這事姓惑『峙” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵于毙,是天一觀的道長敦冬。 經(jīng)常有香客問我,道長唯沮,這世上最難降的妖魔是什么脖旱? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮烂翰,結(jié)果婚禮上夯缺,老公的妹妹穿的比我還像新娘。我一直安慰自己甘耿,他們只是感情好踊兜,可當(dāng)我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著佳恬,像睡著了一般捏境。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上毁葱,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天垫言,我揣著相機與錄音,去河邊找鬼倾剿。 笑死筷频,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的前痘。 我是一名探鬼主播凛捏,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼芹缔!你這毒婦竟也來了坯癣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤最欠,失蹤者是張志新(化名)和其女友劉穎示罗,沒想到半個月后惩猫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡蚜点,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年轧房,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片禽额。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡锯厢,死狀恐怖皮官,靈堂內(nèi)的尸體忽然破棺而出脯倒,到底是詐尸還是另有隱情,我是刑警寧澤捺氢,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布藻丢,位于F島的核電站,受9級特大地震影響摄乒,放射性物質(zhì)發(fā)生泄漏悠反。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一馍佑、第九天 我趴在偏房一處隱蔽的房頂上張望斋否。 院中可真熱鬧,春花似錦拭荤、人聲如沸茵臭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旦委。三九已至,卻和暖如春雏亚,著一層夾襖步出監(jiān)牢的瞬間缨硝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工罢低, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留查辩,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓网持,卻偏偏與公主長得像宜岛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子翎碑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,947評論 2 355

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