Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 深入理解Android事件分發機制

深入理解Android事件分發機制

編輯:關於Android編程

Android事件分發,參考了網上的很多資料。基本基於android2.2的源碼來分析,因為即使是新的版本,裡面的原理思想也沒有改變。有了大神的肩膀,我在理解了其原理的基礎上,進行一個小總結。

先理解一個概念:

事件:在android中,點擊屏幕是時,產生的長按,點擊,滑動,雙擊,多指操作等,構成了android中的事件響應。

如:ACTION_DOWN

ACTION_MOVE

ACTION_UP

所有的操作事件首先必須執行的是按下操作(ACTION_DOWN),之後所有的操作都是以按下操作作為前提,當按下操作完成後,接下來可能是一段移動(ACTION_MOVE)然後抬起(ACTION_UP),或者是按下操作執行完成後沒有移動就直接抬起。這一系列的動作在Android中都可以進行控制。

視圖:android中的所有視圖都繼承於View,ViewGroup也繼承View。如圖:

 

 

\

 

下面是事件分發的總結:

1、ViewGroup的事件分發機制

從上層來分析(不去搞Linux內核,硬件),與用戶直接進行交互的是activity,所以,當用戶點擊屏幕時,發生了啥?

看了大神博客,是執行分發事件的方法,dispatchTouchEvent()

 

    public boolean dispatchTouchEvent(MotionEvent ev) {  
      
            //如果是按下狀態就調用onUserInteraction()方法,onUserInteraction()方法  
            //是個空的方法, 我們直接跳過這裡看下面的實現  
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {  
                onUserInteraction();  
            }  
              
            if (getWindow().superDispatchTouchEvent(ev)) {  
                return true;  
            }  
              
            //getWindow().superDispatchTouchEvent(ev)返回false,這個事件就交給Activity  
            //來處理, Activity的onTouchEvent()方法直接返回了false  
            return onTouchEvent(ev);  
    }  

這個方法中我們還是比較關心getWindow()的superDispatchTouchEvent()方法,getWindow()返回當前Activity的頂層窗口Window對象,我們直接看Window API的superDispatchTouchEvent()方法

 

 

    /** 
         * Used by custom windows, such as Dialog, to pass the touch screen event 
         * further down the view hierarchy. Application developers should 
         * not need to implement or call this. 
         * 
         */  
        public abstract boolean superDispatchTouchEvent(MotionEvent event);  
這個是個抽象方法,所以我們直接找到其子類來看看superDispatchTouchEvent()方法的具體邏輯實現,Window的唯一子類是PhoneWindow,我們就看看PhoneWindow的superDispatchTouchEvent()方法
public boolean superDispatchTouchEvent(KeyEvent event) {  
        return mDecor.superDispatcTouchEvent(event);  
}
裡面直接調用DecorView類的superDispatchTouchEvent()方法,或許很多人不了解DecorView這個類,DecorView是PhoneWindow的一個final的內部類並且繼承FrameLayout的,也是Window界面的最頂層的View對象。耐心看下去

先新建一個項目,取名AndroidTouchEvent,然後直接用模擬器運行項目, MainActivity的布局文件為

    <relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">  
      
        <textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerhorizontal="true" android:layout_centervertical="true" android:text="@string/hello_world">  
      
    </textview></relativelayout>  

利用hierarchyviewer工具來查看下MainActivity的View的層次結構,如下圖

 

\
我們看到最頂層就是PhoneWindow$DecorView,接著DecorView下面有一個LinearLayout, LinearLayout下面有兩個FrameLayout
上面那個FrameLayout是用來顯示標題欄的,這個Demo中是一個TextView,當然我們還可以定制我們的標題欄,利用getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE,R.layout.XXX); xxx就是我們自定義標題欄的布局XML文件
下面的FrameLayout是用來裝載ContentView的,也就是我們在Activity中利用setContentView()方法設置的View,現在我們知道了,原來我們利用setContentView()設置Activity的View的外面還嵌套了這麼多的東西


