View事件分發(fā)機制

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

事件分發(fā)主要由三個方法共同完成:

public boolean dispatchTouchEvent(MotionEvent ev)

public boolean onInterceptTouchEvent(MotionEvent ev)

public boolean onTouchEvent(MotionEvent ev)

三個方法的關系:

<pre>
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
</pre>

對于一個根ViewGroup來說,點擊事件產(chǎn)生后帐偎,首先會傳遞給他,這時他dispatchTouchEvent就會被調(diào)用碾褂,如果這個ViewGroup的onInterceptTouchEvent的返回值為true表示它要攔截當前事件雀瓢,事件就會交給這個ViewGroup的onTouchEvent處理,如果onInterceptTouchEvent的返回值為false表示不攔截當前事件契邀,當前事件會繼續(xù)傳遞給他的子元素偶房,子元素的dispatchTouchEvent會被調(diào)用趁曼,如此反復直到事件被最終處理。

當一個View需要處理事件時棕洋,如果設置了OnTouchListener,那么OnTouchListener中的OnTouchEvent會被調(diào)用挡闰,如果返回true,那么OnTouchEvent方法不會被調(diào)用掰盘,否則會被調(diào)用摄悯。如果當前設置的有OnclickListener,它的onClick方法在onTouchEvent里被調(diào)用。

當一個點擊事件產(chǎn)生后愧捕,它的傳遞過程遵循如下順序:Activity->Window->View,即事件總是先傳遞給Activity射众,在傳遞給Window,最后Window傳遞給頂級的View晃财,頂級的Viw接收到事件后按照事件分發(fā)的機制去分發(fā)事件。

事件分發(fā)的結論

  1. 同一個事件序列是從手指接觸屏幕的那一刻起典蜕,到手指離開屏幕的那一刻結束断盛,在這個過程中產(chǎn)生的一些列的事件,這個事件以down事件開始愉舔,中間含有數(shù)量不定的move事件钢猛,最終以up事件結束。
  2. 正常情況下轩缤,一個事件序列知只能被一個View攔截且消耗
  3. 一旦一個View攔截了某事件命迈,那么同一個事件序列的所有事件都會直接交給他處理,因此同一個事件序列的事件不能分別由兩個View同時處理火的,但是通過特殊手段可以做到壶愤,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。
  4. 某個View一旦開始處理事件馏鹤,如果它不消耗ACTION_DOWN事件(onTouchEvent 返回了false)征椒,那么同一事件序列中的其他事件都不會再交給他處理,會調(diào)用父元素的onTouchEvent
  5. 如果View不消費除ACTION_DOWN以外的其他事件湃累,那么這個點擊事件會消失勃救,此時父元素的onTouchEvent并不會被調(diào)用碍讨,并且當前View可以持續(xù)收到后續(xù)的事件,最終這些消失的點擊事件會傳遞給Activity處理 蒙秒?勃黍?
  6. ViewGroup默認不攔截任何事件。Android源碼中的ViewGroup的onInterceptTouchEvent默認返回false
  7. View沒有onInterceptTouchEvent方法晕讲,一旦有點擊事件傳遞給它覆获,那么它的onTouchEvent方法就會被調(diào)用
  8. View的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickable 和 longClickable 同時為false)益兄,clickable屬性要分情況锻梳,比如Botton的clickable屬性默認為true,而Textview的clickable屬性默認為false净捅。
  9. View的enable屬性不影響onTouchEvent的默認返回值疑枯。哪怕一個View是disable的,只要它的clickable或者longClickable有一個為true蛔六,那么它的onTouchEvent就返回true荆永。
  10. onClick發(fā)生的前提是當前View是可點擊的,并且它收到了down和up事件国章。
  11. 事件傳遞過程是由外向內(nèi)的具钥,事件總是先傳遞給父元素,然后再由父元素傳遞給子元素液兽。子元素可以通過requestDisallowInterceptTouchEvent方法干預父元素的分發(fā)過程骂删。

事件分發(fā)的源碼解析

Activity對事件的分發(fā)過程

當一個點擊操作發(fā)生時,事件最先傳遞給Activity四啰,由Activity的dispatchTouchEvent進行事件派發(fā)宁玫,具體工作由Activity內(nèi)部的Window來完成。
<pre>
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
</pre>

事件交給Activity所屬的Window進行分發(fā)柑晒,如果返回了true欧瘪,整個事件循環(huán)就結束了,返回false意味事件沒人處理匙赞,所有的View的onTouchEvent都返回了false佛掖,那么Activity的onTouchEvent就會被調(diào)用。

getWindow().superDispatchTouchEvent(ev),這里的Window是SDK里window的唯一實現(xiàn)類PhoneWindow
<pre>
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
</pre>

這里的邏輯非常的清晰涌庭,PhoneWindow直接將事件傳遞給了DecorView芥被,隨后DecorView進行事件的分發(fā)。

