Android Touch事件分發機制詳解之由點擊引發的戰爭

更多分享:http://www.cherylgood.cn

  • 之前我們在Scroller的使用詳解中,在onMeasure方法中可能你會看到 childView.setClickable(true);為什么要設置childView為true呢,假如不設置的話,你會發現ACTION_MOVE并沒有執行。為什么會出現這樣的問題呢?此時我是一臉懵逼的,要想徹底搞明白,對于Android事件分發機制的了解是必不可少的。

  • 首先我們先來一個測試。


    QQ20170418-162600.png

    如上圖,我們activity里只有一個button
    代碼如下:


package guanaj.com.scrollerdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Scroller;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";
private Scroller scroller;
private LinearLayout llContent;
private Button mButton;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);


    mButton = (Button) findViewById(R.id.m_button);
    mButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.i(TAG,"#onClick 我被點擊了");
        }
    });
    mButton.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {

            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:{
                    Log.i(TAG,"#onTouch ACTION_DOWN ");
                    break;
                }
                case MotionEvent.ACTION_MOVE:{
                    Log.i(TAG,"#onTouch ACTION_MOVE ");
                    break;
                }
                case MotionEvent.ACTION_UP:{
                    Log.i(TAG,"#onTouch ACTION_UP ");
                    break;
                }
                default:{
                    Log.i(TAG,"#onTouch");
                }
            }
            return false;
        }
    });
}

然后我們點擊一下:

04-18 16:23:20.022 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_DOWN
04-18 16:23:20.052 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.062 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.082 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.132 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.132 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.152 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.162 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:23:20.172 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_UP
04-18 16:23:20.192 30413-30413/guanaj.com.scrollerdemo I/MainActivity: #onClick 我被點擊了

  • -||手抖了一下,出現了幾個MOVE
  • 從log里我們能得到一下信息:
    1、 OnTouch事件是優先于OnClick事件的,也就是說點擊事件會先傳遞到OnTouch,然后才會傳遞到OnClick。
    2、ACTION_DOWN 與ACTION_UP只會發生一次,也就是手指壓下去ACTION_DOWN,手指松開ACTION_UP,此時恰巧手抖了,出現了多個ACTION_MOVE。
    3、ACTION_UP后OnClick事件才會觸發。

可能你會發現,onClick回調方法沒有返回值,OnTouch回調方法返回個false?懵逼的我嘗試著把false變成true........懵逼的事情發生了。
log


04-18 16:35:42.672 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_DOWN
04-18 16:35:42.702 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.722 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.732 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.762 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.772 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_MOVE
04-18 16:35:42.772 1294-1294/guanaj.com.scrollerdemo I/MainActivity: #onTouch ACTION_UP

懵逼的OnClick事件沒有觸發~~懵逼的我只能為了滿足小小的好奇心,到網上查了些資料以及查看了源碼:

源碼追逐戰開始了:


1、首先從android系統的機制入手:我們的按鈕是在activity里面的,而我們的activity是怎么來的呢,這里我們要引出一個類,Window:官方描述:

  • Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, title area, default key processing, etc. The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.
  • 定義頂級窗口外觀和行為策略的一個抽象基類。 應該使用這個類的實例作為添加到窗口管理器的頂級視圖。 它提供標準的UI策略,如背景,標題區域,默認的鍵處理等。這個抽象類的唯一現有實現是android.view.PhoneWindow,當你需要一個窗口應該實例化它。

2、線索轉移,現在開始追蹤PhoenWindow,一直追到Activity.java,此時在Activity.java 的源碼中看到如下代碼:

public class Activity extends ContextThemeWrapper
    implements LayoutInflater.Factory2,
    Window.Callback, KeyEvent.Callback,
    OnCreateContextMenuListener, ComponentCallbacks2,
    Window.OnWindowDismissedCallback, WindowControllerCallback {

看到了Window的字樣,感覺應該是他了,然后從attach函數中看到:

  final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
        CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor,
        Window window) {
    attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this, window);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);

