android View 事件分发

524

我们在学习View的时候,不可避免会遇到事件的分发,而往往遇到的很多滑动冲突的问题都是由于处理事件分发时不恰当所造成的。因此,深入了解View事件分发机制的原理,对于我们来说是很有必要的。下面我们就通过源码来详细讲解一下View事件的分发机制

在通过源码来分析view事件分发机制之前,我们先来看一个简单的示例demo。

在demo中我们自定义了三个view分别是:

public class CustomView extends View {
    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("TAG","EventInterception CustomView onTouchEvent");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d("TAG","EventInterception CustomView dispatchTouchEvent");
        return super.dispatchTouchEvent(event);
    }
}
public class CustomViewGroupA extends LinearLayout {
    public CustomViewGroupA(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("TAG","EventInterception CustomViewGroupA onTouchEvent");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("TAG","EventInterception CustomViewGroupA dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d("TAG","EventInterception CustomViewGroupA onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }
}
public class CustomViewGroupB extends LinearLayout {
    public CustomViewGroupB(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("TAG","EventInterception CustomViewGroupB onTouchEvent");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("TAG","EventInterception CustomViewGroupB dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d("TAG","EventInterception CustomViewGroupB onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }
}

并分别重写了 onTouchEvent、dispatchTouchEvent以及onInterceptTouchEvent
布局文件中我们是这样布局的:


        
            
        
    

程序运行时当我们点击 CustomView 时运行结果是:

 D/TAG: EventInterception CustomViewGroupA dispatchTouchEvent
 D/TAG: EventInterception CustomViewGroupA onInterceptTouchEvent
 D/TAG: EventInterception CustomViewGroupB dispatchTouchEvent
 D/TAG: EventInterception CustomViewGroupB onInterceptTouchEvent
 D/TAG: EventInterception CustomView dispatchTouchEvent
 D/TAG: EventInterception CustomView onTouchEvent
 D/TAG: EventInterception CustomViewGroupB onTouchEvent
 D/TAG: EventInterception CustomViewGroupA onTouchEvent

下面我们就通过对上述demo的具体分析来对view的事件分发进行更近一步的了解。

在上面的示例demo中我们主要是重写了dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 这三个方法,那么这三个方法具体是做什么?下面我们来详细介绍一下这三个方法。

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

该方法用来进行事件分发,如果事件能够传递给当前view,那么此方法一定会被调用,返回结果受当前view的onTouchEvent 和下级view的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件那么在同一个事件序列当中,此方法不会在被调用,返回结果表示是否拦截当前事件。

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

在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前view无法再次接受到事件。
上述这三个方法到底有什么区别?它们之间的关联是什么?关于这一点,我们将会从源码层面对其进行详细的解释。

我们知道当一个点击操作发生时,是由当前Activity的dispathOnTouchEvent来进行事件分发的。其内部代码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

在上述代码中我们可以看到,内部会首先通过getWindow()方法 获取当前activity所附属的Window窗口,然后调用 superDispatchTouchEvent()方法,通过该方法可以看到,如果返回了true,则表示某一个view对事件进行了处理,则直接跳出了该函数,如果返回为false,则将该事件的处理抛给了activity的 onTouchEvent方法。而对于view事件是如何传递的 我们只需要关注 superDispatchTouchEvent 该方法是如何将事件一步一步传递下去的。

我们知道getWindow()该方法返回的是一个Window类,在该类中我们可以看到

public abstract boolean superDispatchTouchEvent(MotionEvent event);

superDispatchTouchEvent 方法是一个抽象方法,而该方法的具体实现,需要看PhoneWindow内部是如何具体实现的。

private DecorView mDecor;
public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

在PhoneWindow类中看到内部直接调用了mDecor.superDispatchTouchEvent(event),

到这里可以看到 点击事件的传递 Activity —> Window—>DecorView.

接下来我们关注 DecorView中superDispatchTouchEvent 是如何将事件进行分发的。

在DecorView中 :

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
    ....
    public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
        ...
}

直接调用了 FrameLayout 中的dispatchTouchEvent方法,而该方法的具体实现是在ViewGroup中,接下来分析ViewGroup中dispatchTouchEvent是如何实现的。

这个方法比较长,我们只需要看其中我们比较关注的部分即可。

 if (actionMasked == MotionEvent.ACTION_DOWN) {
       cancelAndClearTouchTargets(ev);
       resetTouchState();
     }

首先这里先判断事件是否为DOWN事件,如果是,则初始化,把mFirstTouchTarget置为null。由于一个完整的事件序列是以DOWN开始,以UP结束,所以如果是DOWN事件,那么说明是一个新的事件序列,所以需要初始化之前的状态。

mFirstTouchTarget 置为null 是在 resetTouchState()方法内执行,具体实现是在

private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }

以上方法内将mFirstTouchTarget = null;

