View事件機制分析

楊充發表於2019-05-13

目錄介紹

  • 01.Android中事件分發順序
    • 1.1 事件分發的物件是誰
    • 1.2 事件分發的本質
    • 1.3 事件在哪些物件間進行傳遞
    • 1.4 事件分發過程涉及方法
    • 1.5 Android中事件分發順序
  • 02.Activity的事件分發機制
    • 2.1 原始碼分析
    • 2.2 點選事件呼叫順序
    • 2.3 得出結論
  • 03.ViewGroup事件的分發機制
    • 3.1 看一下這個案例
    • 3.2 原始碼分析
    • 3.3 得出結論
  • 04.View事件的分發機制
    • 4.1 原始碼分析
    • 4.2 得出結論
    • 4.3 驗證結論
  • 05.思考一下
    • 5.1 onTouch()和onTouchEvent()的區別
    • 5.2 Touch事件的後續事件傳遞

好訊息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 連結地址:github.com/yangchong21…
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

01.Android中事件分發順序

1.1 事件分發的物件是誰

  • 事件分發的物件是事件。注意,事件分發是向下傳遞的,也就是父到子的順序。
  • 當使用者觸控螢幕時(View或ViewGroup派生的控制元件),將產生點選事件(Touch事件)。
    • Touch事件相關細節(發生觸控的位置、時間、歷史記錄、手勢動作等)被封裝成MotionEvent物件
  • 主要發生的Touch事件有如下四種:
    • MotionEvent.ACTION_DOWN:按下View(所有事件的開始)
    • MotionEvent.ACTION_MOVE:滑動View
    • MotionEvent.ACTION_CANCEL:非人為原因結束本次事件
    • MotionEvent.ACTION_UP:抬起View(與DOWN對應)
  • 事件列:
    • 從手指接觸螢幕至手指離開螢幕,這個過程產生的一系列事件。即當一個MotionEvent 產生後,系統需要把這個事件傳遞給一個具體的 View 去處理
    • 任何事件列都是以DOWN事件開始,UP事件結束,中間有無數的MOVE事件,如下圖:
    • image

1.2 事件分發的本質

  • 將點選事件(MotionEvent)向某個View進行傳遞並最終得到處理
    • 即當一個點選事件發生後,系統需要將這個事件傳遞給一個具體的View去處理。這個事件傳遞的過程就是分發過程。
    • Android事件分發機制的本質是要解決,點選事件由哪個物件發出,經過哪些物件,最終達到哪個物件並最終得到處理。

1.3 事件在哪些物件間進行傳遞

  • Activity、ViewGroup、View
    • 一個點選事件產生後,傳遞順序是:Activity(Window) -> ViewGroup -> View
    • Android的UI介面是由Activity、ViewGroup、View及其派生類組合而成的
  • View是所有UI元件的基類
    • 一般Button、ImageView、TextView等控制元件都是繼承父類View
  • ViewGroup是容納UI元件的容器,即一組View的集合(包含很多子View和子VewGroup),
    • 其本身也是從View派生的,即ViewGroup是View的子類
    • 是Android所有佈局的父類或間接父類:專案用到的佈局(LinearLayout、RelativeLayout等),都繼承自ViewGroup,即屬於ViewGroup子類。
    • 與普通View的區別:ViewGroup實際上也是一個View,只不過比起View,它多了可以包含子View和定義佈局引數的功能。

1.4 事件分發過程涉及方法

  • 事件分發過程由這幾個方法協作完成
    • dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()
    • View事件機制分析

1.5 Android中事件分發順序

  • Android中事件分發順序:
    • Activity(Window) -> ViewGroup -> View
  • 其中:
    • super:呼叫父類方法
    • true:消費事件,即事件不繼續往下傳遞
    • false:不消費事件,事件繼續往下傳遞 / 交由給父控制元件onTouchEvent()處理
  • 充分理解Android分發機制,本質上是要理解:
    • Activity對點選事件的分發機制
    • ViewGroup對點選事件的分發機制
    • View對點選事件的分發機制

02.Activity的事件分發機制

