Android View 事件體系筆記(二):View 事件分發(fā)機制

Android View 事件體系筆記(二).png

聲明:本文內(nèi)容依據(jù)《Android開發(fā)藝術(shù)探索》的思路赂鲤,基于 API 26 進行總結(jié)

一霜定、View 事件分發(fā)機制概覽

1.1 點擊事件傳遞規(guī)則

定義:所謂點擊事件的事件分發(fā)漓滔,其實就是對 MotionEvent 事件的分發(fā)過程锈津。即當(dāng)一個 MotionEvent 產(chǎn)生之后藏姐,系統(tǒng)需要把這個事件傳遞給一個具體的 View隆箩。
關(guān)于MotionEvent對象的含義及參數(shù)可以參考上一篇文章。

點擊事件的分發(fā)由三個重要的方法共同完成:

  • public boolean dispatchTouchEvent(MotionEvent ev) (dispatch:處理)
    用來進行事件的分發(fā)羔杨。如果事件能夠傳遞給當(dāng)前 View 捌臊,一定會調(diào)用此方法,返回結(jié)果受當(dāng)前 View 的 onTouchEvent 和下級 View 的 dispatchTouchEvent 方法的影響兜材,表示是否消費當(dāng)前事件理澎。
  • public boolean onInterceptTouchEvent(MotionEvent event) (Intercept:攔截)
    用來判斷是否攔截某事件,如果當(dāng)前 View 攔截了某事件曙寡,那么在同一事件序列當(dāng)中糠爬,此方法不會被再次調(diào)用,返回結(jié)果表示是否攔截當(dāng)前事件举庶。
  • public boolean onTouchEvent(MotionEvent event)
    在 dispatchTouchEvent 方法中調(diào)用执隧,用來處理點擊事件,返回結(jié)果表示是否消費當(dāng)前事件户侥,如果不消費镀琉,則在同一個事件序列中,當(dāng)前 View 無法再次接收到事件蕊唐。

偽代碼表明三個方法的關(guān)系:

// 事件傳遞其實是MotionEvent對象的傳遞
public boolean dispatchTouchEvent(MotionEvent event){
    // 標(biāo)記是否消費事件
    boolean consume = false;
    // 判斷當(dāng)前 View 是否攔截此事件屋摔,攔截則進一步處理
    if(onInterceptTouchEvent(event)){
        consume = onTouchEvent(event);
    }else {
        // 不攔截則傳遞到子 View 接著判定
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}
三個重要方法.png

View 設(shè)置 OnTouchListener,回調(diào) onTouch 方法替梨,如果 onTouch 返回 false凡壤,則當(dāng)前 View 的 onTouchEvent 方法會被調(diào)用,如果 true耙替,則被攔截亚侠。說明 OnTouchListener 優(yōu)先級高于 onTouchEvent 。
在 onTouchEvent 方法中俗扇,如果當(dāng)前設(shè)置的有 OnClickListener硝烂,那么它的 onClick 方法會被調(diào)用。OnClickListener 優(yōu)先級最低铜幽,處于事件傳遞的尾端滞谢。

1.2 點擊事件傳遞順序

順序: Activity --> ViewGroup --> View串稀。
這里借用一張圖來說明:

事件分發(fā)流程圖.png

簡單解釋一下這張圖:

  • 三個重要角色 Activity、ViewGroup狮杨、View
  • 三個角色包含兩個重要函數(shù):dispatchTouchEvent() 和 onTouchEvent()母截,ViewGroup 多了一個 onInterceptTouchEvent() 用來判斷是否攔截當(dāng)前事件
  • dispatchTouchEvent() 用來傳遞點擊事件,onInterceptTouchEvent() 用來判斷是否攔截當(dāng)前事件橄教,onTouchEvent() 用來處理是否消費當(dāng)前事件
  • 如果 dispatchTouchEvent() 返回 false 則傳遞給上級 View 或 Activity清寇,如果 true 則傳遞事件
  • 如果某 View 的 onTouchEvent 返回 false,則它的父容器的 onTouchEvent 會被調(diào)用护蝶,依此類推华烟。如果都返回 false,交給 Activity 處理持灰。

二盔夜、基于 Android 8.0 源碼分析View 事件分發(fā)機制

2.1 事件最先傳遞給 Activity,由其 dispatchTouchEvent 進行派發(fā)

也就是上面流程圖的第一部分


ActivityToViewGroup.png

android.app.Activity.java

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            // 空方法
            onUserInteraction();
        }
        // 事件交給 Activity 所屬的 Window 進行分發(fā)堤魁,返回 true 事件結(jié)束
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        // 如果沒有 View 處理事件栈拖,調(diào)用 Activity 的 onTouchEvent
        return onTouchEvent(ev);
    }