結論:phoneWindow其實是創建了activity之后才創建它的,PhoneWindow作為activity的窗口頂級類進行存在,Activity展示的內容也是通過PhoneWindow來設置的,此時可以回想activity中的setContentView(R.layout.activity_main);繼續追源碼可以看到,調用了window的setContentView方法,而前面說過,window的唯一實現類是PhoneWindow。

  public void setContentView(@LayoutRes int layoutResID) {
 getWindow().setContentView(layoutResID);  //調用getWindow方法,返回mWindow
 initWindowDecorActionBar();
}
...
public Window getWindow() {   
 return mWindow;
}  

此時開始猜想,當activity里面的元素被點擊時,事件的轉發第一個會不會是傳遞給PhoneWindow呢?其實PhoneWindow中存在一個內部類DecorView對象.....好像有點偏題了,好吧,直接給出PhoneWindow的追蹤結論:


未命名.jpg
  • 左邊的frameLayout放的是我們的系統狀態欄,右邊的frameLayout存放的就是我們在activity中setContentView的布局內容了。
  • 我們從activity的dispatchTouchEvent源碼里也能獲得準確的信息:

     public boolean dispatchTouchEvent(MotionEvent ev) {
      if (ev.getAction() == MotionEvent.ACTION_DOWN) {
          onUserInteraction();
      }
      if (getWindow().superDispatchTouchEvent(ev)) {
          return true;
      }
      return onTouchEvent(ev);
     }
    
  • 從上面代碼可以看到,activity的dispatchTouchEvent里面先調用里window的superDispatchTouchEvent方法,如果event沒被消費掉,就會調用自己的onTouchEvent方法


  • 重點轉移到getWindow().superDispatchTouchEvent(),getWindow()返回當前Activity的頂層窗口Window(PhoneWindow)對象,我們直接看Window API的superDispatchTouchEvent()方法

    @Override  
    public boolean superDispatchTouchEvent(MotionEvent event) {  
      return mDecor.superDispatchTouchEvent(event);  
    }  
    
  • 里面直接調用了DecorView類的superDispatchTouchEvent(),DecorView是PhoneWindow的一個final的內部類并且繼承FrameLayout的,也是Window界面的最頂層的View對象,其實decorView是在activity的setContentView方法里面構建的,我們


  • 繼續看源碼:

    @Override  
    public void setContentView(int layoutResID) {  
    if (mContentParent == null) {  
        installDecor();  
    } else {  
        mContentParent.removeAllViews();  
    }  
    mLayoutInflater.inflate(layoutResID, mContentParent);  
    final Callback cb = getCallback();  
    if (cb != null && !isDestroyed()) {  
        cb.onContentChanged();  
    }  
    }  
    
  • 可以看到,在渲染我們的布局文件前,先調用了installDecor();


  • 我們繼續看installDecor源碼:

    private void installDecor() {  
      if (mDecor == null) {  
          mDecor = generateDecor();  
          mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);  
          mDecor.setIsRootNamespace(true);  
          if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {  
              mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);  
          }  
      }  
      if (mContentParent == null) {  
          mContentParent = generateLayout(mDecor);  
    
          // Set up decor part of UI to ignore fitsSystemWindows if appropriate.  
          mDecor.makeOptionalFitsSystemWindows();  
    
          mTitleView = (TextView)findViewById(com.android.internal.R.id.title);  
          if (mTitleView != null) {  
              mTitleView.setLayoutDirection(mDecor.getLayoutDirection());  
              if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {  
                  View titleContainer = findViewById(com.android.internal.R.id.title_container);  
                  if (titleContainer != null) {  
                      titleContainer.setVisibility(View.GONE);  
                  } else {  
                      mTitleView.setVisibility(View.GONE);  
                  }  
                  if (mContentParent instanceof FrameLayout) {  
                      ((FrameLayout)mContentParent).setForeground(null);  
                  }  
              } else {  
                  mTitleView.setText(mTitle);  
              }  
          } else {  
              mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);  
              if (mActionBar != null) {  
                  mActionBar.setWindowCallback(getCallback());  
                  if (mActionBar.getTitle() == null) {  
                      mActionBar.setWindowTitle(mTitle);  
                  }  
                  final int localFeatures = getLocalFeatures();  
                  if ((localFeatures & (1 << FEATURE_PROGRESS)) != 0) {  
                      mActionBar.initProgress();  
                  }  
                  if ((localFeatures & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {  
                      mActionBar.initIndeterminateProgress();  
                  }  
    
                  boolean splitActionBar = false;  
                  final boolean splitWhenNarrow =  
                          (mUiOptions & ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW) != 0;  
                  if (splitWhenNarrow) {  
                      splitActionBar = getContext().getResources().getBoolean(  
                              com.android.internal.R.bool.split_action_bar_is_narrow);  
                  } else {  
                      splitActionBar = getWindowStyle().getBoolean(  
                              com.android.internal.R.styleable.Window_windowSplitActionBar, false);  
                  }  
                  final ActionBarContainer splitView = (ActionBarContainer) findViewById(  
                          com.android.internal.R.id.split_action_bar);  
                  if (splitView != null) {  
                      mActionBar.setSplitView(splitView);  
                      mActionBar.setSplitActionBar(splitActionBar);  
                      mActionBar.setSplitWhenNarrow(splitWhenNarrow);  
    
                      final ActionBarContextView cab = (ActionBarContextView) findViewById(  
                              com.android.internal.R.id.action_context_bar);  
                      cab.setSplitView(splitView);  
                      cab.setSplitActionBar(splitActionBar);  
                      cab.setSplitWhenNarrow(splitWhenNarrow);  
                  } else if (splitActionBar) {  
                      Log.e(TAG, "Requested split action bar with " +  
                              "incompatible window decor! Ignoring request.");  
                  }  
    
                  // Post the panel invalidate for later; avoid application onCreateOptionsMenu  
                  // being called in the middle of onCreate or similar.  
                  mDecor.post(new Runnable() {  
                      public void run() {  
                          // Invalidate if the panel menu hasn't been created before this.  
                          PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);  
                          if (!isDestroyed() && (st == null || st.menu == null)) {  
                              invalidatePanelMenu(FEATURE_ACTION_BAR);  
                          }  
                      }  
                  });  
              }  
          }  
      }  
    }  
    
  • 從代碼可以看到:

  • mContentParent = generateLayout(mDecor); 使用DecorView創建contentparent,而從mContentParent instanceof FrameLayout 得到contentparent繼承自framelayout,那么DecorView類的superDispatchTouchEvent()應該就是framelayout的DispatchTouchEvent方法,我們看framelayout

  • ~~ 然而framelayout并沒有DispatchTouchEvent方法,而framelayout繼承自viewgroup,那么肯定在viewgroup里面了。

  • 接下來我們就看DecorView類的superDispatchTouchEvent()方法

    public boolean superDispatchTouchEvent(MotionEvent event)
       { 
         return super.dispatchTouchEvent(event);  
     }  
    
  • 在里面調用了父類FrameLayout的dispatchTouchEvent()方法,而FrameLayout中并沒有dispatchTouchEvent()方法,所以我們直接看ViewGroup的dispatchTouchEvent()方法,繼續看源碼:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
      if (mInputEventConsistencyVerifier != null) {
          mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
      }
    
      // If the event targets the accessibility focused view and this is it, start
      // normal event dispatch. Maybe a descendant is what will handle the click.
      if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
          ev.setTargetAccessibilityFocus(false);
      }
    
      boolean handled = false;
      if (onFilterTouchEventForSecurity(ev)) {
          final int action = ev.getAction();
          final int actionMasked = action & MotionEvent.ACTION_MASK;
    
          // 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();
          }
    
          // Check for interception.
          final boolean intercepted;
          if (actionMasked == MotionEvent.ACTION_DOWN
                  || mFirstTouchTarget != null) {
              final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
              if (!disallowIntercept) {
                  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;
          }
    
          // If intercepted, start normal event dispatch. Also if there is already
          // a view that is handling the gesture, do normal event dispatch.
          if (intercepted || mFirstTouchTarget != null) {
              ev.setTargetAccessibilityFocus(false);
          }
    
          // Check for cancelation.
          final boolean canceled = resetCancelNextUpFlag(this)
                  || actionMasked == MotionEvent.ACTION_CANCEL;
    
          // Update list of touch targets for pointer down, if needed.
          final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
          TouchTarget newTouchTarget = null;
          boolean alreadyDispatchedToNewTouchTarget = false;
          if (!canceled && !intercepted) {
    
              // If the event is targeting accessiiblity focus we give it to the
              // view that has accessibility focus and if it does not handle it
              // we clear the flag and dispatch the event to all children as usual.
              // We are looking up the accessibility focused host to avoid keeping
              // state since these events are very rare.
              View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                      ? findChildWithAccessibilityFocus() : null;
    
              if (actionMasked == MotionEvent.ACTION_DOWN
                      || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                      || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                  final int actionIndex = ev.getActionIndex(); // always 0 for down
                  final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                          : TouchTarget.ALL_POINTER_IDS;
    
                  // Clean up earlier touch targets for this pointer id in case they
                  // have become out of sync.
                  removePointersFromTouchTargets(idBitsToAssign);
    
                  final int childrenCount = mChildrenCount;
                  if (newTouchTarget == null && childrenCount != 0) {
                      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;
                      for (int i = childrenCount - 1; i >= 0; i--) {
                          final int childIndex = getAndVerifyPreorderedIndex(
                                  childrenCount, i, customOrder);
                          final View child = getAndVerifyPreorderedView(
                                  preorderedList, children, childIndex);
    
                          // If there is a view that has accessibility focus we want it
                          // to get the event first and if not handled we will perform a
                          // normal dispatch. We may do a double iteration but this is
                          // safer given the timeframe.
                          if (childWithAccessibilityFocus != null) {
                              if (childWithAccessibilityFocus != child) {
                                  continue;
                              }
                              childWithAccessibilityFocus = null;
                              i = childrenCount - 1;
                          }
    
                          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);
                          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();
                              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();
                  }
    
                  if (newTouchTarget == null && mFirstTouchTarget != null) {
                      // Did not find a child to receive the event.
                      // Assign the pointer to the least recently added target.
                      newTouchTarget = mFirstTouchTarget;
                      while (newTouchTarget.next != null) {
                          newTouchTarget = newTouchTarget.next;
                      }
                      newTouchTarget.pointerIdBits |= idBitsToAssign;
                  }
              }
          }
    
          // 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);
          } else {
              // Dispatch to touch targets, excluding the new touch target if we already
              // dispatched to it.  Cancel touch targets if necessary.
              TouchTarget predecessor = null;
              TouchTarget target = mFirstTouchTarget;
              while (target != null) {
                  final TouchTarget next = target.next;
                  if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                      handled = true;
                  } else {
                      final boolean cancelChild = resetCancelNextUpFlag(target.child)
                              || intercepted;
                      if (dispatchTransformedTouchEvent(ev, cancelChild,
                              target.child, target.pointerIdBits)) {
                          handled = true;
                      }
                      if (cancelChild) {
                          if (predecessor == null) {
                              mFirstTouchTarget = next;
                          } else {
                              predecessor.next = next;
                          }
                          target.recycle();
                          target = next;
                          continue;
                      }
                  }
                  predecessor = target;
                  target = next;
              }
          }
    
          // Update list of touch targets for pointer up or cancel, if needed.
          if (canceled
                  || actionMasked == MotionEvent.ACTION_UP
                  || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
              resetTouchState();
          } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
              final int actionIndex = ev.getActionIndex();
              final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
              removePointersFromTouchTargets(idBitsToRemove);
          }
      }
    
      if (!handled && mInputEventConsistencyVerifier != null) {
          mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
      }
      return handled;
      }
    
  • 重點語句:先檢測是否允許攔截touch事件,disallowIntercept 為false表示允許攔截,然后調用onInterceptTouchEvent 看是否真的要攔截。所以如果想讓父view不攔截我們的touch事件,一般通過requestDisallowInterceptTouchEvent 設置disallowIntercept的值來控制。
    // Check for interception.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
    || mFirstTouchTarget != null) {
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
    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;
    }

  • 判斷了攔截之后,就會便利可以接收touch事件的child view。將事件分發下去:
    for (int i = childrenCount - 1; i >= 0; i--) {
    ...
    //只轉發給focus的childview
    if (childWithAccessibilityFocus != null) {
    if (childWithAccessibilityFocus != child) {
    continue;
    }
    childWithAccessibilityFocus = null;
    i = childrenCount - 1;
    }
    ......
    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    .......
    break;
    }

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

  • 我們繼續看dispatchTransformedTouchEvent這個方法:

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
          View child, int desiredPointerIdBits) {
      final boolean handled;
    
      // Canceling motions is a special case.  We don't need to perform any transformations
      // or filtering.  The important part is the action, not the contents.
      final int oldAction = event.getAction();
      if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
          event.setAction(MotionEvent.ACTION_CANCEL);
          if (child == null) {
              handled = super.dispatchTouchEvent(event);
          } else {
              handled = child.dispatchTouchEvent(event);
          }
          event.setAction(oldAction);
          return handled;
      }
    
      // Calculate the number of pointers to deliver.
      final int oldPointerIdBits = event.getPointerIdBits();
      final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    
      // If for some reason we ended up in an inconsistent state where it looks like we
      // might produce a motion event with no pointers in it, then drop the event.
      if (newPointerIdBits == 0) {
          return false;
      }
    
      // If the number of pointers is the same and we don't need to perform any fancy
      // irreversible transformations, then we can reuse the motion event for this
      // dispatch as long as we are careful to revert any changes we make.
      // Otherwise we need to make a copy.
      final MotionEvent transformedEvent;
      if (newPointerIdBits == oldPointerIdBits) {
          if (child == null || child.hasIdentityMatrix()) {
              if (child == null) {
                  handled = super.dispatchTouchEvent(event);
              } else {
                  final float offsetX = mScrollX - child.mLeft;
                  final float offsetY = mScrollY - child.mTop;
                  event.offsetLocation(offsetX, offsetY);
    
                  handled = child.dispatchTouchEvent(event);
    
                  event.offsetLocation(-offsetX, -offsetY);
              }
              return handled;
          }
          transformedEvent = MotionEvent.obtain(event);
      } else {
          transformedEvent = event.split(newPointerIdBits);
      }
    
      // Perform any necessary transformations and dispatch.
      if (child == null) {
          handled = super.dispatchTouchEvent(transformedEvent);
      } else {
          final float offsetX = mScrollX - child.mLeft;
          final float offsetY = mScrollY - child.mTop;
          transformedEvent.offsetLocation(offsetX, offsetY);
          if (! child.hasIdentityMatrix()) {
              transformedEvent.transform(child.getInverseMatrix());
          }
    
          handled = child.dispatchTouchEvent(transformedEvent);
      }
    
      // Done.
      transformedEvent.recycle();
      return handled;
    

    }

  • 可以看到,里面判斷childview是否null,是的話調用super.dispatchTouchEvent繼續講事件分發出去,也就是假如該viewgroup沒有childview 了,就分發給自己,否則調用的childview的dispatchTouchEvent方法,然后就消費結果返回;
    -我們繼續看childview的dispatchTouchEvent方法:

    public boolean dispatchTouchEvent(MotionEvent event) {
           ......
          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;
      }
    
  • 我挑出了方法中重要的代碼:

  • 1、先判斷mOnTouchListener 、mViewFlags & ENABLED_MASK== ENABLED、mOnTouchListener.onTouch(this, event)如果都為true,就直接返回true,否則執行onTouchEvent(event)方法并返回。

  • mOnTouchListener 其實就是我們設置的onTouchListener。所以如果view設置了onTouchListener就會先調用onTouchListener方法,(mViewFlags & ENABLED_MASK) == ENABLED是判斷當前點擊的控件是否是enable的,如果是button這類控件默認是開啟的;

  • 結論,如果執行了onTouch且在onTouch里返回true,onTouchEvent方法就不會再執行了;