2.1 原始碼分析

  • 當一個點選事件發生時,事件最先傳到Activity的dispatchTouchEvent()進行事件分發
    • 具體是由Activity的Window來完成
  • 我們來看下Activity的dispatchTouchEvent()的原始碼
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //第一步
        //一般事件列開始都是DOWN,所以這裡基本是true
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //第二步
            onUserInteraction();
        }
        //第三步
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    複製程式碼
  • 第一步
    • 一般事件列開始都是DOWN(按下按鈕),所以這裡返回true,執行onUserInteraction()
  • 第二步
    • 先來看下onUserInteraction()原始碼
    public void onUserInteraction() { 
    }
    複製程式碼
    • 從原始碼可以看出:
      • 該方法為空方法
      • 從註釋得知:當此activity在棧頂時,觸屏點選按home,back,menu鍵等都會觸發此方法
      • 所以onUserInteraction()主要用於屏保
  • 第三步
    • Window類是抽象類,且PhoneWindow是Window類的唯一實現類
    • superDispatchTouchEvent(ev)是抽象方法
    • 通過PhoneWindow類中看一下superDispatchTouchEvent()的作用
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
        //mDecor是DecorView的例項
        //DecorView是檢視的頂層view,繼承自FrameLayout,是所有介面的父類
    }
    複製程式碼
  • 接下來我們看mDecor.superDispatchTouchEvent(event):
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    //DecorView繼承自FrameLayout
    //那麼它的父類就是ViewGroup
    而super.dispatchTouchEvent(event)方法,其實就應該是ViewGroup的dispatchTouchEvent()
    
    }
    複製程式碼
  • 得出結果
    • 執行getWindow().superDispatchTouchEvent(ev)實際上是執行了ViewGroup.dispatchTouchEvent(event)
    • 這樣事件就從 Activity 傳遞到了 ViewGroup

2.2 點選事件呼叫順序

  • 三個方法執行順序
    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        LogUtils.e("yc----------事件攔截----------");
        return super.onInterceptTouchEvent(e);
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        LogUtils.e("yc----------事件分發----------");
        return super.dispatchTouchEvent(ev);
    }
    
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        LogUtils.e("yc----------事件觸控----------");
        return super.onTouchEvent(e);
    }
    複製程式碼
    • 執行結果如下
    yc----------事件分發----------
    yc----------事件攔截----------
    yc----------事件觸控----------
    複製程式碼
  • 下面將用一段虛擬碼來闡述上述三個方法的關係和點選事件傳遞規則
    // 點選事件產生後,會直接呼叫dispatchTouchEvent分發方法
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //代表是否消耗事件
        boolean consume = false;
    
        if (onInterceptTouchEvent(ev)) {
            //如果onInterceptTouchEvent()返回true則代表當前View攔截了點選事件
            //則該點選事件則會交給當前View進行處理
            //即呼叫onTouchEvent ()方法去處理點選事件
            consume = onTouchEvent (ev) ;
        } else {
            //如果onInterceptTouchEvent()返回false則代表當前View不攔截點選事件
            //則該點選事件則會繼續傳遞給它的子元素
            //子元素的dispatchTouchEvent()就會被呼叫,重複上述過程
            //直到點選事件被最終處理為止
            consume = child.dispatchTouchEvent (ev) ;
        }
        return consume;
    }
    複製程式碼
  • 當一個點選事件發生時,呼叫順序如下
    • 1.事件最先傳到Activity的dispatchTouchEvent()進行事件分發
    • 2.呼叫Window類實現類PhoneWindow的superDispatchTouchEvent()
    • 3.呼叫DecorView的superDispatchTouchEvent()
    • 4.最終呼叫DecorView父類的dispatchTouchEvent(),即ViewGroup的dispatchTouchEvent()

2.3 得出結論

  • 當一個點選事件發生時,事件最先傳到Activity的dispatchTouchEvent()進行事件分發,最終是呼叫了ViewGroup的dispatchTouchEvent()方法
  • 這樣事件就從 Activity 傳遞到了 ViewGroup

03.ViewGroup事件的分發機制