我們來理清下思路,Activity的最頂層窗體是PhoneWindow,而PhoneWindow的最頂層View是DecorView,接下來我們就看DecorView類的superDispatchTouchEvent()方法

    public boolean superDispatchTouchEvent(MotionEvent event) {  
                return super.dispatchTouchEvent(event);  
    }  

在裡面調用了父類FrameLayout的dispatchTouchEvent()方法,而FrameLayout(繼承於ViewGroup)中並沒有dispatchTouchEvent()方法,所以我們直接看ViewGroup的dispatchTouchEvent()方法
    /** 
        * {@inheritDoc} 
        */  
       @Override  
       public boolean dispatchTouchEvent(MotionEvent ev) {  
           final int action = ev.getAction();  
           final float xf = ev.getX();  
           final float yf = ev.getY();  
           final float scrolledXFloat = xf + mScrollX;  
           final float scrolledYFloat = yf + mScrollY;  
           final Rect frame = mTempRect;  
      
           //這個值默認是false, 然後我們可以通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法  
           //來改變disallowIntercept的值  
           boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
      
           //這裡是ACTION_DOWN的處理邏輯  
           if (action == MotionEvent.ACTION_DOWN) {  
            //清除mMotionTarget, 每次ACTION_DOWN都很設置mMotionTarget為null  
               if (mMotionTarget != null) {  
                   mMotionTarget = null;  
               }  
      
               //disallowIntercept默認是false, 就看ViewGroup的onInterceptTouchEvent()方法  
               if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
                   ev.setAction(MotionEvent.ACTION_DOWN);  
                   final int scrolledXInt = (int) scrolledXFloat;  
                   final int scrolledYInt = (int) scrolledYFloat;  
                   final View[] children = mChildren;  
                   final int count = mChildrenCount;  
                   //遍歷其子View  
                   for (int i = count - 1; i >= 0; i--) {  
                       final View child = children[i];  
                         
                       //如果該子View是VISIBLE或者該子View正在執行動畫, 表示該View才  
                       //可以接受到Touch事件  
                       if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                               || child.getAnimation() != null) {  
                        //獲取子View的位置范圍  
                           child.getHitRect(frame);  
                             
                           //如Touch到屏幕上的點在該子View上面  
                           if (frame.contains(scrolledXInt, scrolledYInt)) {  
                               // offset the event to the view's coordinate system  
                               final float xc = scrolledXFloat - child.mLeft;  
                               final float yc = scrolledYFloat - child.mTop;  
                               ev.setLocation(xc, yc);  
                               child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
                                 
                               //調用該子View的dispatchTouchEvent()方法  
                               if (child.dispatchTouchEvent(ev))  {  
                                   // 如果child.dispatchTouchEvent(ev)返回true表示  
                                //該事件被消費了,設置mMotionTarget為該子View  
                                   mMotionTarget = child;  
                                   //直接返回true  
                                   return true;  
                               }  
                               // The event didn't get handled, try the next view.  
                               // Don't reset the event's location, it's not  
                               // necessary here.  
                           }  
                       }  
                   }  
               }  
           }  
      
           //判斷是否為ACTION_UP或者ACTION_CANCEL  
           boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
                   (action == MotionEvent.ACTION_CANCEL);  
      
           if (isUpOrCancel) {  
               //如果是ACTION_UP或者ACTION_CANCEL, 將disallowIntercept設置為默認的false  
            //假如我們調用了requestDisallowInterceptTouchEvent()方法來設置disallowIntercept為true  
            //當我們抬起手指或者取消Touch事件的時候要將disallowIntercept重置為false  
            //所以說上面的disallowIntercept默認在我們每次ACTION_DOWN的時候都是false  
               mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
           }  
      
           // The event wasn't an ACTION_DOWN, dispatch it to our target if  
           // we have one.  
           final View target = mMotionTarget;  
           //mMotionTarget為null意味著沒有找到消費Touch事件的View, 所以我們需要調用ViewGroup父類的  
           //dispatchTouchEvent()方法,也就是View的dispatchTouchEvent()方法  
           if (target == null) {  
               // We don't have a target, this means we're handling the  
               // event as a regular view.  
               ev.setLocation(xf, yf);  
               if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
                   ev.setAction(MotionEvent.ACTION_CANCEL);  
                   mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
               }  
               return super.dispatchTouchEvent(ev);  
           }  
      
           //這個if裡面的代碼ACTION_DOWN不會執行,只有ACTION_MOVE  
           //ACTION_UP才會走到這裡, 假如在ACTION_MOVE或者ACTION_UP攔截的  
           //Touch事件, 將ACTION_CANCEL派發給target,然後直接返回true  
           //表示消費了此Touch事件  
           if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
               final float xc = scrolledXFloat - (float) target.mLeft;  
               final float yc = scrolledYFloat - (float) target.mTop;  
               mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
               ev.setAction(MotionEvent.ACTION_CANCEL);  
               ev.setLocation(xc, yc);  
                 
               if (!target.dispatchTouchEvent(ev)) {  
               }  
               // clear the target  
               mMotionTarget = null;  
               // Don't dispatch this event to our own view, because we already  
               // saw it when intercepting; we just want to give the following  
               // event to the normal onTouchEvent().  
               return true;  
           }  
      
           if (isUpOrCancel) {  
               mMotionTarget = null;  
           }  
      
           // finally offset the event to the target's coordinate system and  
           // dispatch the event.  
           final float xc = scrolledXFloat - (float) target.mLeft;  
           final float yc = scrolledYFloat - (float) target.mTop;  
           ev.setLocation(xc, yc);  
      
           if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
               ev.setAction(MotionEvent.ACTION_CANCEL);  
               target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
               mMotionTarget = null;  
           }  
      
           //如果沒有攔截ACTION_MOVE, ACTION_DOWN的話,直接將Touch事件派發給target  
           return target.dispatchTouchEvent(ev);  
       }  

