原理解析
- 这里要分析的对象就是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