3.1 看一下這個案例

  • 佈局如下:
    • View事件機制分析
  • 結果測試
    • 只點選Button,發現執行順序:btn1,btn2
    • 再點選空白處,發現執行順序:btn1,btn2,viewGroup
  • 從上面的測試結果發現:
    • 當點選Button時,執行Button的onClick(),但ViewGroupLayout註冊的onTouch()不會執行
    • 只有點選空白區域時才會執行ViewGroupLayout的onTouch();
    • 結論:Button的onClick()將事件消費掉了,因此事件不會再繼續向下傳遞。

3.2 原始碼分析

  • ViewGroup的dispatchTouchEvent()原始碼分析,該方法比較複雜,擷取幾個重要的邏輯片段進行介紹,來解析整個分發流程。
    // 發生ACTION_DOWN事件或者已經發生過ACTION_DOWN,並且將mFirstTouchTarget賦值,才進入此區域,主要功能是攔截器
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
        //disallowIntercept:是否禁用事件攔截的功能(預設是false),即不禁用
        //可以在子View通過呼叫requestDisallowInterceptTouchEvent方法對這個值進行修改,不讓該View攔截事件
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        //預設情況下會進入該方法
        if (!disallowIntercept) {
            //呼叫攔截方法
            intercepted = onInterceptTouchEvent(ev); 
            ev.setAction(action);
        } else {
            intercepted = false;
        }
    } else {
        // 當沒有觸控targets,且不是down事件時,開始持續攔截觸控。
        intercepted = true;
    }
    複製程式碼
    • 這一段的內容主要是為判斷是否攔截。如果當前事件的MotionEvent.ACTION_DOWN,則進入判斷,呼叫ViewGroup onInterceptTouchEvent()方法的值,判斷是否攔截。如果mFirstTouchTarget != null,即已經發生過MotionEvent.ACTION_DOWN,並且該事件已經有ViewGroup的子View進行處理了,那麼也進入判斷,呼叫ViewGroup onInterceptTouchEvent()方法的值,判斷是否攔截。如果不是以上兩種情況,即已經是MOVE或UP事件了,並且之前的事件沒有物件進行處理,則設定成true,開始攔截接下來的所有事件。這也就解釋瞭如果子View的onTouchEvent()方法返回false,那麼接下來的一些列事件都不會交給他處理。如果VieGroup的onInterceptTouchEvent()第一次執行為true,則mFirstTouchTarget = null,則也會使得接下來不會呼叫onInterceptTouchEvent(),直接將攔截設定為true。
  • 當ViewGroup不攔截事件的時候,事件會向下分發交由它的子View或ViewGroup進行處理。
      /* 從最底層的父檢視開始遍歷,
       ** 找尋newTouchTarget,即上面的mFirstTouchTarget
       ** 如果已經存在找尋newTouchTarget,說明正在接收觸控事件,則跳出迴圈。
        */
    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 (childWithAccessibilityFocus != null) {
         if (childWithAccessibilityFocus != child) {
            continue;
         }
         childWithAccessibilityFocus = null;
         i = childrenCount - 1;
      }
      //如果view不可見,或者觸控的座標點不在view的範圍內,則跳過本次迴圈
      if (!canViewReceivePointerEvents(child) 
          || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
        }
    
       newTouchTarget = getTouchTarget(child);
       // 已經開始接收觸控事件,並退出整個迴圈。
       if (newTouchTarget != null) {
           newTouchTarget.pointerIdBits |= idBitsToAssign;
           break;
        }
    
        //重置取消或抬起標誌位
        //如果觸控位置在child的區域內,則把事件分發給子View或ViewGroup
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            // 獲取TouchDown的時間點
            mLastTouchDownTime = ev.getDownTime();
            // 獲取TouchDown的Index
            if (preorderedList != null) {
               for (int j = 0; j < childrenCount; j++) {
                   if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
               }
             } else {
                     mLastTouchDownIndex = childIndex;
                    }
    
          //獲取TouchDown的x,y座標
          mLastTouchDownX = ev.getX();
          mLastTouchDownY = ev.getY();
          //新增TouchTarget,則mFirstTouchTarget != null。
          newTouchTarget = addTouchTarget(child, idBitsToAssign);
          //表示以及分發給NewTouchTarget
          alreadyDispatchedToNewTouchTarget = true;
          break;
    }
    複製程式碼
    • dispatchTransformedTouchEvent()方法實際就是呼叫子元素的dispatchTouchEvent()方法。
    • 其中dispatchTransformedTouchEvent()方法的重要邏輯如下:
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    複製程式碼
    • 由於其中傳遞的child不為空,所以就會呼叫子元素的dispatchTouchEvent()。如果子元素的dispatchTouchEvent()方法返回true,那麼mFirstTouchTarget就會被賦值,同時跳出for迴圈。
    //新增TouchTarget,則mFirstTouchTarget != null。
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
     //表示以及分發給NewTouchTarget
     alreadyDispatchedToNewTouchTarget = true;
    複製程式碼
    • 其中在addTouchTarget(child, idBitsToAssign);內部完成mFirstTouchTarget被賦值。如果mFirstTouchTarget為空,將會讓ViewGroup預設攔截所有操作。如果遍歷所有子View或ViewGroup,都沒有消費事件。ViewGroup會自己處理事件。

