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之间是一个递归的形式,不断的遍历视图层次结构!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容