2.2 Window 會將事件傳遞給 decor view刷喜,一般是當(dāng)前頁面的底層容器(setContentView 所設(shè)置的 View 的父容器)通過 Activity.getWindow.getDecorView() 可以獲得删豺。

(1)Window 是一個抽象類谦屑,其 superDispatchTouchEvent 也是一個抽象方法。所以要找到其實現(xiàn)類涛漂。

Window 類源碼描述(API 26).png

通過 Window 源碼描述可知其實現(xiàn)類為 PhoneWindow赏表,描述大意為:Window 類可以控制頂級 View 的外觀和行為策略,它的唯一實現(xiàn)位于 android.view.PhoneWindow 中匈仗,當(dāng)你要實例化這個 Window 類的時候瓢剿,你并不知道它的細節(jié),因為這個類會被重構(gòu)悠轩,只有一個工廠方法可以使用间狂。
(2)PhoneWindow 類是 Window 唯一的實現(xiàn)類,前者直接將事件傳遞給了 DecorView

PhoneWindow 類 package com.android.internal.policy;

@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
    return mDecor.superDispatchKeyEvent(event);
}

PhoneWindow 類的 mDecor 對象

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    ...
    @Override
    public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }

通過 ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0); 可以獲取 Activity 所設(shè)置的 View火架,這個 mDecor 就是 getWindow().getDecorView() 返回的 View鉴象,通過 setContentView 設(shè)置的 View 是它的一個子 View。目前事件由 mDecor.superDispatchKeyEvent(event) 傳遞到了 DecorView 這里何鸡。

(3)DecorView 是整個Window界面的最頂層View纺弊,包含通知欄,標(biāo)題欄骡男,內(nèi)容顯示欄(就是 Activity setContentView 設(shè)置的布局 View)三塊區(qū)域

public class DecorView extends FrameLayout

    public boolean superDispatchKeyEvent(KeyEvent event) {
        ...
        // 傳遞給父類的 dispatchKeyEvent
        return super.dispatchKeyEvent(event);
    }

DecorView 的父類為 FrameLayout 且是父 View淆游,事件會一層層傳遞最終會傳遞給 View。到這時,事件已經(jīng)傳遞到頂級 View 了犹菱,也就是在 Activity 中通過 setContentView 設(shè)置的 View拾稳。頂級 View 也叫根 View,一般來說都是 ViewGroup腊脱。

2.3 頂級 View 對點擊事件的分發(fā)過程

點擊事件達到頂級 View (一般是一個 ViewGroup)以后访得,會調(diào)用 ViewGroup 的 dispatchTouchEvent 方法,對應(yīng)流程圖為:

ViewGroup Start.png