3.3 得出結論

  • Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View
  • 在ViewGroup中通過onInterceptTouchEvent()對事件傳遞進行攔截
    • 1.onInterceptTouchEvent方法返回true代表攔截事件,即不允許事件繼續向子View傳遞;
    • 2.返回false代表不攔截事件,即允許事件繼續向子View傳遞;(預設返回false)
    • 3.子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件。

04.View事件的分發機制

4.1 原始碼分析

  • View中dispatchTouchEvent()的原始碼分析
    public boolean dispatchTouchEvent(MotionEvent event) {  
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                mOnTouchListener.onTouch(this, event)) {  
            return true;  
        }  
        return onTouchEvent(event);  
    }
    複製程式碼
  • 從上面可以看出:
    • 只有以下三個條件都為真,dispatchTouchEvent()才返回true;否則執行onTouchEvent(event)方法
    第一個條件:mOnTouchListener != null;
    第二個條件:(mViewFlags & ENABLED_MASK) == ENABLED;
    第三個條件:mOnTouchListener.onTouch(this, event);
    複製程式碼
  • 下面,我們來看看下這三個判斷條件:
    • 第一個條件:mOnTouchListener!= null
    //mOnTouchListener是在View類下setOnTouchListener方法裡賦值的
    public void setOnTouchListener(OnTouchListener l) { 
    
    //即只要我們給控制元件註冊了Touch事件,mOnTouchListener就一定被賦值(不為空)
        mOnTouchListener = l;  
    }
    複製程式碼
    • 第二個條件:(mViewFlags & ENABLED_MASK) == ENABLED
    • 該條件是判斷當前點選的控制元件是否enable
    • 由於很多View預設是enable的,因此該條件恆定為true
    • 第三個條件:mOnTouchListener.onTouch(this, event)
    • 回撥控制元件註冊Touch事件時的onTouch方法
    //手動呼叫設定
    button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
            return false;  
        }  
    });
    複製程式碼
    • 如果在onTouch方法返回true,就會讓上述三個條件全部成立,從而整個方法直接返回true。
    • 如果在onTouch方法裡返回false,就會去執行onTouchEvent(event)方法。
  • 接下來,我們繼續看:**onTouchEvent(event)**的原始碼分析
    public boolean onTouchEvent(MotionEvent event) {  
        final int viewFlags = mViewFlags;  
        if ((viewFlags & ENABLED_MASK) == DISABLED) {  
            // 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;  
            }  
        }  
         //如果該控制元件是可以點選的就會進入到下兩行的switch判斷中去;
    
        if (((viewFlags & CLICKABLE) == CLICKABLE ||  
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
        //如果當前的事件是抬起手指,則會進入到MotionEvent.ACTION_UP這個case當中。
    
            switch (event.getAction()) {  
                case MotionEvent.ACTION_UP:  
                    boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
                   // 在經過種種判斷之後,會執行到關注點1的performClick()方法。
                   //請往下看關注點1
                    if ((mPrivateFlags & 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) {  
                            // 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)) {  
                //關注點1
                //請往下看performClick()的原始碼分析
                                    performClick();  
                                }  
                            }  
                        }  
                        if (mUnsetPressedState == null) {  
                            mUnsetPressedState = new UnsetPressedState();  
                        }  
                        if (prepressed) {  
                            mPrivateFlags |= PRESSED;  
                            refreshDrawableState();  
                            postDelayed(mUnsetPressedState,  
                                    ViewConfiguration.getPressedStateDuration());  
                        } else if (!post(mUnsetPressedState)) {  
                            // If the post failed, unpress right now  
                            mUnsetPressedState.run();  
                        }  
                        removeTapCallback();  
                    }  
                    break;  
                case MotionEvent.ACTION_DOWN:  
                    if (mPendingCheckForTap == null) {  
                        mPendingCheckForTap = new CheckForTap();  
                    }  
                    mPrivateFlags |= PREPRESSED;  
                    mHasPerformedLongPress = false;  
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                    break;  
                case MotionEvent.ACTION_CANCEL:  
                    mPrivateFlags &= ~PRESSED;  
                    refreshDrawableState();  
                    removeTapCallback();  
                    break;  
                case MotionEvent.ACTION_MOVE:  
                    final int x = (int) event.getX();  
                    final int y = (int) event.getY();  
                    // Be lenient about moving outside of buttons  
                    int slop = mTouchSlop;  
                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                            (y < 0 - slop) || (y >= getHeight() + slop)) {  
                        // Outside button  
                        removeTapCallback();  
                        if ((mPrivateFlags & PRESSED) != 0) {  
                            // Remove any future long press/tap checks  
                            removeLongPressCallback();  
                            // Need to switch from pressed to not pressed  
                            mPrivateFlags &= ~PRESSED;  
                            refreshDrawableState();  
                        }  
                    }  
                    break;  
            }  
    //如果該控制元件是可以點選的,就一定會返回true
            return true;  
        }  
    //如果該控制元件是不可以點選的,就一定會返回false
        return false;  
    }
    複製程式碼
  • 關注點1:
    • performClick()的原始碼分析
    public boolean performClick() {  
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  
    
        if (mOnClickListener != null) {  
            playSoundEffect(SoundEffectConstants.CLICK);  
            mOnClickListener.onClick(this);  
            return true;  
        }  
        return false;  
    }
    複製程式碼
    • 只要mOnClickListener不為null,就會去呼叫onClick方法;
  • 那麼,mOnClickListener又是在哪裡賦值的呢?請繼續看:
    public void setOnClickListener(OnClickListener l) {  
        if (!isClickable()) {  
            setClickable(true);  
        }  
        mOnClickListener = l;  
    }
    複製程式碼
    • 當我們通過呼叫setOnClickListener方法來給控制元件註冊一個點選事件時,就會給mOnClickListener賦值(不為空),即會回撥onClick()。