這個方法相對來說還是蠻長,不過所有的邏輯都寫在一起,看起來比較方便,接下來我們就具體來分析一下

 

 

 

我們點擊屏幕上面的TextView來看看Touch是如何分發的,先看看ACTION_DOWN

在DecorView這一層會直接調用ViewGroup的dispatchTouchEvent(), 先看18行,每次ACTION_DOWN都會將mMotionTarget設置為null,mMotionTarget是什麼?我們先不管,繼續看代碼,走到25行, disallowIntercept默認為false,我們再看ViewGroup的onInterceptTouchEvent()方法

public boolean onInterceptTouchEvent(MotionEvent ev) {  
          return false;  
}  

直接返回false, 繼續往下看,循環遍歷DecorView裡面的Child,從上面的MainActivity的層次結構圖我們可以看出,DecorView裡面只有一個Child那就是LinearLayout, 第43行判斷Touch的位置在不在LinnearLayout上面,這是毫無疑問的,所以直接跳到51行, 調用LinearLayout的dispatchTouchEvent()方法,LinearLayout也沒有dispatchTouchEvent()這個方法,所以也是調用ViewGroup的dispatchTouchEvent()方法,所以這個方法卡在51行沒有繼續下去,而是去先執行LinearLayout的dispatchTouchEvent()

 

LinearLayout調用dispatchTouchEvent()的邏輯跟DecorView是一樣的,所以也是遍歷LinearLayout的兩個FrameLayout,判斷Touch的是哪個FrameLayout,很明顯是下面那個,調用下面那個FrameLayout的dispatchTouchEvent(), 所以LinearLayout的dispatchTouchEvent()卡在51也沒繼續下去