之后的邏輯是這樣的:
如果頂級 ViewGroup 攔截事件即 onInterceptTouchEvent 返回 true陕凹,則事件由 ViewGroup 處理悍抑,這時如果 ViewGroup 的 mOnTouchListener 被設(shè)置,則 onTouch 會被調(diào)用捆姜,否則 onTouchEvent 會被調(diào)用传趾。
在 onTouchEvent 中迎膜,如果設(shè)置了 mOnClickListener泥技,則 onClick 會被調(diào)用。
如果頂級 View 不攔截事件磕仅,則事件會傳遞給子 View 并調(diào)用其 dispatchTouchEvent珊豹,接下來會一直處理到結(jié)束。
ViewGroup 分發(fā)過程.png

package android.view; ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {...} 方法源碼

(1)當(dāng)前 ViewGroup 是否攔截點擊事件

// 檢查攔截榕订。
final boolean intercepted;
// Step 1 判斷按下或者mFirstTouchTarget:
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    // Step 2 判斷 mGroupFlags 和 FLAG_DISALLOW_INTERCEPT
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // 動作改變時重置事件狀態(tài)
    } else {
        intercepted = false;
    }
} else {
    // 沒有觸摸目標(biāo)且不是最初的按下事件店茶,所以
    // 該 ViewGroup 繼續(xù)處理事件
    intercepted = true;
}

Step 1 判斷按下或者mFirstTouchTarget:if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null): 判斷當(dāng)前動作是否為 MotionEvent.ACTION_DOWN 按下,或者 mFirstTouchTarget != null劫恒。mFirstTouchTarget 通過查看后面邏輯可以看出指的是如果事件由 ViewGroup 的子元素成功處理贩幻,則 mFirstTouchTarget 會被賦值并指向子元素。
也就是說两嘴,當(dāng) ViewGroup 不攔截事件并將事件交給子 View 去成功處理丛楚,ViewGroup 不會再處理除 MotionEvent.ACTION_DOWN 以外的事件。因為此時 mFirstTouchTarget 的值不為 null憔辫,同時 MotionEvent.ACTION_MOVE 和 MotionEvent.ACTION_UP 不會再經(jīng)歷 ViewGroup 的 onInterceptTouchEvent(ev) 方法趣些。
Step 2 判斷 mGroupFlags 和 FLAG_DISALLOW_INTERCEPT: mGroupFlags 參數(shù)不用理會。FLAG_DISALLOW_INTERCEPT 標(biāo)記一旦被設(shè)置贰您,ViewGroup 將無法攔截除 ACTION_DOWN 以外的事件坏平。這個標(biāo)記的設(shè)置一般是子 View 通過 ViewGroup 的 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {...} 方法來設(shè)置的。每次按下以后都會清除和重置標(biāo)記:

// 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);
    resetTouchState();
}

這個方法是在第(1)步之前的锦亦,由于每次重新按下都會清除標(biāo)記舶替,所以 ViewGroup 總是能調(diào)用onInterceptTouchEvent來判斷是否攔截事件。如果事件不為 ACTION_DOWN 且其子 View 設(shè)置了標(biāo)記杠园,ViewGroup 就不會再攔截事件而是直接交給子 View 去處理顾瞪。
結(jié)論:

  • 當(dāng) ViewGroup 決定攔截事件,則后續(xù)不會再調(diào)用 onInterceptTouchEvent 方法,因為 Step 1中條件 actionMasked 玲昧!= MotionEvent.ACTION_DOWN 并且 mFirstTouchTarget == null 故而不會再跑里面的方法栖茉。
  • FLAG_DISALLOW_INTERCEPT 標(biāo)記的作用是讓 ViewGroup 不再攔截事件,當(dāng)然前提是 ViewGroup 不攔截 ACTION_DOWN 事件孵延÷榔可以利用這個方法去解決滑動沖突。

(2)ViewGroup 不攔截事件向下分發(fā)的過程