4.2 得出結論

  • 1.onTouch()的執行高於onClick()
  • 2.每當控制元件被點選時:
    • 如果在回撥onTouch()裡返回false,就會讓dispatchTouchEvent方法返回false,那麼就會執行onTouchEvent();如果回撥了setOnClickListener()來給控制元件註冊點選事件的話,最後會在performClick()方法裡回撥onClick()。
      • onTouch()返回false(該事件沒被onTouch()消費掉) = 執行onTouchEvent() = 執行OnClick()
    • 如果在回撥onTouch()裡返回true,就會讓dispatchTouchEvent方法返回true,那麼將不會執行onTouchEvent(),即onClick()也不會執行;
      • onTouch()返回true(該事件被onTouch()消費掉) = dispatchTouchEvent()返回true(不會再繼續向下傳遞) = 不會執行onTouchEvent() = 不會執行OnClick()

4.3 驗證結論

  • 在回撥onTouch()裡返回true
    TextView textView = findViewById(R.id.tv_13);
    //設定OnTouchListener()
    textView.setOnTouchListener(new View.OnTouchListener() {
    
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            Log.d("小楊逗比","執行了onTouch(), 動作是:" + event.getAction());
            return true;
        }
    });
    //設定OnClickListener
    textView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.d("小楊逗比","執行了onClick()");
        }
    });
    複製程式碼
    • 列印日誌如下所示
    • 注意action為0是ACTION_DOWN,為2是ACTION_MOVE,為1是ACTION_UP。
    2019-04-04 13:37:58.301 13616-13616/org.yczbj.ycrefreshview D/小楊逗比: 執行了onTouch(), 動作是:0
    2019-04-04 13:37:58.315 13616-13616/org.yczbj.ycrefreshview D/小楊逗比: 執行了onTouch(), 動作是:2
    2019-04-04 13:37:58.405 13616-13616/org.yczbj.ycrefreshview D/小楊逗比: 執行了onTouch(), 動作是:2
    2019-04-04 13:37:58.408 13616-13616/org.yczbj.ycrefreshview D/小楊逗比: 執行了onTouch(), 動作是:1
    複製程式碼
  • 在回撥onTouch()裡返回false
    • 列印結果如下所示
    2019-04-04 13:41:26.961 14006-14006/org.yczbj.ycrefreshview D/小楊逗比: 執行了onTouch(), 動作是:0
    2019-04-04 13:41:26.978 14006-14006/org.yczbj.ycrefreshview D/小楊逗比: 執行了onTouch(), 動作是:2
    2019-04-04 13:41:27.072 14006-14006/org.yczbj.ycrefreshview D/小楊逗比: 執行了onTouch(), 動作是:2
    2019-04-04 13:41:27.074 14006-14006/org.yczbj.ycrefreshview D/小楊逗比: 執行了onTouch(), 動作是:1
    2019-04-04 13:41:27.076 14006-14006/org.yczbj.ycrefreshview D/小楊逗比: 執行了onClick()
    複製程式碼
  • 總結:onTouch()返回true就認為該事件被onTouch()消費掉,因而不會再繼續向下傳遞,即不會執行OnClick()。