繼續調用FrameLayout的dispatchTouchEvent()方法,和上面一樣的邏輯,下面的FrameLayout也只有一個Child,就是RelativeLayout,FrameLayout的dispatchTouchEvent()繼續卡在51行,先執行RelativeLayout的dispatchTouchEvent()方法

執行RelativeLayout的dispatchTouchEvent()方法邏輯還是一樣的,循環遍歷RelativeLayout裡面的孩子,裡面只有一個TextView, 所以這裡就調用TextView的dispatchTouchEvent(), TextView並沒有dispatchTouchEvent()這個方法,於是找TextView的父類View,在看View的dispatchTouchEvent()的方法之前,我們先理清下上面這些ViewGroup執行dispatchTouchEvent()的思路,我畫了一張圖幫大家理清下(這裡沒有畫出onInterceptTouchEvent()方法)

\

 

這就是ViewGroup的事件分發機制,會一層一層地把事件一直分發下去,直到目標View。當然,我們也可以在中間的ViewGroup把事件消費掉(重寫某些方法就可以,比如onTouchListener,onTouchEvent。返回ture即可。看完下面即可理解),這樣事件的分發就會終止。

2、View的Touch事件分發機制

我們都知道,按鈕的點擊事件的設置。如下

 

    button.setOnClickListener(new OnClickListener() {  
        @Override  
        public void onClick(View v) {  
            Log.d("TAG", "onClick execute");  
        }  
    });  
如果想給這個按鈕再添加一個touch事件,只需要調用:
 button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
            Log.d("TAG", "onTouch execute, action " + event.getAction());  
            return false;  
        }  
    });  
onTouch方法裡能做的事情比onClick要多一些,比如判斷手指按下、抬起、移動等事件。那麼如果我兩個事件都注冊了,哪一個會先執行呢?我們來試一下就知道了,運行程序點擊按鈕,打印結果如下:

 

\

可以看到,onTouch是優先於onClick執行的,並且onTouch執行了兩次,一次是ACTION_DOWN,一次是ACTION_UP(你還可能會有多次ACTION_MOVE的執行,如果你手抖了一下)。因此事件傳遞的順序是先經過onTouch,再傳遞到onClick。

細心的朋友應該可以注意到,onTouch方法是有返回值的,這裡我們返回的是false,如果我們嘗試把onTouch方法裡的返回值改成true,再運行一次,結果如下:

我們發現,onClick方法不再執行了!為什麼會這樣呢?你可以先理解成onTouch方法返回true就認為這個事件被onTouch消費掉了,因而不會再繼續向下傳遞。

\


我們發現,onClick方法不再執行了!為什麼會這樣呢?你可以先理解成onTouch方法返回true就認為這個事件被onTouch消費掉了,因而不會再繼續向下傳遞。
 

\

然後我們來看一下View中dispatchTouchEvent方法的源碼:

 

    public boolean dispatchTouchEvent(MotionEvent event) {  
            if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
                    mOnTouchListener.onTouch(this, event)) {  
                return true;  
            }  
            return onTouchEvent(event);  
        }  

 

在這個方法裡面,先進行了一個判斷

第一個條件mOnTouchListener就是我們調用View的setTouchListener()方法設置的

第二個條件是判斷View是否為enabled的, View一般都是enabled,除非你手動設置為disabled

第三個條件就是OnTouchListener接口的onTouch()方法的返回值了,如果調用了setTouchListener()設置OnTouchListener,並且onTouch()方法返回true,View的dispatchTouchEvent()方法就直接返回true,否則就執行View的onTouchEvent() 並返回View的onTouchEvent()的值
現在你了解了View的onTouchEvent()方法和onTouch()的關系了吧,為什麼Android提供了處理Touch事件onTouchEvent()方法還要增加一個OnTouchListener接口呢?我覺得OnTouchListener接口是對處理Touch事件的屏蔽和擴展作用吧,屏蔽作用就是事件必須先到達onTouch,由是否在onTouch中消費掉事件(即返回true或者false來決定是否執行onTouchEvent),我就說下擴展吧,比如我們要打印View的Touch的點的坐標,我們可以自定義一個View如下

    public class CustomView extends View {  
          
        public CustomView(Context context, AttributeSet attrs) {  
            super(context, attrs);  
        }  
      
        public CustomView(Context context, AttributeSet attrs, int defStyle) {  
            super(context, attrs, defStyle);  
        }  
      
        @Override  
        public boolean onTouchEvent(MotionEvent event) {  
              
            Log.i("tag", "X的坐標 = " + event.getX() + " Y的坐標 = " + event.getY());  
              
            return super.onTouchEvent(event);  
        }  
      
    }  