ViewGroup對事件的分發(fā)過程

ViewGroup onInterceptTouchEvent返回true坐榆,事件由自己處理撕彤,如果ViewGroup setOnTouchListener則onTouch會被調(diào)用,否則onToucheEvent會被調(diào)用,簡言之羹铅,如果兩個都提供的話onTouch會屏蔽onTouchEvent蚀狰,如果setOnClickListener了,在onTouchEvent中职员,onClick會被調(diào)用麻蹋?這里作者說的有些問題,onTouch返回為true才會屏蔽焊切。如果ViewGroup不攔截事件扮授,則事件會傳遞給它所在的點擊事件上的子view。
<pre>final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
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;
}
</pre>

上面的代碼表示专肪,ViewGroup在兩種情況下會判斷是否攔截當前事件刹勃,事件類型為ACTION_DOWN或者mFirstTouchTarget!=null嚎尤; 當事件由ViewGroup的子view成功處理時荔仁,mFirstTouchTarget會被賦值并指向子view這段代碼按照我的理解:事件開始時ACTION_DOWN,ViewGroup進入判斷是否攔截,如果攔截mFirstTouchTarget就為null芽死,然后event 為ACTION_MOVE乏梁,ViewGroup進入判斷ACTION==DOWN false,mFirstTouchTarget关贵!=null false遇骑,然后直接進入else intercepted = true,結果為onInterceptTouchEvent不會再被調(diào)用揖曾,并且同一事件序列里除ACTION_DOWN的事件都由ViewGroup處理落萎。

FLAG_DISALLOW_INTERCEPT標記位,這個標記位通過requestDisallowInterceptTouchEvent方法設置炭剪,一般用于子View中模暗。FLAG_DISALLOW_INTERCEPT一旦設置后,ViewGroup無法攔截除了ACTION_DOWN其他的點擊事件念祭。因為在ViewGroup分發(fā)事件時,如果是ACTION_DOWN就會重置FLAG_DISALLOW_INTERCEPT這個標記位碍侦。

ViewGroup不攔截事件時粱坤,事件會向下分發(fā)給它的子View,首先遍歷ViewGroup的所有子元素瓷产,然后判斷子元素是否能狗接收到點擊事件站玄。是否接收到點擊事件由兩點衡量:子元素是否在播放動畫,點擊事件的坐標是否在子元素的區(qū)域內(nèi)濒旦。如果某個子元素滿足這兩個條件株旷,那么事件就會交給他處理,這時會調(diào)用子View的dispatchTouchEvent。如果子元素的dispatchTouchevent返回true那么mFirstTouchTarget就會被賦值同時跳出for循環(huán)晾剖。

遍歷所有的子元素事件沒有被合適地處理到锉矢,有兩種情況:1.ViewGroup沒有子元素 2.子元素處理了事件但是在dispatchTouchEvent中返回了false,一般是在onTouchEvent里返回了false齿尽,這兩種情況ViewGroup自己處理事件

View的事件分發(fā)過程

<pre>
public boolean dispatchTouchEvent(MotionEvent event) {
..

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

   ..
    return result;
}

</pre>

從上面的代碼看沽损,首先會判斷是否設置了OnTouchListener,如果OnTouchListener里的OnTouch返回了true循头,OnTouchevent就不會被執(zhí)行绵估,OnTouch的優(yōu)先級要高于OnTouchEvent,DispatchTouchEvent也會返回true

然后是OnTouchEvent里的具體處理:

<pre>
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == 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));
    }

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

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            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) {
                        // 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)) {
                                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();
                    }

                    removeTapCallback();
                }
                break;

            case MotionEvent.ACTION_DOWN:
                mHasPerformedLongPress = false;

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                // Walk up the hierarchy to determine if we're inside a scrolling container.
                boolean isInScrollingContainer = isInScrollingContainer();

                // For views inside a scrolling container, delay the pressed feedback for
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    setPressed(true, x, y);
                    checkForLongClick(0);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                drawableHotspotChanged(x, y);

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    removeTapCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        // Remove any future long press/tap checks
                        removeLongPressCallback();

                        setPressed(false);
                    }
                }
                break;
        }

        return true;
    }

    return false;
}

</pre>

挑重點看卡骂,只要View的CLICKABLE和LONG_CLICKABLE有一個為true国裳,那么它就會消耗這個事件不管他是不是DISABLE狀態(tài),View的LONG_CLICKABLE默認為false全跨,而CLICKABLE是否為false和具體的View有關缝左,例如:Button是可點擊的CLICKABLE為true,Imageview螟蒸、TextView為false盒使。這里我有了一個疑問?通過前面的分析知道如果OnClickListener.onClick在onTouchEvent里調(diào)用七嫌,如果CLICKABLE和LONG_CLICKABLE直接返回了false少办,其他邏輯根本沒有走?那么為什么TextView 設置了點擊事件可以正常調(diào)用呢诵原?
原因是:setOnClickListener會自動講View的CLICKABLE置為true英妓,setOnLongClickListener會自動講LONG_CLICKABLE置為true