小結:

  • 1、首先Activity接收到touch事件后在dispatchTouchEvent方法中會調用DecorView的superDispatchTouchEvent方法;
  • 2、DecorView最終調用的是viewGroup的3、dispatchTouchEvent方法;
  • 3、在viewgroup的dispatchTouchEvent方法中會做判斷:
  • 1)檢查disallowIntercept
  • 2)disallowIntercept允許攔截:檢查onInterceptTouchEvent是否真的要攔截,如果攔截則event不再分發下去,調用自己的dispatchTouchEvent方法,如果不攔截,則調用ViewGroup自己dispatchTransformedTouchEvent方法,
  • 3)在dispatchTransformedTouchEvent里又做了兩件事,如果有childview,就調用childview的dispatchTouchEvent方法將事件分發下去,如果沒有childview,仍然是調用自己的dispatchTouchEvent方法。
    -4)所以調用view的dispatchTouchEvent有兩種情況,一種是攔截了,不傳給子view了,一種是沒有子view了;

  • 從我們demo的測試結果可以知道,先執行了onTouchEvent才執行onClick,而在view的dispatchTouchEvent里并沒有看到onClick相關的代碼,所以onClick肯定在onTouchEvent中被調用的,所以我們繼續看onTouchEvent方法的源碼:

     public boolean onTouchEvent(MotionEvent event) {
      final float x = event.getX();
      final float y = event.getY();
      final int viewFlags = mViewFlags;
      final int action = event.getAction();
    
      if ((viewFlags & ENABLED_MASK) == DISABLED) {
          if (action == 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)
                  || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
      }
      if (mTouchDelegate != null) {
          if (mTouchDelegate.onTouchEvent(event)) {
              return true;
          }
      }
    
      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 (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 && !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();
                              }
                          }
                      }
    
                      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();
                  }
                  mIgnoreNextUpEvent = false;
                  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, x, y);
                  }
                  break;
    
              case MotionEvent.ACTION_CANCEL:
                  setPressed(false);
                  removeTapCallback();
                  removeLongPressCallback();
                  mInContextButtonPress = false;
                  mHasPerformedLongPress = false;
                  mIgnoreNextUpEvent = false;
                  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;
    

    }

  • 從代碼里發現了個很好玩的東西mTouchDelegate:

    if (mTouchDelegate != null) {
          if (mTouchDelegate.onTouchEvent(event)) {
              return true;
          }
      }
    
  • 也就是說,我們可以給控件設置mTouchDelegate來進行touch事件的攔截,小小的發現;

  • 在調用來mTouchDelegate的代碼之后,如果沒有攔截掉,就開始完成點擊事件的操作了,

  • 首先判斷控件是否是可點擊的。如果是就進入switch判斷中去;重點看case MotionEvent.ACTION_UP:經過各種判斷后最終會調用 performClick();

    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;
      }   sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
      return result;
    }
    
  • 可以看到, 我們設置了onClickListener,就會調用onClick方法,并且返回true

  • 重點,在switch語句最后會返回個ture,所以你會發現,只要執行了switch,最后都會返回true;為什么會返回true呢?其實這里涉及到touch事件的層級傳遞:

當給控件注冊了touch事件后,每次點擊它的時候都會觸ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件;但是,一旦你在執行某一個action的時候返回了false,后面的action就不會再執行了。假如你在ACTION_DOWN返回false,那么后面的都不會在執行了。也就是說,在dispatchTouchEvent進行事件分發的時候,只有前一個action返回true,才會觸發下一個action;

  • 我們再看一次View.dispatchTouchEvent代碼;

    public boolean dispatchTouchEvent(MotionEvent event) {
     ......
    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;
    }
    
  • 可以看到,onTouchEvent(event)返回true,最終dispatchTouchEvent才會返回true,所以,我們如果想action繼續傳遞下去,onTouchEvent就要返回true;而onTouch方法中返回true或者false只是控制是否要調用onTouchEvent方法進而影響onclick方法的調用而已。這也正是我們onTouchEvent方法最終總會返回true的原因。


總結:

  • onTouch能夠得到執行需要兩個前提條件,
    • 1、mOnTouchListener不為空
    • 2、被點擊的控件的enable必須時true
  • 所以enable為false的控制永遠響應不了onTouch事件
  • 如果被點擊的控件是不可點擊的,那么就進入不了onTouchEvent里的switch里面,返回false,導致后面的action都接收不到,這就是為什么在使用一些不帶點擊能力的控件時,我們需要設置clickable為true,使其可以被點擊
  • 可通過調用requestDisallowInterceptTouchEvent設置父控件是否攔截touch事件
  • 可通過onInterceptTouchEvent設置自己是否攔截touch事件,組織其繼續分發到子view中;
ontouch.png

從圖中我們可以看到,在viewgroup跟view之間是一個遞歸的形式,不斷的遍歷視圖層次結構!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容