if (newTouchTarget == null && childrenCount != 0) {
    // Step 1:獲取點擊坐標(biāo)位置
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    // Find a child that can receive the event.
    // Scan children from front to back.
    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
     // Step 2:遍歷 ViewGroup 所有子元素
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);

        // 如果有一個子 View 可以被點擊并且我們需要它獲得點擊事件尘应,
        // 會讓它首先獲得點擊事件惶凝,如果該 View 不處理則會執(zhí)行正常的調(diào)度。
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }
        // Step 3:判斷是否可以接收點擊事件(判斷方法為是否在執(zhí)行動畫)犬钢,
        // 或者坐標(biāo)是否坐落在子元素的區(qū)域內(nèi)
        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        newTouchTarget = getTouchTarget(child);
        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);
         // Step 4:進行判斷苍鲜,如果 child 不為 null,則 child 進行 dispatchTouchEvent玷犹。反之混滔,則調(diào)用父 View 的 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();
            // Step 5:結(jié)合上方 if 返回 true 表示子元素的 dispatchTouchEvent 處理成功歹颓,
            // mFirstTouchTarget 就會被賦值同時跳出 for 循環(huán) 
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

        // The accessibility focus didn't handle the event, so clear
        // the flag and do a normal dispatch to all children.
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
}

Step 1: 只是記錄點擊的坐標(biāo)坯屿。
Step 2: 遍歷 ViewGroup 所有子元素。
Step 3: canViewReceivePointerEvents 該子 View 是否正在播放動畫巍扛。

    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }

isTransformedTouchPointInView 判斷點擊是否在該子 View 的區(qū)域內(nèi)领跛。
如果子元素滿足這兩個條件,那么事件就會傳遞給它來進行處理撤奸。
Step 4: dispatchTransformedTouchEvent 方法包含下列語句吠昭,也就是根據(jù) child view 是否為 null 來決定是否將事件傳遞給父 View。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            ...
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
}

Step 5: addTouchTarget 方法主要是為 mFirstTouchTarget 賦值胧瓜,表面該事件已經(jīng)交給子 View 進行處理矢棚。如果 mFirstTouchTarget 為 null,ViewGroup 就會默認攔截接下來同一序列的所有點擊事件贷痪。

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

如果所有的子元素都沒有處理事件幻妓,包含兩種情況:第一是 ViewGroup 沒有子元素,第二是子元素處理了點擊事件劫拢,但是在 dispatchTouchEvent 中返回了 false肉津。在這樣的情況下 ViewGroup 會接著處理:

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

如果 mFirstTouchTarget 為 null,dispatchTransformedTouchEvent 傳遞的第三個參數(shù)為 null舱沧,也就是說會調(diào)用 ViewGroup 父類的 dispatchTouchEvent妹沙。

2.4 View 對點擊事件的處理過程
View 開始處理

package android.view; --> View 源碼片段

(1)dispatchTouchEvent 方法

public boolean dispatchTouchEvent(MotionEvent event) {
      ...
      boolean result = false;
      ...
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        // Step 1:判斷有沒有設(shè)置 OnTouchListener
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        // Step 2:上方 onTouch 返回 true,result 為 true 則不會調(diào)用 onTouchEvent
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    return result;
}

Step 1:首先判斷有沒有設(shè)置 OnTouchListener熟吏,如果設(shè)置了并且 onTouch 返回 true距糖,則標(biāo)記 result 為 true玄窝,說明 onTouch 消費了事件。
Step 2: result 為 true 則不會調(diào)用 onTouchEvent悍引,如果標(biāo)記 result 為 false 并且 onTouchEvent 返回 true恩脂,說明該 View 消費了點擊事件,最后標(biāo)記設(shè)置為 true 并返回告知父 View 事件已經(jīng)被消費了趣斤。

(2)onTouchEvent 方法
首先獲取當(dāng)前 View 的狀態(tài) clickable(是否可點擊)俩块,然后再進行判定如果該 View 的狀態(tài)為 DISABLED(不可用)最后再返回 clickable。有一句重要的注釋:一個可點擊的不可用的 View 依舊會消費點擊事件浓领,它只是不作響應(yīng)而已玉凯。說明不可用的 View 依舊消費點擊事件。

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    return clickable;
}

