#原理解析
- 这里要分析的对象就是MotionEvent,即
点击事件;点击事件的事件分发,本质是对MotionEvent事件的分发过程, 即, 当一个MotionEvent产生了以后, 系统需要把这个事件传递给一个具体的View, 而这个传递的过程就是分发过程。
#分发与拦截
点击事件的分发过程由三个重要方法共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
####public boolean dispatchTouchEvent(MotionEvent ev)
- 用来进行事件的分发传递。
- 如果事件能够传递给当前View,那么此方法一定会被调用,
- 返回值是boolean类型,
返回结果受
当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响; - 表示是否消耗当前事件。
####public boolean onInterceptTouchEvent(MotionEvent event)
- 在
dispatchTouchEvent()内部调用,用来判断是否拦截某个事件; - 如果当前View
拦截了某个事件,那么在同一个事件序列当中, 此方法不会被再次调用, - 返回结果表示
是否拦截当前事件。
- 该方法只在ViewGroup中有,View(不包含 ViewGroup)是没有的。
- 一旦拦截, 则执行ViewGroup的onTouchEvent, 在ViewGroup中处理事件,而不接着分发给View。
- 且只调用一次,所以后面的事件都会交给ViewGroup处理。
####public boolean onTouchEvent(MotionEvent event)
-
同样在
dispatchTouchEvent方法中调用,用来处理点击事件; -
返回结果表示
是否消耗当前事件, -
如果
不消耗,则在同一个事件序列中, 当前View无法再次接收到事件。 -
上述三个方法的区别与关系,可以用如下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
- 通过以上伪代码,可以大致了解点击事件在
View层的传递规则:-
对于一个
根ViewGroup来说, 点击事件产生后,首先会传递给它, 这时其dispatchTouchEvent会被调用; -
如果这个ViewGroup的
onInterceptTouchEvent方法 返回true就表示它要拦截当前事件, 接着事件就会交给这个ViewGroup处理, 即它的onTouchEvent方法就会被调用;!!! -
如果这个ViewGroup的
onInterceptTouchEvent方法 返回false就表示它不拦截当前事件, 这时当前事件就会继续传递给它的子元素, 接着子元素的dispatchTouchEvent方法就会被调用, 如此反复直到事件被最终处理。
-
- 即, 接收到事件 --> 分发 --> 是否拦截 --> 拦截则就地处理【ViewGroup/View:调用自身
onTouch()-->onTouchEvent()-->performClick()-->onClick()】!!!, 否则继续往下传!
这里可以看一下文末的两篇博客!
#事件处理
-
当一个
View需要处理事件时, 如果它设置了OnTouchListener, 则OnTouchListener中的onTouch方法会被回调; -
这时事件如何处理还要看
onTouch的返回值,-
如果返回
false,【事件不消费,继续往下传递】 则当前View的onTouchEvent方法会被调用, 接着是performClick()-->onClick()被调用; 然后 它的父容器的onTouchEvent将会被调用, 依此类推。
【注意这里跟onInterceptTouchEvent不一样,onInterceptTouchEvent仅在ViewGroup级, true表拦截处理,调用ViewGroup自身的onTouch()-->onTouchEvent(),onTouch在View级时候,false表继续流程,调用View自身的onTouchEvent()】 -
如果返回
true,【事件被消费】 那么onTouchEvent方法将不会被调用。
-
-
由此可见, 给View设置的
OnTouchListener,其优先级比onTouchEvent要高。 在onTouchEvent方法中, 如果当前设置的有OnClickListener,那么它的onClick方法会被调用。 而常用的OnClickListener,其优先级最低,即处于事件传递的尾端。
优先级:
onTouch()-->onTouchEvent()-->performClick()-->onClick()
以上是事件处理方法的优先级顺序,按照这个顺序, 只要排在前面的事件方法返回true,消耗处理了点击事件了,点击事件便就地结束,不再下发,排在后面的点击事件也就不会再被调用和响应了; 【文末有实例】
另,onTouch()的实现需要实现onTouchListener;onTouchEvent()/performClick()直接在自定义View文件中重写即可;onClick()的实现需要实现onClick;
-
当一个点击事件产生后, 其传递过程顺序:
Activity -> Window -> 顶级View(上述说的表示View层中的顺序); -
顶级View接收到事件后,就会按照事件分发机制去分发事件。
-
如果一个View的
onTouchEvent返回false, 那么它的父容器的onTouchEvent将会被调用, 依此类推。 【除非下往上回传到某个返回true的onTouchEvent(), 则在那里停止,否则——】 -
如果所有的元素都不处理这个事件, 那么这个事件将会最终传递给
Activity处理, 即Activity的onTouchEvent方法会被调用。
- 形象地举个例子, 假如点击事件是一个难题, 这个难题最终被上级领导分给了一个程序员去处理(类似事件分发过程), 结果这个程序员搞不定(onTouchEvent返回了false), 但难题必须要解决, 那只能交给水平更高的上级解决(上级的onTouchEvent被调用), 如果上级再搞不定,那只能交给上级的上级去解决, 就这样将难题一层层地向上抛。 【即一个从上到下(分发传递),再从下到上的过程(onTouchEvent(), 例见**事件拦截机制大概流程(Android群英传)**中的图例】
#####关于事件传递机制的一些结论(每一个点前面的短语是一个笔者自提的概况中心,便于记忆) 根据它们可以更好地理解整个传递机制: (1)【事件序列,定义】 “
同一个事件序列” 的定义: 指从手指接触屏幕的那一刻起, 到手指离开屏幕的那一刻结束, 在这个过程中所产生的一系列事件, 这个事件序列以down事件开始, 中间含有数量不定的move事件, 最终以up事件结束。(2)【处理事件,独一无二】 正常情况下,
一个事件序列只能被一个View拦截且消耗!!! 这一条的原因可以参考(3), 因为一旦一个元素拦截了某此事件, 那么同一个事件序列内的所有事件都会直接交给它处理!!! 因此同一个事件序列中的事件不能分别由两个View同时处理!!! 除非, 将本该由某个View自己处理的事件 通过onTouchEvent强行传递给其他View处理。
(3)【事件序列,从一而终】 某个View一旦决定拦截,则这一个事件序列都只能由它来处理 (如果事件序列能够传递给它的话), 并且它的onInterceptTouchEvent不会再被调用!!! 当一个View决定拦截一个事件后, 那么系统会把同一个事件序列内的其他方法都直接交给它来处理, 因此 就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
(4)【短期失信】 某个View一旦开始处理事件, 如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false), 那么同一事件序列中的其他事件都不会再交给它来处理, 【即,View放弃处理ACTION_DOWN,便放弃了整个事件序列!!!】 并且事件将重新交由它的父元素去处理, 即父元素的onTouchEvent会被调用。【事件向上“回传”】 即, 事件一旦交给一个View处理,那么它就必须消耗掉!!! 否则同一事件序列中剩下的事件就不再交给它来处理了!!! 好比上级交给程序员一件事,如果这件事没有处理好, 短期内上级就不敢再把事情交给这个程序员做。
(5)【余粮上缴】 如果View不消耗除ACTION_DOWN以外的其他事件, 那么这个点击事件会消失, 此时父元素的onTouchEvent并不会被调用, 并且当前View可以持续收到后续的事件, 最终这些消失的点击事件会传递给Activity处理。
(6)ViewGroup默认不拦截任何事件。 Android源码中 ViewGroup的onInterceptTouch-Event方法默认返回false。
(7)View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
(8)View的onTouchEvent默认都会消耗事件(返回true)!!!!!!! 除非它是不可点击的(clickable和longClickable同时为false)。 View的longClickable属性默认都为false,clickable属性要分情况, 比如Button的clickable属性默认为true, 而TextView的clickable属性默认为false。
(9)【enable无用,clickable居上】 View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的!!!!! 只要它的clickable或者longClickable有一个为true, 那么它的onTouchEvent就返回true!!!
(10)onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。
(11)【由外而内;以下犯上】 事件传递过程是由外向内的, 即事件总是先传递给父元素,然后再由父元素分发给子View, 通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
稍微复习一下: 事件方法的优先级:
onTouch()-->onTouchEvent()-->performClick()-->onClick()
以上是事件处理方法的优先级顺序,按照这个顺序, 只要排在前面的事件方法返回true,消耗处理了点击事件了,点击事件便就地结束,不再下发,排在后面的点击事件也就不会再被调用和响应了;
下面是关于事件优先级的一个实例:
public class DragView3 extends View implements View.OnClickListener {
private int lastX;
private int lastY;
public DragView3(Context context) {
super(context);
ininView();
}
public DragView3(Context context, AttributeSet attrs) {
super(context, attrs);
ininView();
}
public DragView3(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ininView();
}
private void ininView() {
setBackgroundColor(Color.BLUE);
this.setOnClickListener(this);//测试onTouchEvent与onClick的优先级!!
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录触摸点坐标
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
// LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
}
return true;
}
//测试onTouchEvent与onClick的优先级!!
@Override
public void onClick(View v) {
setBackgroundColor(Color.RED);
}
}
- 如上代码,
-
给自定义View配置了
onClick监听器, 如果onClick能响应,点击View之后会从蓝色变成红色, 但是运行之后我们发现并没有变色,即onClick没有被调用; View响应的只是onTouchEvent中的滑动逻辑而已。(下面图一) -
这是因为
onTouchEvent返回true,把事件消耗掉了!! 于是事件在onTouchEvent中处理结束,不再往下传,传不到onClick那里!!! -
如果, 将以上代码中的
onTouchEvent注释掉, 使之默认返回false,不消耗事件,这时onClick则会响应! 那么再次运行程序,可以发现点击View之后, View从蓝色变成红色!!!(下面图二)
-
- 由此,
事件处理方法的优先级不言而喻!
#小结
- 三个关键方法:
dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent;分别的作用和关系;- 分发与拦截,是一个依据
分发顺序的从上往下的过程!!!!! 逻辑骨架就是, 接收到事件 --> 分发 --> 是否拦截 --> 拦截则就地处理【ViewGroup/View:调用自身onTouch()-->onTouchEvent()-->performClick()-->onClick()】!!!, 否则继续往下传,传到最下层的View为止,接着进入处理过程! 分发的顺序是Activity -> Window(PhoneWindow) -> DecorView -> 顶级View(上述说的表示View层中的顺序) -> ViewGroup -> View; 这里可以看一下文末的两篇博客!- 事件的处理则是分发的“回溯”,!!!!! 顺序与分发相反,是一个
从下到上的过程, 从最下层的View开始到最上层(即Activity), 如果所有元素都不消耗这个事件,事件最终就传回Activity; 消耗指onTouch、onTouchEvent、onClick等;
#源码分析
- 上面说了,
Android事件分发流程: Activity -> ViewGroup -> View;
- 所以,想充分理解Android分发机制,本质上是要理解:
Activity对点击事件的分发过程ViewGroup对点击事件的分发过程View对点击事件的分发过程
##Activity对点击事件的分发过程
-
点击事件用MotionEvent来表示, 当一个点击操作发生时,事件最先传递给当前Activity, 由Activity的dispatchTouchEvent来进行事件派发, 具体的工作是由Activity内部的Window来完成的!!!!!!!! -
Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器), 通过Activity.getWindow.getDecorView()可以获得。 -
先从Activity的dispatchTouchEvent开始,源码:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
如上,
首先事件开始交给Activity所附属的Window进行分发,如果返回true,
整个事件循环就结束了:
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
返回false意味着事件没有元素处理,
所有View的onTouchEvent都返回了false,
那么Activity的onTouchEvent就会被调用。
return onTouchEvent(ev);
-
接下来看Window是如何将事件传递给ViewGroup的;
Window是个抽象类!!! 而Window的superDispatchTouchEvent方法也是个抽象方法!!! 因此我们必须找到Window的实现类才行。源码:public abstract boolean superDispatchTouchEvent(MotionEvent event); -
Window的实现类其实是
PhoneWindow, 这一点从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. policy.
PhoneWindow,which you should instantiate when needing a Window.
Eventually that class will be refactored and a factory method added for creating
Window instances without knowing about a particular implementation.
-
大概是说,
Window类可以控制顶级View的外观和行为策略!!!- 它的
唯一实现位于android.policy.PhoneWindow中!!! - 当你要
实例化这个Window类的时候, 你并不知道它的细节,因为这个类会被重构, 只有一个工厂方法可以使用。
-
所以可以看下
android.policy.PhoneWindow, 尽管实例化的时候此类会被重构,仅是重构而已,功能是类似的。 -
由于Window的唯一实现是
PhoneWindow, 接下来看PhoneWindow是如何处理点击事件的,PhoneWindow.superDispatchTouchEvent源码:
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
-
可以清楚看到,
PhoneWindow将事件直接传递给了DecorView!!!!!!!!!! -
DecorView是什么:
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
// This is the top-level view of the window,containing the window decor.
private DecorView mDecor;
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
-
通过
((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)可以获取Activity所设置的View!!!!!!!! 这个mDecor就是getWindow().getDecorView()返回的View!!! 而通过setContentView设置的View是它(DecorView mDecor)的一个子View【所谓顶级View】!!! -
至此,事件传递到了
DecorView这儿, 由于DecorView继承自FrameLayout且是父View, 所以最终事件会传递给View!!! 从而应用能响应点击事件!! -
从这里开始, 事件已经传递到
顶级View了, 即 在Activity中通过setContentView所设置的View, 另外顶级View也叫根View,顶级View一般都是ViewGroup。
##顶级View对点击事件的分发过程
-
点击事件达到顶级View(一般是一个ViewGroup)以后, 会调用ViewGroup的dispatchTouchEvent方法, 然后, 如果顶级ViewGroup拦截事件即onInterceptTouchEvent返回true, 则事件由ViewGroup处理, 如果ViewGroup的mOnTouchListener被设置则onTouch会被调用, 否则onTouchEvent会被调用。 如果都提供的话,onTouch会屏蔽掉onTouchEvent。 -
在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用!!!!!!!!! 如果顶级ViewGroup不拦截事件, 则事件会传递给它所在的点击事件链上的子View, 这时
子View的dispatchTouchEvent会被调用。 到此,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发。
以上是对原理部分的回顾; 下面开始顶级View的源码分析;
- ViewGroup对点击事件的分发过程,
其主要实现在
ViewGroup的dispatchTouch-Event方法中, 这个方法比较长,这里分段说明。
首先下面一段,描述当前View是否拦截点击事情这个逻辑。
// 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;
}
- 如上,
-
ViewGroup在如下两种情况下会判断是否要拦截当前事件:
事件类型为ACTION_DOWN或者mFirstTouchTarget != null。 ACTION_DOWN事件好理解,那么mFirstTouchTarget != null是什么? -
这个从后面的代码逻辑可以看出来, 当事件由ViewGroup的
子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素【于是 != null】, 换种方式来说, 当ViewGroup【不拦截事件并将事件交由子元素处理时mFirstTouchTarget != null】。 反过来, 一旦事件由当前ViewGroup拦截时,mFirstTouchTarget != null就不成立。 -
那么当ACTION_MOVE和ACTION_UP事件到来时,由于(actionMasked == MotionEvent. ACTION_DOWN || mFirstTouchTarget != null)这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给它处理。 当然,这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。为什么说是除了ACTION_DOWN以外的其他事件呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件,这一点从源码中也可以看出来。在下面的代码中,ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT进行重置,因此子View调用request-DisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理。
-
...
参考:
- 《Android开发艺术探索》
- 《Android群英传》
- Android事件分发机制详解(源码)!!!
- 事件拦截机制大概流程(Android群英传)
- 要点提炼|开发艺术之View