View的事件分發(fā)機制以及滑動沖突

View的事件分發(fā)機制以及滑動沖突

[TOC]

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

點擊時間的分發(fā)過程 總是繞不過三個很重要的方法來共同完成:dispatchTouchEvent(MotionEvent ev), onIntercepTouchEvent(MotionEvent ev), onTouchEvent(MotionEvent ev)

public boolean dispatchTouchEvent(MotionEvent ev)

? 用來進行事件的分發(fā)绞惦。如果時間能夠分發(fā)到當前View,那么此方法一定會被調(diào)用颇象,返回的結(jié)果受View的OntouchEvent和下級View的dispatchEvent方法的影響腮恩,表示是否消耗當前事件距辆。

? public boolean onIntercepTouchEvent(MotionEvent ev)

? 只有ViewGroup才會擁有的方法,用于攔截某個事件盗尸,如果當前的View攔截某個事件宣旱,那么在同一個時間序列中至会,此方法不會被再次調(diào)用,返回結(jié)果表示是否攔截當前事件矮慕。

? **public boolean onTouchEvent(MotionEvent ev) **

? 在dispatchTouchEvent方法中調(diào)用帮匾,用來處理點擊事件,返回結(jié)果表示是否消耗當前事件痴鳄,如果不消耗瘟斜,則在同一個事件序列中,當前View無法再次接受到事件痪寻。

那么這三個方法的調(diào)用順序是如何呢螺句?

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

對于一個根ViewGroup來說,點擊事件產(chǎn)生后橡类,首先會傳遞給它壹蔓,此時它的dispatchTouchEvent就會被調(diào)用,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件猫态,接著事件就會交給這個ViewGroup處理佣蓉,及它的onTouchEvent方法就會被調(diào)用,如果它的onInterceptTouchEvent返回為false亲雪,表示它不攔截當前事件勇凭,此時當前事件就會繼續(xù)傳遞給它的子元素,此時如果是View义辕,則會直接調(diào)用onTouchEvent方法虾标。

OnTouchListener, View.onTouchEvent 和OnclickListener的區(qū)別

當一個View需要處理事件。設置了OnTouchListener,則OnTouchListener的onTouch方法會被回調(diào)灌砖,如果onTouch的方法返回True璧函,則View.onTouchEvent方法不會被調(diào)用傀蚌,反之則會被調(diào)用。View.onTouchEvent方法中蘸吓,如果當前設置的有OnclickListener善炫,其優(yōu)先級最低。

這三者的優(yōu)先級: OnTouchListener -> View.onTouchEvent -> OnclickListener

當一個點擊事件產(chǎn)生后库继,它的傳遞過程遵循如下順序 Activity -> PhoneWindow -> RootView箩艺。 由Activity 傳給PhoneWindow Window 最后傳給頂級View。

關(guān)于時間傳遞的機制宪萄,我們首先在這里給一些結(jié)論艺谆,

  1. 同一事件順序是指手指接觸屏幕的那一刻起, 到手指離開屏幕的那一刻結(jié)束,在這個過程中所產(chǎn)生的一系列事件拜英,這個時間序列以Down事件開始静汤,中間含有數(shù)量不一的move事件。最后以up事件結(jié)束居凶。
  2. 正常情況下撒妈,一個事件序列只能被一個View攔截且消耗,因此一個時間序列的事件不能分別由兩個View同時處理排监,但是我們可以通過代碼控制事件傳遞狰右。
  3. 某個View一旦決定攔截孕豹,那么一個事件序列都只能由它來處理踩萎,并且它的onIntercepTouchEvent不會再被調(diào)用。當一個View決定攔截一個事件后璃诀,同一事件的剩下事件也會交給它來處理挨队,也就是說onIntercepTouchEvent不會被再調(diào)用谷暮。
  4. 某個View一旦開始處理事件,如果他不消耗ACTION_DOWN事件(OnTouchEvent返回為false)盛垦,那么同一事件中的其他時間都不會再交給它來處理湿弦,并且事件將重新交由它的父元素去處理。
  5. 如果View不消耗除了ACTION_DOWN以外的其他事件腾夯,那么這個點擊事件會消失颊埃,此時父元素的onTouchEvent并不會被調(diào)用,并且當前View可以持續(xù)收到后續(xù)的事件蝶俱,最終這些消失的點擊事件會傳遞給Activity處理
  6. ViewGroup默認是不攔截任何事件班利,默認onInterceptTouchEvent方法默認返回False
  7. View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它榨呆,那么它的onTouchEvent方法就會被調(diào)用
  8. View的OntouchEvent默認都是會消耗事件罗标,除非它是不可以點擊的(clickable longclickable同時為false)。 View的longClickable屬性都是false,clickable屬性要分情況闯割,Button的clickable屬性默認為true彻消,TextView的clickable屬性默認為false。 當然 如果給view設置了setOnclickListener 或者setOnLongClickListener 會默認開啟宙拉。
  9. 事件傳遞過程是由外向內(nèi)的宾尚,即事件總是先傳遞給父元素,然后再由父元素分發(fā)給子元素鼓黔,通過requestDisallowInterceptTouchEvent方法可以在子元素中干涉父元素的事件分發(fā)過程央勒,但是ACTION_DOWN事件除外不见。

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