接下来我们关注以下代码:

   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); 
            } else {
               intercepted = false;
            }
      } 
  else {
         intercepted = true;
  }

我们知道一个点击事件的开始是从MotionEvent.ACTION_DOWN 开始,所以在上述代码中我们需要着重关注

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

是如何处理的。

在这里对于disallowIntercept有几种情况:

1:如果是ACTION_UP或者ACTION_CANCEL, 将disallowIntercept设置为默认的false
2:假如子view调用了requestDisallowInterceptTouchEvent()方法来设置disallowIntercept为true
3:当我们抬起手指或者取消Touch事件的时候要将disallowIntercept重置为false

所以说上面的disallowIntercept默认在我们每次ACTION_DOWN的时候都是false

注:当子view调用了requestDisallowInterceptTouchEvent()方法来设置disallowIntercept为true 时,viewGroup将无法拦截除了ACTION_DOWN 以外的其他点击事件(比如 ACTION_MOVE、ACTION_UP)等,这是为什么呢? 我们在之前已经提到过,因为VIewGroup 在分发事件开始时,会将一些标记重置,所以子view的 requestDisallowInterceptTouchEvent 方法的设置,对于ACTION_DOWN事件来说没有影响。

通过上述代码我们可以看出 当点击事件是ACTION_DOWN ViewGroup总会调用onInterceptTouchEvent 方法来询问自己是否拦截事件。

我们通过源码可以看出 ViewGroup中onInterceptTouchEvent

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

从上述代码中我们可以看到在 onInterceptTouchEvent 方法中 直接返回false;

当onInterceptTouchEvent 返回为 false时,ViewGroup的dispatchTouchEvent方法中将会遍历子view,代码如下:


 for (int i = childrenCount - 1; i >= 0; i--) {
     final int childIndex = getAndVerifyPreorderedIndex(
       childrenCount, i, customOrder);
       final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    mLastTouchDownTime = ev.getDownTime();
      if (preorderedList != null) {

         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;
}

我们可以看出会遍历其内部的子view 然后调用dispatchTransformedTouchEvent 该方法,在该方法中我们可以看到,内部实质是调用了 子view的dispatchTouchEvent方法。
在遍历子view时当child为null,ViewGroup直接 把该事件的处理交给了 view

 if (child == null) {
      handled = super.dispatchTouchEvent(event);
   } else {
      handled = child.dispatchTouchEvent(event);
 }

下面我们来看view中 dispatchTouchEvent 方法中是如何执行的:

       ...
if (onFilterTouchEventForSecurity(event)) {
   if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
       result = true;
   }
   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;
            }
        }

我们可以看出view对事件的处理比较简单,因为view(不包含ViewGroup)是一个单独的元素,它没有子元素 因此事件无法向下传递,所以只能自己处理。从上面的源码中可以看出view对点击事件的处理,首先判断是否设置了onTouchListener,如果onTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见onTouchListener的优先级高于onTouchEvent。

下面我们来看一下 onTouchEvent方法中对事件的具体处理。

 if (((viewFlags & CLICKABLE) == CLICKABLE ||
    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) 
    ||(viewFlags & CONTEXT_CLICKABLE) == 
        CONTEXT_CLICKABLE) {

        ....
    if (!post(mPerformClick)) {
         performClick();
        }
        ....
}

我们看到只要 view到 clickable 或者long_clickable又一个为true那么 它就会消耗这个事件,及onTouchEvent方法返回true,然后当Action_up 事件发生时,出发performClick方法。
在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;
    }

如果view设置了onClickListener 那么 performClick方法内部会调用它的onClick方法
我们看到当子view 的onTouchListener、onTouchEvent或者onClick处理了点击事件之后,直接返回了true,下面我们 重新回到 ViewGroup的dispatchTouchEvent方法中,
在该方法中 我们可以看到

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

     mLastTouchDownTime = ev.getDownTime();
      if (preorderedList != null) {

       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;
    }

其内部通过addTouchTarget 对mFirstTouchTarget 进行赋值。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

然后直接break 跳出该for循环。

当遍历子view时,当子view的child不为null,通过下面的代码

if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }

我们可以看出同样时把viewGroup的事件直接交给了view,之后的分析与上述一致。
当子ViewGroup设置了onTouchListener、onTouchEvent或者onClick点击事件时,父ViewGroup内将不会继续遍历,事件将被子ViewGroup处理,然后直接break,跳出该for循环。

至此我们得出以下结论:当view无论时view还是viewgroup当内部设置了onTouchListener、onTouchEvent或者onClick事件之后,如果返回为true那么该事件将由view或者viewgroup进行处理。如果所有的view以及viewgroup全部返回为false,那么该事件将由Activity的onTouch进行处理,这个在文章的开头我们以及提到过了。

下面我们将通过一张图来说明该事件传递的流程

这里写图片描述