事件分发是:当发生了一个事件时,在屏幕上找到一个合适的控件来处理这个事件的过程。因为一个界面上控件如此之多,发生一个事件后总要寻找一个合适来处理事件吧。这个过程就叫做事件分发的机制。
Activity是Android应用程序的门面和载体,它代表一个完整的用户界面。Activity提供了一个窗口来绘制各种视图,即PhoneWindow类。该类继承自顶层窗口类Window,并且包含一个DecorView类对象。DecorView继承自FrameLayout(帧布局),所以本质上是一个ViewGroup,而且是当前活动所放置的全部View的根视图(RootView)。当我们创建一个活动时,在活动的onCreate()方法中调用 setContentView(layoutID) 方法就是为该活动的ContentView部分指定布局内容从而完成GUI的渲染。
当用户点击屏幕产生一个事件,事件通过底层硬件捕获,然后交给ViewRootImpl处理,ViewRootImpl通过Window将事件交给Activity。事件要传递给Activity那么它就必须持有Activity的引用,Window在Activity的attach方法中通过mWindow.setCallback(this)调用持有了Activity的引用,Activity实现了Window.Callback的接口方法。所以最终事件是通过Window.Callback.dispatchTouchEvent把时间交给Acitivity的。
/**
* 从窗口返回到调用方的API。这允许客户端拦截密钥调度、面板和菜单等。
*/
public interface Callback {
public boolean dispatchKeyEvent(KeyEvent event);
public boolean dispatchKeyShortcutEvent(KeyEvent event);
public boolean dispatchTouchEvent(MotionEvent event);
...........
}
事件发生时,ViewRootImpl通过Window将事件交给Activity,然后再一层层的向下层传递,直到找到合适的处理控件。大致如下:
硬件 -> ViewRootImpl -> Window -> Activity -> PhoneWindow -> DecorView -> VIewGroup -> View
但是如果事件传递到最后的View还是没有找到合适的View消费事件,那么事件就会向相反的方向传递,最终传递给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃:
Activity <- PhoneWindow <- DecorView <- ViewGroup <- View
事件主要分为触摸事件和点击事件。
触摸事件对应的是MotionEvent类,主要有以下三种类型
用户触摸屏幕操作由ACTION_DOWN事件开始,结束于ACTION_UP事件,可以有0次或多次ACTION_MOVE事件。
用户手指按下→停留若干时间(可长可短)→用户手指松开,这一完整的过程视为一次点击事件。可以看出,触摸事件先于点击事件执行。
在我们平时的使用或写自定义View时,都会直接或间接的使用View的事件分发,View的事件分发主要与View源码中的3个方法有关:
当事件发生时,ViewGroup会在dispatchTouchEvent方法中先看自己能否处理事件,如果不能再去遍历子View查找合适的处理控件。如果到最后result还是false,表示所有的子View都不能处理,才会调用自身的onTouchEvent来处理。View的事件分发我们需要看一个方法dispatchTouchEvent。我们通过View的事件分发的实现源码来分析分发流程。
/**
* 将触摸屏运动事件向下传递到目标视图,如果它是目标视图,则传递此视图。
* @param 事件要调度的运动事件。
* @return 如果事件由视图处理,则为True;否则为false。
*/
public boolean dispatchTouchEvent(MotionEvent event) {
//省略代码
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//ListenerInfo是View的内部类,这个类定义了各种的监听以及事件,
//包括焦点变化监听,滚动变化监听,点击事件监听等
ListenerInfo li = mListenerInfo;
//如果li.mOnTouchListener.onTouch(this, event)返回true,
//并且view的状态是enable状态下,该方法的result就直接返回true
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果result=false,才会走下面的onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
//省略代码
return result;
}
注释写得很清楚了,主要概括几个要点:
dispatchTouchEvent中没有发现view的onClick方法的调用,其实onClick在onTouchEvent中,在判断事件类型的swithc中,在ACTION_UP抬起的事件中,我们看到这样的代码:
public boolean onTouchEvent(MotionEvent event) {
...
switch (action) {
case MotionEvent.ACTION_UP:
...
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)) {
performClickInternal();
}
}
}
}
在performClickInternal中调用了 performClick()方法,就是在这个方法中执行了view的onClick方法
/**
* Entry point for {@link #performClick()} - other methods on View should call it instead of
* {@code performClick()} directly to make sure the autofill manager is notified when
* necessary (as subclasses could extend {@code performClick()} without calling the parent's
* method).
*/
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
在performClick中,可以看到如果设置了监听的话就会调用view的onClick方法。
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
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);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
所以我们可以得出一个初步的结论。也就是说在view的onTouchEventListener中的onTouch()如果返回了true,表示此次事件被处理,也就是按下的ACTION_DOWN事件被消耗,但是ACTION_UP事件无法传递执行,所以onClick不会执行。
如果onTouchEventListener中的onTouch返回false。则会执行View自身的onTouchEvent方法,表示onTouch不会消耗事件,所以onClick点击事件会响应。
有一点需要特别注意:setClickable(false)需要在setOnClickListener之后调用才起作用,因为在setOnClickListener中将view设置setClickable(true)了
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
* @param l The callback that will run
* @see #setClickable(boolean)
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
前面分析了View的事件分发,但在实际开发过程中真正要使用View事件分发时,基本都是因为ViewGroup的嵌套导致的内外滑动问题,所以对ViewGroup的事件分发更需要深入了解,和View的事件分发一样,ViewGroup事件分发一样与几个重要方法有关:
使用一段伪代码来表述上面三个方法在ViewGroup事件分发中的作用,代码如下:
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
if(onInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else{
consume = child.dispatchTouchEvent(event);
}
return consume;
}
从上面代码中看出,事件传递到ViewGroup时首先传递到dispatchTouchEvent(MotionEvent event)中,然后执行以下逻辑,首先在ViewGroup.dispatchTouchEvent() 中调用onInterceptTouchEvent() 方法:
在onInterceptTouchEvent() 返回false时,表明当前ViewGroup不消耗事件,此事件会向下传递给子View,此子View可能是View也可能是ViewGroup,如果是View则按照上面的事件分发消耗事件。
事件的传递首先是从手指触摸屏幕开始,所以我们先查看ViewGroup的dispatchTouchEvent()中的ACTION_DOWN方法,剔除剩余复杂的逻辑,方法有一段主要的代码:
public boolean dispatchTouchEvent(MotionEvent event){
...
// 标志自身是否拦截此事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 当子View调用requestDisallowInterceptTouchEvent函数时该变量为true。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 返回true表示子View设置了父容器不拦截事件
if (!disallowIntercept) {
// 如果子View没有禁止父View拦截事件,父View通过该函数判断是否需要拦截此事件。
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 一定拦截事条件:事件类型不为ACTION_DOWN并且mFirstTouchTarget为null。
// 这说明ACTION_DOWN事件已经被自身消耗,那么该事件序列中的剩余事件也应该被自身消耗。
intercepted = true;
}
...
}
上述代码虽然简单但ViewGroup的事件分发多半与此处的逻辑有关,里面的每个细节都会影响到最终的事件消耗,总结上面代码执行如下:
当intercepted为false也就是不拦截的时候,就会遍历子元素,并将事件向下分发交给子元素进行处理:
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//给mFirstTouchTarget赋值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
}
}
可以看到当在遍历子孩子的时候会调用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法,,而在dispatchTransformedTouchEvent方法中我们会发现下面代码,通过下面代码会发现,当child不为空的时候他就会直接调用子元素的dispatchTouchEvent方法,这样事件就交由子元素处理了,从而完成一轮分发。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
当子View.dispatchTouchEvent返回为true时,就会调用addTouchTarget(child, idBitsToAssign)方法,该方法就是在给mFirstTouchTarget赋值。
当子View.dispatchTouchEvent返回为false时,就不会调用addTouchTarget(child, idBitsToAssign)方法,故mFirstTouchTarget为null。
那么mFirstTouchTarget为null时会出现什么情况呢,继续向下看,会看到下面的代码,注意这里的view传的是null,也就是说会调用super.dispatchTouchEvent(event)代码,super.dispatchTouchEvent(event)是什么呢?他就是我们自己的dispatchTouchEcent方法。也就是事件将我们自己去处理。
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
根据上面的View和ViewGroup的事件分发学习,这里给出几个View事件传递的结论(以下结论针对系统自动分发),并根据学习内容进行逐条分析
事件拦截最经典的使用示例和场景就是滑动冲突,按照View的冲突场景分,滑动冲突可以分为3类:
一般处理滑动冲突有两种拦截方法:外拦截和内拦截
外拦截顾名思义是在View的外部拦截事件,对View来说外部就是其父类容器,即在父容器中拦截事件,通过上面的代码我们知道,ViewGroup的事件拦截取决与onInterceptTouchEvent()的返回值,所以我们在ViewGroup中重写onInterceptTouchEvent()方法,在父类需要的时候返回true拦截事件,具体需要的场景要按照自己的业务逻辑判断:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
if(isParentNeed()){
//父容器的逻辑
return true;
}
break;
default:
break;
}
super.onInterceptTouchEvent(event)
return false;
}
从上面代码中看出:在onInterceptTouchEvent()的ACTION_DOWN中必须返回false,即不拦截ACTION_DOWN事件,因为如果ACTION_DOWN一但拦截,事件后面的事件都会默认给ViewGroup处理,也不会再调用onInterceptTouchEvent()询问拦截,那子View将没有获取事件的机会;在ACTION_DOWN中,根据自己需要的时候返回true,那此时事件就会被父ViewGroup消耗。
内部拦截法父View拦截除ACTION_DOWN以外的其它事件。子View在ACTION_DOWN中调用getParent().requestDisallowInterceptTouchEvent(true)方法接管事件并在ACTION_MOVE中根据业务逻辑决定事件是否教给父View处理。如需交给父View处理则调用requestDisallowInterceptTouchEvent(false)方法。内部拦截法不符合事件分发流程,是通过子VIew反向控制父View拦截,规则:
/**
* 内部拦截法
* 父View需拦截除DOWN以外的其他事件
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(ev);
return false;
} else {
return true;
}
}
/**
* 内部拦截法
* 子View.dispatchTouchEvent特殊处理
*/
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (isParentNeed()) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
上述代码是内部拦截的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。