點擊事件是有MotionEvent來表示澳化,當一個點擊事件發(fā)生,最先傳入的是Activity稳吮,由Activity的disPatchEvent來進行事件派發(fā)缎谷,,具體工作是由Activity內(nèi)部的Window來完成的灶似。 Window會將時間傳遞給DecorView列林,一般DecorView就是當前界面的頂級容器(即是setContentView所設置的View的父容器),如圖:

public boolean disPatchTouchEvent(MotionEvent ev){
  if(ev.getAction == MotionEvent.ACTION_DOWN){
        onUserInteraction();
  }
  if(getWindow().superDispatchTouchEvent(ev)){
    return truel
  }
  return onTouchEvent(ev);
}

Window是如何將事件傳遞給ViewGroup的呢酪惭,Window類其實是一個抽象類 它可以控制頂級view的外觀和行為策略,Window的唯一實現(xiàn)是PhoneView類希痴,

publlic boolean superDisPatchTouchEvent(MotionEvent event){
  return mDecor.superDispatchTouchEvent(event);
}

這個mDecot其實就是我們getWindow().getDecorView()返回的View,我們通過設置setContentView設置的View就是它的一個子View,自此事件傳遞到了頂級View,即我們設置的SetContentView所設置的View春感。

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

我們在看一下ViewGroup對點擊事件的分發(fā)過程砌创,主要實現(xiàn)在disPatchEvent方法中,

final boolean intercepted
if(actionMasked == MotionEvent.Action_Down || mFirsrtTouchTarget != null){
  final boolean disallowIntercept = (GroupFlag & FLAG_DISALLOW_INTERCEPT) != 0;
  if(!disallowIntercept){
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action);
  }else{
    intercepted = false;
  } 
}else{
  intercepted = true;
}

由ViewGroup的子元素成功處理時鲫懒, mFirsrtTouchTarget 會被賦值并指向子元素嫩实。所以一旦事件是由當前的ViewGroup攔截,接下來同一時序的其他事件都會默認交給ViewGroup來處理窥岩。

另外一個特殊情況就是FLAG_DISALLOW_INTERCEPT標識符甲献,這這個一般通過requestDisallowInterceptTouchEvent方法來設置的,一般用于子View颂翼,一般設置ViewGroup將無法攔截除了ACTION_DOWN以外的其他點擊事件晃洒,因為ViewGroup在事件分發(fā)的時候會重置FLAG_DISALLOW_INTERCEPT標識符,這意味著當面對ACTION_DOWN的時候朦乏,ViewGroup一定會調(diào)用onInterceptTouchEvent來判斷自己是否需要攔截事件

                 final ArrayList<View> preorderedList = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(childIndex);                                                                           
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))                            {                           
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }

首先遍歷所有的ViewGroup的所有子元素,判斷是否能夠接受到點擊事件主要由兩點來衡量:子元素是否在播動畫和點擊事件的坐標是否落在子元素的區(qū)域內(nèi)锥累,dispatchTransformedTouchEvent就是調(diào)用了child的dispatchTouchEvent方法, 如果子元素的disPatchTouchEvent返回false 則會繼續(xù)分發(fā)給下一個子元素(如果有的話), 在addTouchTarget(child, idBitsToAssign) 給mFirsrtTouchTarget 賦值集歇。mFirsrtTouchTarget 是否為null直接影響ViewGroup對事件的攔截策略桶略。

            dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
            {
                if (child == null) {
                handled = super.dispatchTouchEvent(event);
                } else {
                handled = child.dispatchTouchEvent(event);
                }
            }

如果遍歷了所有的子元素事件都沒有被合適處理,這包含兩種情況: 第一種是ViewGroup沒有子元素,第二種是子元素處理了點擊事件际歼,但是在dispatchTouchEvenr中返回了false 一般是子元素在OnTouchEvent中返回了false惶翻,這是ViewGroup會自己處理點擊事件。

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

View的事件分發(fā)

         if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }

首先判斷是否有設置OnTouchListener,如果onTouchListener中的Touch返回true,那么onTouchEvent就不會被調(diào)用鹅心。

                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 (!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)) {
                                    performClick();
                                }
                            }
                        }
                   