也可以直接對View設置OnTouchListener接口,在return的時候調用下v.onTouchEvent()

 

    view.setOnTouchListener(new OnTouchListener() {  
                  
                @Override  
                public boolean onTouch(View v, MotionEvent event) {  
                      
                    Log.i("tag", "X的坐標 = " + event.getX() + " Y的坐標 = " + event.getY());  
                      
                    return v.onTouchEvent(event);  
                }  
      });  

我們再看View的onTouchEvent()方法
    public boolean onTouchEvent(MotionEvent event) {  
          final int viewFlags = mViewFlags;  
      
          if ((viewFlags & ENABLED_MASK) == DISABLED) {  
              return (((viewFlags & CLICKABLE) == CLICKABLE ||  
                      (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
          }  
      
          //如果設置了Touch代理,就交給代理來處理,mTouchDelegate默認是null  
          if (mTouchDelegate != null) {  
              if (mTouchDelegate.onTouchEvent(event)) {  
                  return true;  
              }  
          }  
      
          //如果View是clickable或者longClickable的onTouchEvent就返回true, 否則返回false  
          if (((viewFlags & CLICKABLE) == CLICKABLE ||  
                  (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
              switch (event.getAction()) {  
                  case MotionEvent.ACTION_UP:  
                      boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
                      if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
                          boolean focusTaken = false;  
                          if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
                              focusTaken = requestFocus();  
                          }  
      
                          if (!mHasPerformedLongPress) {  
                              removeLongPressCallback();  
      
                              if (!focusTaken) {  
                                  if (mPerformClick == null) {  
                                      mPerformClick = new PerformClick();  
                                  }  
                                  if (!post(mPerformClick)) {  
                                      performClick();  
                                  }  
                              }  
                          }  
      
                          if (mUnsetPressedState == null) {  
                              mUnsetPressedState = new UnsetPressedState();  
                          }  
      
                          if (prepressed) {  
                              mPrivateFlags |= PRESSED;  
                              refreshDrawableState();  
                              postDelayed(mUnsetPressedState,  
                                      ViewConfiguration.getPressedStateDuration());  
                          } else if (!post(mUnsetPressedState)) {  
                              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();  
      
                      //當手指在View上面滑動超過View的邊界,  
                      int slop = mTouchSlop;  
                      if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                              (y < 0 - slop) || (y >= getHeight() + slop)) {  
                          // Outside button  
                          removeTapCallback();  
                          if ((mPrivateFlags & PRESSED) != 0) {  
                              removeLongPressCallback();  
      
                              mPrivateFlags &= ~PRESSED;  
                              refreshDrawableState();  
                          }  
                      }  
                      break;  
              }  
              return true;  
          }  
      
          return false;  
      }  
這個方法也是比較長的,我們先看第4行,如果一個View是disabled, 並且該View是Clickable或者longClickable, onTouchEvent()就不執行下面的代碼邏輯直接返回true, 表示該View就一直消費Touch事件,如果一個enabled的View,並且是clickable或者longClickable的,onTouchEvent()會執行下面的代碼邏輯並返回true,綜上,一個clickable或者longclickable的View是一直消費Touch事件的,而一般的View既不是clickable也不是longclickable的(即不會消費Touch事件,只會執行ACTION_DOWN而不會執行ACTION_MOVE和ACTION_UP)Button是clickable的,可以消費Touch事件,但是我們可以通過setClickable()和setLongClickable()來設置View是否為clickable和longClickable。當然還可以通過重寫View的onTouchEvent()方法來控制Touch事件的消費與否

 

 

我們在看57行的ACTION_DOWN, 新建一個CheckForTap,我們看看CheckForTap是什麼

    private final class CheckForTap implements Runnable {  
           public void run() {  
               mPrivateFlags &= ~PREPRESSED;  
               mPrivateFlags |= PRESSED;  
               refreshDrawableState();  
               if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {  
                   postCheckForLongClick(ViewConfiguration.getTapTimeout());  
               }  
           }  
       }  

原來是個Runnable對象,然後使用Handler的post方法延時ViewConfiguration.getTapTimeout()執行CheckForTap的run()方法,在run方法中先判斷view是否longClickable的,一般的View都是false,postCheckForLongClick(ViewConfiguration.getTapTimeout())這段代碼就是執行長按的邏輯的代碼,只有當我們設置為longClickble才會去執行postCheckForLongClick(ViewConfiguration.getTapTimeout()),這裡我就不介紹了

由於考慮到文章篇幅的問題,我就不繼續分析View的長按事件和點擊事件了,在這裡我直接得出結論吧

長按事件是在ACTION_DOWN中執行,點擊事件是在ACTION_UP中執行,要想執行長按事件,這個View必須是longclickable的, 也許你會納悶,一般的View不是longClickable為什麼也會執行長按事件呢?我們要執行長按事件必須要調用setOnLongClickListener()設置OnLongClickListener接口,我們看看這個方法的源碼

    public void setOnLongClickListener(OnLongClickListener l) {  
         if (!isLongClickable()) {  
             setLongClickable(true);  
         }  
         mOnLongClickListener = l;  
     }  
如果這個View不是longClickable的,我們就調用setLongClickable(true)方法設置為longClickable的,所以才會去執行長按方法onLongClick();

要想執行點擊事件,這個View就必須要消費ACTION_DOWN和ACTION_MOVE事件,並且沒有設置OnLongClickListener的情況下,如果設置了OnLongClickListener的情況下,需要onLongClick()返回false才能執行到onClick()方法,也許你又會納悶,一般的View默認是不消費touch事件的,這不是和你上面說的相違背嘛,我們要向執行點擊事件必須要調用setOnClickListener()來設置OnClickListener接口,我們看看這個方法的源碼就知道了

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

所以說一個enable的並且是clickable的View是一直消費touch事件的,所以才會執行到onClick()方法

 

 

 

對於View的Touch事件的分發機制算是告一段落了,從上面我們可以得出TextView的dispatchTouchEvent()的返回false的,即不消費Touch事件。我們就要往上看RelativeLayout的dispatchTouchEvent()方法的51行,由於TextView.dispatchTouchEvent()為false, 導致mMotionTarget沒有被賦值,還是null, 繼續往下走執行RelativeLayout的dispatchTouchEvent()方法, 來到第84行, 判斷target是否為null,這個target就是mMotionTarget,滿足條件,執行92行的 super.dispatchTouchEvent(ev)代碼並返回, 這裡調用的是RelativeLayout父類View的dispatchTouchEvent()方法,由於RelativeLayout沒有設置onTouchListener, 所以這裡直接調用RelativeLayout(其實就是View, 因為RelativeLayout沒有重寫onTouchEvent())的onTouchEvent()方法 由於RelativeLayout既不是clickable的也是longClickable的,所以其onTouchEvent()方法false, RelativeLayout的dispatchTouchEvent()也是返回false,這裡就執行完了RelativeLayout的dispatchTouchEvent()方法

繼續執行FrameLayout的dispatchTouchEvent()的第51行,由於RelativeLayout.dispatchTouchEvent()返回的是false, 跟上面的邏輯是一樣的, 也是執行到92行的super.dispatchTouchEvent(ev)代碼並返回,然後執行FrameLayout的onTouchEvent()方法,而FrameLayout的onTouchEvent()也是返回false,所以FrameLayout的dispatchTouchEvent()方法返回false,執行完畢FrameLayout的dispatchTouchEvent()方法

在上面的我就不分析了,大家自行分析一下,跟上面的邏輯是一樣的,我直接畫了個圖來幫大家理解下(這裡沒有畫出onInterceptTouchEvent()方法)

\

 

所以我們點擊屏幕上面的TextView的事件分發流程是上圖那個樣子的,表示Activity的View都不消費ACTION_DOWN事件,所以就不能在觸發ACTION_MOVE, ACTION_UP等事件了,具體是為什麼?我還不太清楚,畢竟從Activity到TextView這一層是分析不出來的,估計是在底層實現的。

但如果將TextView換成Button,流程是不是還是這個樣子呢?答案不是,我們來分析分析一下,如果是Button , Button是一個clickable的View,onTouchEvent()返回true, 表示他一直消費Touch事件,所以Button的dispatchTouchEvent()方法返回true, 回到RelativeLayout的dispatchTouchEvent()方法的51行,滿足條件,進入到if方法體,設置mMotionTarget為Button,然後直接返回true, RelativeLayout的dispatchTouchEvent()方法執行完畢,不會調用到RelativeLayout的onTouchEvent()方法

然後到FrameLayout的dispatchTouchEvent()方法的51行,由於RelativeLayout.dispatchTouchEvent()返回true, 滿足條件,進入if方法體,設置mMotionTarget為RelativeLayout,注意下,這裡的mMotionTarget跟RelativeLayout的dispatchTouchEvent()方法的mMotionTarget不是同一個哦,因為他們是不同的方法中的,然後返回true

同理FrameLayout的dispatchTouchEvent()也是返回true,DecorView的dispatchTouchEvent()方法也返回true, 還是畫一個流程圖(這裡沒有畫出onInterceptTouchEvent()方法)給大家理清下

\

 

從上面的流程圖得出一個結論,Touch事件是從頂層的View一直往下分發到手指按下的最裡面的View,如果這個View的onTouchEvent()返回false,即不消費Touch事件,這個Touch事件就會向上找父布局調用其父布局的onTouchEvent()處理,如果這個View返回true,表示消費了Touch事件,就不調用父布局的onTouchEvent()

接下來我們用一個自定義的ViewGroup來替換RelativeLayout,自定義ViewGroup代碼如下

 

    package com.example.androidtouchevent;  
      
    import android.content.Context;  
    import android.util.AttributeSet;  
    import android.view.MotionEvent;  
    import android.widget.RelativeLayout;  
      
    public class CustomLayout extends RelativeLayout {  
          
        public CustomLayout(Context context, AttributeSet attrs) {  
            super(context, attrs, 0);  
        }  
      
        public CustomLayout(Context context, AttributeSet attrs, int defStyle) {  
            super(context, attrs, defStyle);  
        }  
      
        @Override  
        public boolean onTouchEvent(MotionEvent event) {  
            return super.onTouchEvent(event);  
        }  
      
        @Override  
        public boolean onInterceptTouchEvent(MotionEvent ev) {  
            return true;  
        }  
          
      
    }  

我們就重寫了onInterceptTouchEvent(),返回true, RelativeLayout默認是返回false, 然後再CustomLayout布局中加一個Button ,如下圖

 

\

 

我們這次不從DecorView的dispatchTouchEvent()分析了,直接從CustomLayout的dispatchTouchEvent()分析

我們先看ACTION_DOWN 來到25行,由於我們重寫了onInterceptTouchEvent()返回true, 所以不走這個if裡面,直接往下看代碼,來到84行, target為null,所以進入if方法裡面,直接調用super.dispatchTouchEvent()方法, 也就是View的dispatchTouchEvent()方法,而在View的dispatchTouchEvent()方法中是直接調用View的onTouchEvent()方法,但是CustomLayout重寫了onTouchEvent(),所以這裡還是調用CustomLayout的onTouchEvent(), 這個方法返回false, 不消費Touch事件,所以不會在觸發ACTION_MOVE,ACTION_UP等事件了,這裡我再畫一個流程圖吧(含有onInterceptTouchEvent()方法的)


\

 

好了,就分析到這裡吧,差不多分析完了,還有一種情況沒有分析到,例如我將CustomLayout的代碼改成下面的情形,Touch事件又是怎麼分發的呢?

 

    package com.example.androidtouchevent;  
      
    import android.content.Context;  
    import android.util.AttributeSet;  
    import android.view.MotionEvent;  
    import android.widget.RelativeLayout;  
      
    public class CustomLayout extends RelativeLayout {  
          
        public CustomLayout(Context context, AttributeSet attrs) {  
            super(context, attrs, 0);  
        }  
      
        public CustomLayout(Context context, AttributeSet attrs, int defStyle) {  
            super(context, attrs, defStyle);  
        }  
      
        @Override  
        public boolean onTouchEvent(MotionEvent event) {  
            return super.onTouchEvent(event);  
        }  
      
        @Override  
        public boolean onInterceptTouchEvent(MotionEvent ev) {  
            if(ev.getAction() == MotionEvent.ACTION_MOVE){  
                return true;  
            }  
            return super.onInterceptTouchEvent(ev);  
        }  
          
      
    }  
我的理解是:當事件來到CustomLayout時,一般來說,onInterceptTouchEvent方法是直接返回false的,現在就多了一些條件而已,即是當用戶ACTION_DOWN之後,繼續在屏幕ACTION_MOVE的時候(在滑動,還沒抬起),就返回true,也就是說當前的CustomLayout把事件攔截了下來,其中的onTouchEvent方法裡面的邏輯會得到執行,事件不再向後分發。

 

(一個重要結論,哪裡onTouchEvent返回了true,哪裡的各種action就會得到執行,其他的就不會執行各種action邏輯,如果返回false,則執行父類的onTouchEvent。如有誤,望不惜賜教)

 

 

\

 

\

 

 

 

這篇文章的篇幅有點長,如果你想了解Touch事件的分發機制,你一定要認真看完,下面來總結一下吧

 

1.Activity的最頂層Window是PhoneWindow,PhoneWindow的最頂層View是DecorView

2.一個clickable或者longClickable的View會永遠消費Touch事件,不管他是enabled還是disabled的

3.View的長按事件是在ACTION_DOWN中執行,要想執行長按事件該View必須是longClickable的,並且不能產生ACTION_MOVE

4.View的點擊事件是在ACTION_UP中執行,想要執行點擊事件的前提是消費了ACTION_DOWN和ACTION_MOVE,並且沒有設置OnLongClickListener的情況下,如設置了OnLongClickListener的情況,則必須使onLongClick()返回false

5.如果View設置了onTouchListener了,並且onTouch()方法返回true,則不執行View的onTouchEvent()方法,也表示View消費了Touch事件,返回false則繼續執行onTouchEvent()

6.Touch事件是從最頂層的View一直分發到手指touch的最裡層的View,如果最裡層View消費了ACTION_DOWN事件(設置onTouchListener,並且onTouch()返回true 或者onTouchEvent()方法返回true)才會觸發ACTION_MOVE,ACTION_UP的發生,如果某個ViewGroup攔截了Touch事件,則Touch事件交給ViewGroup處理

7.Touch事件的分發過程中,如果消費了ACTION_DOWN,而在分發ACTION_MOVE的時候,某個ViewGroup攔截了Touch事件,就像上面那個自定義CustomLayout,則會將ACTION_CANCEL分發給該ViewGroup下面的Touch到的View,然後將Touch事件交給ViewGroup處理,並返回true

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved