Android下Touch事件的分发机制

pgl198 7年前
   <p>我们通过一个示例来分析Touch事件的分发过程。</p>    <p><strong>示例:</strong></p>    <p>布局文件:</p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"      xmlns:tools="http://schemas.android.com/tools"      android:id="@+id/rootview"      android:layout_width="match_parent"      android:layout_height="match_parent"      android:orientation="vertical"      tools:context="com.example.maimingliang.test.view.TestTouchActivity">            <TextView          android:id="@+id/txt"          android:layout_width="match_parent"          android:gravity="center"          android:layout_height="55dp"          android:text="textView"/>          <ImageView          android:id="@+id/img"          android:layout_marginTop="20dp"          android:layout_width="wrap_content"          android:layout_height="wrap_content"          android:background="@mipmap/ic_launcher"/>      </LinearLayout></code></pre>    <p>Activity:</p>    <pre>  <code class="language-java">public class TestTouchActivity extends AppCompatActivity {        private static final String TAG = "TestTouchActivity";      @Bind(R.id.txt)      TextView tv;      @Bind(R.id.img)      ImageView img;        @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState);          setContentView(R.layout.activity_test_touch);          ButterKnife.bind(this);          initView();      }        private void initView() {            tv.setOnClickListener(new View.OnClickListener() {              @Override              public void onClick(View v) {                  Log.d(TAG,"-------> tv Onclick");              }          });            tv.setOnTouchListener(new View.OnTouchListener() {              @Override              public boolean onTouch(View v, MotionEvent event) {                    Log.d(TAG, "-------> tv onTouch");                  return false;              }          });              img.setOnClickListener(new View.OnClickListener() {              @Override              public void onClick(View v) {                  Log.d(TAG, "--------> img onClick");              }          });            img.setOnTouchListener(new View.OnTouchListener() {              @Override              public boolean onTouch(View v, MotionEvent event) {                  Log.d(TAG, "--------> img onTouch");                    return true;              }          });      }</code></pre>    <p>点击图片,现象</p>    <p><img src="https://simg.open-open.com/show/3c94c7a6ba88fb07c2da73d51122858f.png"></p>    <p style="text-align:center">这里写图片描述</p>    <p>可以看到onTouch事件比onClick事件优先级高。</p>    <p>再看看把setOnTouchListener事件的返回值改为true:</p>    <p><img src="https://simg.open-open.com/show/51afbad157f963283b7b5bfd144ffab0.png"></p>    <p style="text-align:center">这里写图片描述</p>    <p>可以看到onClick事件没有了。这是为什么?我们透过源码来看看这个现象。</p>    <h3><strong>事件分发机制源码分析</strong></h3>    <p>当我们触摸屏幕上的某个控件时,底层的设备硬件传递给InputManager经过一 定的处理后,传递给AmS,再经过AmS的处理后就传递到我们的Activity,接着传递Window,最后传递到顶级View。</p>    <p>触摸事件的分发过程有三个重要的方法:</p>    <p>public boolean dispatchTouchEvent(MotionEvent ev)</p>    <p>用来分发事件的,如果当前事件能传递到该View,该 方法一定调用,View的onTouchEvent方法会调用,而该方法的返回值所onTouchEvent影响。</p>    <p>public boolean onInterceptHoverEvent(MotionEvent event)</p>    <p>用来拦截事件的,如果返回值为true,表示拦截。否则不拦截。</p>    <p>public boolean onTouchEvent(MotionEvent event)</p>    <p>处理当前事件的。如果返回值为true表示消耗该事件。否则无法再接收同一个序列的事件。</p>    <p>同一个序列的事件是;DOWN事件--》多个MOVE事件--》UP事件。</p>    <p>Activity触摸事件分发过程</p>    <p>当触摸事件传递到Activity,Activity的dispatchTouchEvent()方法就会调用,我们去看看:</p>    <pre>  <code class="language-java">public boolean dispatchTouchEvent(MotionEvent ev) {          if (ev.getAction() == MotionEvent.ACTION_DOWN) {              onUserInteraction();          }          if (getWindow().superDispatchTouchEvent(ev)) {              return true;          }          return onTouchEvent(ev);      }</code></pre>    <p>如果当前事件是DOWN事件,调用了onUserInteraction方法,该方法是一个空方法,我们可以重载该方法,在DOWN事件做一些处理。接着就把事件传递给Window来处理该事件。如果返回true,表示有View处理该事件,onTouch Event()方法返回了true,整个事件处理完成。否则Activity的onTouchEvent方法就会被调用。</p>    <p>Window触摸事件的分发过程</p>    <p>Window类是abstract的,唯一的具体实现类是PhoneWindow类,我们去看看PhoneWindow的superDispatchTouchEvent()方法:</p>    <pre>  <code class="language-java">@Override      public boolean superDispatchTouchEvent(MotionEvent event) {          return mDecor.superDispatchTouchEvent(event);      }        private DecorView mDecor;</code></pre>    <p>DecorView类继承于FrameLayout:</p>    <pre>  <code class="language-java">private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {....}</code></pre>    <p>因此就是调用了DecorView的superDispatchTouchEvent方法:</p>    <pre>  <code class="language-java">public boolean superDispatchTouchEvent(MotionEvent event) {              return super.dispatchTouchEvent(event);          }</code></pre>    <p>可以看到,其实就是调用了父类的dispatchTouchEvent()方法。DecorView继承于FrameLayout,FrameLayout继承于ViewGroup。因此就是调用了ViewGroup的dispatchTouchEvent()方法。</p>    <p>DecorView就是我们的顶层View,当我们通过setContentView()方法设置的是顶层View的一个子View。DecorView组成为:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fb49138b1cfac2c43315d0c3b9610e57.png"></p>    <p style="text-align:center">这里写图片描述</p>    <p>可以看出,事件传递的大概过程:</p>    <p>Activity--》Window--》View。某个View的onTouchEvent()方法被调用。如果返回true,传递会Window,Window再传递会Activity,事件处理结束。否则返回false,再同样的传递会Activity。</p>    <h3><strong>顶层View事件分发的过程</strong></h3>    <p>DecorView继承与FrameLayout,是一个ViewGroup,ViewGroup继承于View,继承图:</p>    <p><img src="https://simg.open-open.com/show/9ceb506c4b0db51dee91fc15ec429054.png"></p>    <p style="text-align:center">这里写图片描述</p>    <p>ViewGroup重载了dispatchTouchEvent()方法。那我们去看看该方法:</p>    <pre>  <code class="language-java">@Override     public boolean dispatchTouchEvent(MotionEvent ev) {            ....            boolean handled = false;          if (onFilterTouchEventForSecurity(ev)) {              final int action = ev.getAction();              final int actionMasked = action & MotionEvent.ACTION_MASK;                1.              // 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();              }                2.              // 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;              }                  ....                3.              if (!canceled && !intercepted) {                    ....                          final int childrenCount = mChildrenCount;                      if (newTouchTarget == null && childrenCount != 0) {                          final float x = ev.getX(actionIndex);                          final float y = ev.getY(actionIndex);                            ....                            final View[] children = mChildren;                          for (int i = childrenCount - 1; i >= 0; i--) {                              final int childIndex = customOrder                                      ? getChildDrawingOrder(childrenCount, i) : i;                              final View child = (preorderedList == null)                                      ? children[childIndex] : preorderedList.get(childIndex);                                ....                                if (!canViewReceivePointerEvents(child)                                      || !isTransformedTouchPointInView(x, y, child, null)) {                                  ev.setTargetAccessibilityFocus(false);                                  continue;                              }                                    ....                                  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;                              }                               ....                            }                      }                        ....                    }              }                ....                return handled;      }</code></pre>    <p>这个方法很长我们分几部分来分析。代码中标有1.2.3.....。</p>    <p>1.ViewGroup对DOWN事件重置状态的操作。</p>    <pre>  <code class="language-java">private void resetTouchState() {          clearTouchTargets();          resetCancelNextUpFlag(this);          mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;          mNestedScrollAxes = SCROLL_AXIS_NONE;      }</code></pre>    <p>标志FLAG_DISALLOW_INTERCEPT可以通过requestDisallowInterceptTouchEvent方法设置。因此在DOWN事件该方法不影响该标志,简单来说,就是不影响ViewGroup处理DOWN事件的操作。</p>    <p>2.判断是否拦截事件。</p>    <p>首先判断是否DOWN事件或者mFirstTouchTarget != null。</p>    <p>mFirstTouchTarget的意思是,如果ViewGroup的有子元素成功处理,mFirstTouchTarget就会指向该元素。</p>    <p>如果当前事件是DOWN:FLAG_DISALLOW_INTERCEPT不影响ViewGroup对DOWN事件的处理,因此调用了onInterceptTouchEvent()方法。是否拦截取决于该方法的返回值。</p>    <p>如果onInterceptTouchEvent()返回true,说明ViewGroup拦截事件,mFirstTouchTarget为null,同一序列的事件都由它处理,onInterceptTouchEvent也不会再调用了,因为actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null条件都不满足。如果子 View调用了requestDisallowInterceptTouchEvent()方法后,ViewGroup将无法拦截除DOWN事件以外的其他事件。该方法不影响ViewGroup的DOWN事件。</p>    <p>3.如果ViewGroup不拦截,ViewGroup遍历所有的子View,判断子View是否满足当前的事件。满足的条件有两个:子View是否播放动画和事件的坐标是否在子View的区域。</p>    <p>如果满足条件,调用了dispatchTransformedTouchEvent()方法。去看看:</p>    <pre>  <code class="language-java">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;          }        ....    }</code></pre>    <p>其实就是调用了子View的dispatchTouchEvent()方法。如果返回了true,就会通过addTouchTarget()方法对mFirstTouchTarget赋值并停止遍历子View。</p>    <pre>  <code class="language-java">private TouchTarget addTouchTarget(View child, int pointerIdBits) {          TouchTarget target = TouchTarget.obtain(child, pointerIdBits);          target.next = mFirstTouchTarget;          mFirstTouchTarget = target;          return target;      }</code></pre>    <p>可以看到,mFirstTouchTarget是一个单链表的数据结构。</p>    <p>如果遍历全部的子View都没有成功处理的,mFirstTouchTarget成员变量为null,当该成员变量为null,就会调用:</p>    <pre>  <code class="language-java">if (mFirstTouchTarget == null) {                  // No touch targets so treat this as an ordinary view.                  handled = dispatchTransformedTouchEvent(ev, canceled, null,                          TouchTarget.ALL_POINTER_IDS);              }</code></pre>    <p>因为第三个参数为null,就会调用super.dispatchTouchEvent()方法,调用到了View的dispatchTouchEvent()方法。</p>    <h3><strong>View的事件分发过程</strong></h3>    <p>dispatchTouchEvent方法如下:</p>    <pre>  <code class="language-java">/**       * Pass the touch screen motion event down to the target view, or this       * view if it is the target.       *       * @param event The motion event to be dispatched.       * @return True if the event was handled by the view, false otherwise.       */      public boolean dispatchTouchEvent(MotionEvent event) {          ....            if (onFilterTouchEventForSecurity(event)) {              //noinspection SimplifiableIfStatement              ListenerInfo li = mListenerInfo;              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;      }</code></pre>    <p>从上面的代码可以看出,判断了是否设置了setOnTouchListener,是否为ENABLED,onTouch是否返回了true。</p>    <p>ENABLED对这个判断没有影响。</p>    <p>但onTouch返回true,onTouchEvent方法就不会执行了。而onClick的方法是在onTouchEvent()方法执行的。因此onTouch事件的优先级比onClick事件高,而且还当onTouch方法返回了true,onClick事件就不会调用了。说明了上面的示例的现象。</p>    <p>我们去看看onClick事件是否在onTouch Event方法中执行的。</p>    <pre>  <code class="language-java">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;          .....        .....              }                return true;          }            return false;      }</code></pre>    <p>从上述代码看到,判断了viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE判断是否可点击或者长点击。只要有一个为true,就会返回true,表示消耗此事件。</p>    <p>CLICKABLE和LONG_CLICKABLE的值可以在清单文件中通过android:clickable和 android:longClickable属性设置,也可以通过setOnclickListener()和setLongClickListener()方法设置。</p>    <p>当设置了点击事件调用了performClick()方法:</p>    <pre>  <code class="language-java">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;      }</code></pre>    <p>可以看到回调了我们设置的onClick方法。由此看出onClick事件是在onTouch Event方法执行的。</p>    <p>这就是事件分发的大概流程。</p>    <p>我们根据上面的示例走一下整个触摸事件的分发流程。</p>    <p>我们从顶View开始分析:</p>    <p>整个View树的结构如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/74695a014029be73a1d59d6e535245d6.png"></p>    <p style="text-align:center">这里写图片描述</p>    <p>上面的示例,我们点击的图片。</p>    <p>首先由顶层View(FrameLayout)的dispatchTouch()方法根据点击图片等坐标首先分发到第一个LinearLayout的,然后调用了ViewGroup的dispatchTouch()方法,又根据点击图片等坐标:u6709:️分发到了第二个LinearLayout,接着有调用了ViewGroup的dispatchTouch()方法,又根据点击图片的坐标分发到了ImageView,然后调用了View的dispatchTouch()方法。ImageView设置setOnTouchListener方法和setOnclickListener方法,如果setOnTouchListener方法返回了false,接着调用了onTouchEvent()方法,从而onClick方法调用,onTouchEvent返回true,消耗了此事件。否则dispatchTouch()方法直接返回了true,消耗此事件。</p>    <p>ViewGroup的onInterceptTouchEvent()方法默认返回false,默认不拦截。</p>    <p>END.</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/1e2d439487f0</p>    <p> </p>