<pre>
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
</pre>

View的滑動沖突

外部攔截法

指事件都先經(jīng)過父容器的攔截處理,外部攔截法需要重寫父容器的onInterceptTouchEvent 偽碼表示:

<pre>
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean isInterceped = false;
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
isInterceped = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要攔截當前事件){
isInterceped = true;
}else {
isInterceped = false;
}
break;
case MotionEvent.ACTION_UP:
isInterceped = false;
break;
}
return isInterceped;
}
</pre>

在onInterceptTouchEvent中绍赛,首先是ACTION_DOWN蔓纠,父容器必須返回false,即不攔截ACTION_DOWN事件吗蚌,因為一旦父容器攔截了ACTION_DOWN事件那么后續(xù)的ACTION_MOVE和ACTION_UP事件都直接交給了父容器處理腿倚,這個時候事件就沒法傳遞給子元素了,然后是ACTION_MOVE事件蚯妇,這個事件可以根據(jù)需求考慮是否攔截敷燎,如果攔截就返回true,否則返回false箩言。最后是ACTION_UP事件必須返回false硬贯,因為ACTION_UP本身沒有什么意義,假設事件交給子元素處理陨收,如果ACTION_UP返回了true饭豹,子元素無法接收ACTION_UP事件,這時候子元素中的onCLick事件就無法觸發(fā)。

內(nèi)部攔截

指父容器不攔截任何事件拄衰,所有事件都傳遞給子元素它褪,如果子元素需要此事件就直接消耗掉,否則就交給父容器進行處理肾砂,這種方式需要配合requestDisallowInterceptTouchEvent使用
在子View的dispatchTouchEvent方法里處理列赎,同時父容器要默認攔截除ACTION_DOWN的其他事件

eg:ListView嵌套ViewPager 如果手指是側這滑動的話,就會造成事件沖突镐确,這里我的策略是:判斷橫向還是豎向距離更多一些包吝,橫向的話就交給ViewPager處理,豎向的話交給ListView處理源葫。

外部攔截方式:

<pre>
PointF lastPont = new PointF();
PointF currentPoint = new PointF();
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
currentPoint.x = ev.getX();
currentPoint.y = ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
float dX = Math.abs(currentPoint.x - lastPont.x);
float dY = Math.abs(currentPoint.y - lastPont.y);
if (dY/dX>1){
return super.onInterceptTouchEvent(ev);
}else {
return false;
}
case MotionEvent.ACTION_UP:
break;
}
lastPont.x = currentPoint.x;
lastPont.y = currentPoint.y;
return super.onInterceptTouchEvent(ev);
}
</pre>

內(nèi)部攔截方式:
在ViewPager onTouchEvent中
<pre>
if ((y>1||x>1)&&x/y>1) {
橫向滑動诗越,不讓父布局攔截
getParent().requestDisallowInterceptTouchEvent(true);
}else {
getParent().requestDisallowInterceptTouchEvent(false);
}
</pre>

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市息堂,隨后出現(xiàn)的幾起案子嚷狞,更是在濱河造成了極大的恐慌,老刑警劉巖荣堰,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件床未,死亡現(xiàn)場離奇詭異,居然都是意外死亡振坚,警方通過查閱死者的電腦和手機薇搁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渡八,“玉大人啃洋,你說我怎么就攤上這事∈瑚ⅲ” “怎么了宏娄?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長逮壁。 經(jīng)常有香客問我孵坚,道長,這世上最難降的妖魔是什么窥淆? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任卖宠,我火速辦了婚禮,結果婚禮上祖乳,老公的妹妹穿的比我還像新娘。我一直安慰自己秉氧,他們只是感情好眷昆,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般亚斋。 火紅的嫁衣襯著肌膚如雪作媚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天帅刊,我揣著相機與錄音纸泡,去河邊找鬼。 笑死赖瞒,一個胖子當著我的面吹牛女揭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播栏饮,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼吧兔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了袍嬉?” 一聲冷哼從身側響起境蔼,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伺通,沒想到半個月后箍土,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡罐监,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年吴藻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笑诅。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡调缨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吆你,到底是詐尸還是另有隱情,我是刑警寧澤妇多,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布伤哺,位于F島的核電站者祖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏七问。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一械巡、第九天 我趴在偏房一處隱蔽的房頂上張望饶氏。 院中可真熱鬧,春花似錦有勾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雇逞。三九已至,卻和暖如春喝峦,著一層夾襖步出監(jiān)牢的瞬間势誊,已是汗流浹背谣蠢。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留眉踱,地道東北人。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓谈喳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親婿禽。 傳聞我的和親對象是個殘疾皇子赏僧,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

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