05.思考一下

5.1 onTouch()和onTouchEvent()的區別

  • 這兩個方法都是在View的dispatchTouchEvent中呼叫,但onTouch優先於onTouchEvent執行。
  • 如果在onTouch方法中返回true將事件消費掉,onTouchEvent()將不會再執行。
  • 特別注意:請看下面程式碼
    //&&為短路與,即如果前面條件為false,將不再往下執行
    //所以,onTouch能夠得到執行需要兩個前提條件:
    //1. mOnTouchListener的值不能為空
    //2. 當前點選的控制元件必須是enable的。
    mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)
    複製程式碼
  • 因此如果你有一個控制元件是非enable的,那麼給它註冊onTouch事件將永遠得不到執行。對於這一類控制元件,如果我們想要監聽它的touch事件,就必須通過在該控制元件中重寫onTouchEvent方法來實現。

5.2 Touch事件的後續事件(MOVE、UP)層級傳遞

  • 如果給控制元件註冊了Touch事件,每次點選都會觸發一系列action事件(ACTION_DOWN,ACTION_MOVE,ACTION_UP等)
  • 當dispatchTouchEvent在進行事件分發的時候,只有前一個事件(如ACTION_DOWN)返回true,才會收到後一個事件(ACTION_MOVE和ACTION_UP)
    • 即如果在執行ACTION_DOWN時返回false,後面一系列的ACTION_MOVE和ACTION_UP事件都不會執行
  • 從上面對事件分發機制分析知:
    • dispatchTouchEvent()和 onTouchEvent()消費事件、終結事件傳遞(返回true)
    • 而onInterceptTouchEvent 並不能消費事件,它相當於是一個分叉口起到分流導流的作用,對後續的ACTION_MOVE和ACTION_UP事件接收起到非常大的作用
    • 請記住:接收了ACTION_DOWN事件的函式不一定能收到後續事件(ACTION_MOVE、ACTION_UP)
  • 這裡給出ACTION_MOVE和ACTION_UP事件的傳遞結論
    • 如果在某個物件(Activity、ViewGroup、View)的dispatchTouchEvent()消費事件(返回true),那麼收到ACTION_DOWN的函式也能收到ACTION_MOVE和ACTION_UP
    • 如果在某個物件(Activity、ViewGroup、View)的onTouchEvent()消費事件(返回true),那麼ACTION_MOVE和ACTION_UP的事件從上往下傳到這個View後就不再往下傳遞了,而直接傳給自己的onTouchEvent()並結束本次事件傳遞過程。

其他介紹

01.關於部落格彙總連結

02.關於我的部落格

專案地址:github.com/yangchong21…

相關文章