只要View的CLICKABLE或者是LONG_CLICKABLE 那么他就會消耗這件事吕粗,當ACTION_UP事件發(fā)生 會觸發(fā)performClick() 如果設置了OnclickListener ,performClick就是調(diào)用Onclick方法旭愧。

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

設置Viewd的OnclickListener和OnLongClickListener()會自動將View的Clickable颅筋,LongClickAble為true。

    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的滑動沖突

常見的滑動沖突場景

  • 場景1 ------ 外部滑動方向和內(nèi)部滑動方向不一致
  • 場景2------- 外部滑動方向和內(nèi)部滑動方向一致
  • 場景3------- 上面兩種情況的嵌套

對應處理的方法:

  1. 當用戶左右滑動時输枯,需要外部攔截點擊事件议泵,當用戶需要上下滑動時,需要讓內(nèi)部攔截點擊事件
  2. 這種場景無法根據(jù)滑動的角度桃熄, 距離差已經(jīng)速度差來做判斷 一般需要從業(yè)務上找到突破口
  3. 同上先口,一般也是從業(yè)務的需要上得出相應的處理規(guī)則

兩種滑動沖突的解決方案:

  1. 外部攔截法

    所謂的外部攔截法就是所有的點擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件則攔截 如果不需要此事件就不攔截瞳收,這樣就可以解決滑動沖突的問題碉京,這種方法比較符合點擊事件的分發(fā)機制,外部攔截法需要重寫父容器的onInterceptTouchEvent方法螟深。偽代碼如下

     
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            int action = ev.getAction();
            float y = ev.getY();
    
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mLastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dy = y - mLastY;
                   if(父容器需要當前點擊事件){
                     return true;
                   }
                    break;
            }
           // 默認返回的都是false 
            return super.onInterceptTouchEvent(ev);
        }
    
    1. 內(nèi)部攔截法

      內(nèi)部攔截法主要是父容器不攔截任何事件谐宙,所有事件都傳遞給子元素,如果子元素需要此事件就直接消耗界弧,否則交由父容器進行處理凡蜻,需要配合requestDisallowInterceptTouchEvent()才能正常工作,一般內(nèi)部攔截法比較的復雜夹纫,它的偽代碼如下咽瓷,我們需要重寫子元素的dispatchTouchEvent方法:

      
        @Override
          public boolean dispatchTouchEvent(MotionEvent ev) {
              int action = ev.getAction();
              float y = ev.getY();
      
              switch (action) {
                  case MotionEvent.ACTION_DOWN:
                    parent.requestDisallowInterceptTouchEvent(true)
                      break;
                  case MotionEvent.ACTION_MOVE:              
                     if(父容器需要當前點擊事件){
                       return parent.requestDisallowInterceptTouchEvent(false);
                     }
                      break;
              }
             // 默認返回的都是false 
              return super.dispatchTouchEvent(ev);
          }
      
      

      面對不同的滑動策越的時候只需要修改ACTION_MOVE事件即可,其他不需要動也不能改動舰讹。除了子元素需要處理外茅姜,父元素也要默認攔截除了ACTION_DOWN以外的所有其他事件,這樣當子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)時月匣,父元素才能繼續(xù)攔截所需要的事件钻洒。

    ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锄开,隨后出現(xiàn)的幾起案子素标,更是在濱河造成了極大的恐慌,老刑警劉巖萍悴,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件头遭,死亡現(xiàn)場離奇詭異,居然都是意外死亡计维,警方通過查閱死者的電腦和手機袜香,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門鲫惶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人欠母,你說我怎么就攤上這事欢策。” “怎么了赏淌?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵踩寇,是天一觀的道長。 經(jīng)常有香客問我猜敢,道長姑荷,這世上最難降的妖魔是什么盒延? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任缩擂,我火速辦了婚禮,結(jié)果婚禮上添寺,老公的妹妹穿的比我還像新娘胯盯。我一直安慰自己,他們只是感情好计露,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布博脑。 她就那樣靜靜地躺著,像睡著了一般票罐。 火紅的嫁衣襯著肌膚如雪叉趣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天该押,我揣著相機與錄音疗杉,去河邊找鬼。 笑死蚕礼,一個胖子當著我的面吹牛烟具,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奠蹬,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼朝聋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了囤躁?” 一聲冷哼從身側(cè)響起冀痕,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤荔睹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后言蛇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體应媚,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年猜极,在試婚紗的時候發(fā)現(xiàn)自己被綠了中姜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡跟伏,死狀恐怖丢胚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情受扳,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布勘高,位于F島的核電站,受9級特大地震影響华望,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赖舟,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宾抓。 院中可真熱鬧,春花似錦石洗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽焦人。三九已至,卻和暖如春花椭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背矿辽。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工郭厌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留雕蔽,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓批狐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親嚣艇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

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