接下來判定是否設(shè)置了代理联贩,這里的 onTouchEvent 的工作機制看起來和 OnTouchListener 類似漫仆,不深入研究。

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

接下來看 onTouchEvent 對點擊事件的具體處理

// 如果 View 可點擊或者可顯示懸浮或長按的工具提示
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // 如果是懸浮工具窗的操作就另行處理
            if ((viewFlags & TOOLTIP) == TOOLTIP) {
                handleTooltipUp();
            }
            // 如果 View不可點擊移除各種點擊回調(diào)并跳出
            if (!clickable) {
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;
            }
            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) {
                    // 按鈕在釋放之前我們確實顯示了它的點擊效果泪幌。
                    // 使該按鈕顯示按下的狀態(tài)以確保用戶能夠看到盲厌。
                    setPressed(true, x, y);
                }

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // 通過mHasPerformedLongPress得知這不是長按事件
                    // 說明這是一個單擊事件,所以移除長按檢測
                    removeLongPressCallback();

                    // 如果處于按下狀態(tài)只執(zhí)行點擊操作
                    if (!focusTaken) {
                        // 使用一個 Runnable 的 post 方式來調(diào)用 performClick 好過直接調(diào)用座菠。
                        // 這樣不影響該 View 除了點擊外的其他的狀態(tài)的更新
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            // *重要方法狸眼,用來判定是否設(shè)置了 OnClickListener 再進行一系列處理
                            performClick();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    // If the post failed, unpress right now
                    mUnsetPressedState.run();
                }
                // 處理觸摸回調(diào)
                removeTapCallback();
            }
            mIgnoreNextUpEvent = false;
            break;

        case MotionEvent.ACTION_DOWN:
            ...
            break;

        case MotionEvent.ACTION_CANCEL:
            ...
            break;

        case MotionEvent.ACTION_MOVE:
            ...
            break;
    }

    return true;
}

經(jīng)過了一系列的判定后來到了一個重要方法 performClick()藤树,這個方法來處理具體的 OnClick 回調(diào)等邏輯操作浴滴。以下是 performClick 方法:

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        // 存在OnClickListener,播放點擊音效
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

到這里點擊事件分發(fā)到了 onClick 函數(shù)岁钓,接下來就是我們自己去 onClick 方法中去實現(xiàn)邏輯操作了升略。
View 的 LONG_CLICKABLE 屬性默認為 falseCLICKABLE 屬性依據(jù)是否可點擊來確定屡限。通過 setLongClickablesetClickable 分別可以改變兩個屬性的值品嚣,另外,通過 setOnClickListener 會自動將 View 的 CLICKABLE 屬性設(shè)置為 true钧大,相關(guān)源碼:

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

到這里事件傳遞機制就基本結(jié)束了翰撑。

三、總結(jié)

事件傳遞機制的一些結(jié)論:

  • 同一事件序列是指手指接觸屏幕到離開屏幕中產(chǎn)生的一系列事件啊央。以 down 開始眶诈,中間一些 move,最后以 up 事件結(jié)束瓜饥。(按下滑啊滑再松開)
  • 正常情況下一個事件序列只能被一個 View 攔截且消費逝撬。但是可以通過 onTouchEvent 強行傳遞給其他 View 處理。(這個事我承包了乓土,但是我可以強行給你)
  • 某個 View 一旦決定攔截宪潮,那么這一事件序列都只能由它來處理(如果能傳遞給它的話)溯警,并且它的 onInterceptTouchEvent 不會再被調(diào)用。(這事我承包了狡相,不用再問了)
  • 某個 View 一旦開始處理事件梯轻,如果不消費 ACTION_DOWN 事件(onTouchEvent 返回 false),那么同一事件序列中其他事件都不會交給他處理尽棕,并且事件重新交給它的父元素去處理檩淋,即父元素的 onTouchEvent 會被調(diào)用。(最開始的事情你都不做萄金,后面的就不給你了蟀悦,交給你上面的人去做)
  • 如果 View 不消費除 ACTION_DOWN 以外的其他事件,那么這個點擊事件會消失氧敢,此時父元素的 onTouchEvent 不會被調(diào)用日戈,并且當(dāng)前 View 可以持續(xù)收到后續(xù)事件,最終消失的事件會傳遞給 Activity 處理孙乖。(我只答應(yīng)做開頭的一點事情浙炼,后面可以通知我,也不用告訴我上級唯袄,后面的事情你們自己處理)
  • ViewGroup 默認不攔截任何事件弯屈。Android 源碼 ViewGroup 的 onInterceptTouchEvent 默認返回 false。(我恋拷,ViewGroup资厉,小弟多。事情都給下面做)
  • View 沒有 onInterceptTouchEvent 方法蔬顾,一旦有點擊事件傳遞宴偿,調(diào)用 onTouchEvent 方法。(View 的具體實現(xiàn)是最底層小弟诀豁,不用問我做不做事窄刘,當(dāng)然做)
  • View 的 onTouchEvent 默認都會消費事件(返回true),除非它是不可點擊的 (clickable 和 longClickable 同時為 false)舷胜。View 的 longClickable 屬性默認 false娩践,click 看情況。Button 的 clickable 默認為 true烹骨,TextView 的 clickable 默認為 false翻伺。
  • View 的 enable 屬性不影響 onTouchEvent 的默認返回值。哪怕 View 的 enable 為 false展氓,只要它的 clickable 或者 longClickable 有一個為 true穆趴,那么它的 onTouchEvent 就返回 true。
  • onClick 發(fā)生的前提是 View 可點擊遇汞,并且收到了 down 和 up 的事件未妹。
  • 事件傳遞是由外向內(nèi)的簿废,先傳遞給父元素再由父元素分發(fā)給子 View,通過 requestDisallowInterceptTouchEvent (好長的方法名络它,直譯為:申請不允許攔截點擊事件族檬,也就是干擾父元素消費事件)方法可以干預(yù)父元素的事件分發(fā)過程,但是 ACTION_DOWN 事件除外化戳。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末单料,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子点楼,更是在濱河造成了極大的恐慌扫尖,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掠廓,死亡現(xiàn)場離奇詭異换怖,居然都是意外死亡,警方通過查閱死者的電腦和手機蟀瞧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門沉颂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人悦污,你說我怎么就攤上這事铸屉。” “怎么了切端?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵彻坛,是天一觀的道長。 經(jīng)常有香客問我帆赢,道長小压,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任椰于,我火速辦了婚禮,結(jié)果婚禮上仪搔,老公的妹妹穿的比我還像新娘瘾婿。我一直安慰自己,他們只是感情好烤咧,可當(dāng)我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布偏陪。 她就那樣靜靜地躺著,像睡著了一般煮嫌。 火紅的嫁衣襯著肌膚如雪笛谦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天昌阿,我揣著相機與錄音饥脑,去河邊找鬼恳邀。 笑死,一個胖子當(dāng)著我的面吹牛灶轰,可吹牛的內(nèi)容都是我干的谣沸。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼笋颤,長吁一口氣:“原來是場噩夢啊……” “哼乳附!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起伴澄,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤赋除,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后非凌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贤重,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年清焕,在試婚紗的時候發(fā)現(xiàn)自己被綠了并蝗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡秸妥,死狀恐怖滚停,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情粥惧,我是刑警寧澤键畴,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站突雪,受9級特大地震影響起惕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咏删,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一惹想、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧督函,春花似錦嘀粱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至宛篇,卻和暖如春娃磺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叫倍。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工偷卧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留豺瘤,地道東北人。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓涯冠,卻偏偏與公主長得像炉奴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蛇更,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,884評論 2 354

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