事件这里指的是一系列的MotionEvent(android.view.MotionEvent)类对象,实际上是一个动作码和一个坐标轴值的集合,动作码指明了在触摸时发生的变化,坐标轴值含有位置,时间等运动属性信息,常见动作类型即action code如下所示:
ACTION_MASKED | 描述 |
---|---|
ACTION_DOWN | 当手指第一次触摸屏幕的时候产生,是一个事件的开始,包含着初始位置等信息,该指针的指针数据索引始终为MotionEvent中0 |
ACTION_MOVE | 当手机在屏幕上移动的时候,产生一系列的MOVE,包括坐标轴和其他的运动属性 |
ACTION_UP | 最后一根手指离开屏幕时产生,标志着事件的结束(或者是ACTION_CANCEL) |
ACTION_CANCEL | 动作终止,类似于ACTION_UP,但是不执行任何正常状态下要触发的动作 |
ACTION_POINTER_DOWN! | 超出第一个进入屏幕的触摸手指,多点触控的情况,对应的数据由getActionIndex()返回的索引获取 |
ACTION_POINTER_UP | 非最后一根手指离开屏幕,对应多点触控的情况 |
当屏幕接收到点击,就产生了一个事件,紧接着就会触发一系列特定的方法,一套完整的事件分发机制,从上到下依次是Activity→ViewGroup→View,实际上是按照视图的层次结构进行分发的。
Activity对事件的分发
事件首先传递给当前屏幕上对应的Activity,它是用来和用户进行交换的窗口,每个Activity都会有一个用于绘制其用户界面的窗口,窗口通常会充满屏幕。
Activity要执行的是dispatchTouchEvent(MotionEvent ev)
函数,它可以把捕获到的动作传递给根视图。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
首先如果是触摸事件的开始,即对应动作是ACTION_DOWN,进入if判断内部的onUserInteraction()
,该回调函数的意义是表明了用户早与当前Activity以某种方式进行交动,这些输入事件可能来自键盘,触摸或者轨迹球。与该函数对应的onUserLeaveHint()可以一起重载,用以智能地管理状态栏通知的活动等。
然后将事件交给Window进行分发,如果返回值不为true(一般因为超出Window边界之外没有View去接收触摸事件),则执行Activity本身的onTouchEvent(),这是最后的保障手段,默认返回值为真,表明动作已经被消耗处理。
而getWindow()获取的Window类对象是一个抽象类,可以控制顶层类的外观和行为策略,它的唯一实现类是android.view.PhoneWindow 。PhoneWindow实际上把事件传递给了DecorView。
DecorView是FrameLayout的子类,是最顶层的视图,包含标题栏(title)和内容栏(content)。对应于它的唯一一个子视图结构LinearLayout中的两个FrameLayout子元素,其中一个是标题栏,会随着主题的不同而不同,另一个是内容栏,此外内容栏ID:Android.R.id.content
是固定的。而我们经常在调用的setContentView(View view)
,就是指的这个名称为content的视图。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView中该函数的内容为super.dispatchTouchEvent(),而FrameLayout中并没有这个方法,继续向上,最终实际上对应的是ViewGroup中的这个方法。如此,整个触摸事件的动作便从Activity传到顶级View。
ViewGroup对事件的分发
函数dispatchTouchEvent(MotionEvent ev)
,返回值表示事件是否分发处理。
清除状态
首先是通过onFilterTouchEventForSecurity(MotionEvent)
函数过滤TouchEvent,如果被拦截就直接返回FALSE。之后获得动作类型,如果是ACTION_DOWN,表示一个新的手势动作开始了,就取消清除所有之前的TouchTarget记录,该类是一个单链表数据结构;并且重置触摸状态,例如将FLAG_DISALLOW_INTERCEPT标志位重置为0,表示允许父视图中断事件。
判定拦截
然后是判断是否拦截事件,ViewGroup在两种状态下会拦截事件,当动作为ACTION_DOWN或者mFirstTouchTarget非空,具体代码如下:
// 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;
}
当一个事件开始时(ACTION_DOWN),必然要判断是否拦截该事件。另一方面,函数会记录下哪一个子View消耗了该事件,以便之后把后同一个事件序列的所有动作都交给它处理。如果当前事件被该ViewGroup拦截,那么mFirstTouchTarget的值就为null,不管后续到来的动作是什么,判决条件都是FALSE,即始终拦截后续的事件。即没有触摸目标并且这个动作不是初始按下,就直接拦截该事件。
下一步是是获取FLAG_DISALLOW_INTERCEPT
标志位的情况,这个标志位可以通过调用requestDisallowInterceptTouchEvent(boolean disallowIntercept)
方法设定的,参数值为true,表示不允许拦截事件,不执行onInterceptTouchEvent
函数,否则就执行。
在ViewGroup不拦截正常分发MotionEvent时,每一个动作都会经过onInterceptTouchEvent()方法。即onInterceptTouchEvent()函数使父视图有机会在子视图接收到事件以前,看到它所要接收处理的事件。
onInterceptTouchEvent()返回值含义
返回值 | 描述 |
---|---|
true | 拦截MotionEvent事件,这表示它不会被传递给子View,先前正在消耗处理事件的子视图会收到ACTION_CANCEL,并且从该点开始的所有后续事件将发送到父节点的onTouchEvent()方法 |
false | 简单地监视事件,事件依旧沿着视图层次结构传播到通常的目标,使用目标的onTouchEvent()方法处理事件 |
事件向下分发
如果该ViewGroup没有拦截事件的时候,事件继续向下分发,遍历所有子视图,找到一个子视图来接收事件。
首先判断视图是否可以接收点事件,当视图是可见的,视图正在或者计划播放动画效果都是可以接收点击事件的,同时判断事件对应的坐标点是否在子视图的区域内,不满足条件的跳过,进行下一个。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
最终在dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desirePointerIdBits)
函数中,调用了子视图的dispatchTouchEvent
方法,这个过程中,对触摸事件的位置进行了转换操作,实际上就是根据Scroll计算了位置偏移。这样就将事件分发给了子视图。
如果子视图返回值为true,那么就可以确定分发对象,跳出for循环。在此之前调用addTouchTarget
方法,函数中对mFirstTouchTarget(TouchTarget类对象)赋值,它影响着ViewGroup的分发拦截方式,不为null时,当后边的一系列动作到来时,就可以直接传递给相应的视图,否则会拦截同一手势序列中的所有触摸事件。
如果遍历结束以后,都没有找到消耗事件的子视图,那么ViewGroup会自己处理该事件,调用dispatchTransformeTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)
函数,把子视图参数设置为null,即把自身当做一个普通的View,调用父类View的dispatchTouchEvent
方法。
View对事件的处理
View是一个单独的视图,没有子视图需要分发,所有直接由自身处理。
调用onTouch方法
首先是判断View有没有设置触摸监听(View默认情况下是ENABLE的),以及是否设置OnTouchListener接口的回调函数onTouch(View v, MotionEvent event)
,如果设置了,则调用该函数并取得处理之后的返回值。为true就不会调用后续的onTouchEvent方法,显然onTouch
方法优先级高于onTouchEvent
方法;如果返回值是false,则调用onTouchEvent
方法。
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;
}
调用onTouchEvent方法
对onTouchEvent
方法进行分析,首先会涉及到视图的状态,视图状态有很多,常用的视图状态有如下:
名称 | 描述 |
---|---|
enabled | 表示当前视图可用状态。可以通过setEnable 方法进行位运算,改变视图的状态。如果对应的为为0,表示不可用,就无法响应onTouch事件,正常情况下(mViewFlags & ENABLED_MASK) == ENABLED
|
focused | 视图是否获得焦点。判断方式(mViewFlags & FOCUSABLE_MASK) == FOCUSABLE ,类似于打游戏通过手柄的上下左右键切焦点,requestFocus 方法可以改变焦点 |
pressed | 视图是否处于按下状态,按下对象,必须为Clickable。调用setPressed 方法来对这一状态进行改变,判断方式(mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED
|
clickable | 视图是否可以点击,CLICKABLE对应的是setClickable 函数,在设定点击的响应函数setOnClickListene r时,自动会把视图设置为可点击状态 |
当View处于不可用状态,即·(viewFlags & ENABLED_MASK) == DISABLED·时,函数的返回值为:
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
即只要是可以点击的,不论是长按LONG_CLICKABLE还是短按CLICKABLE,还是内容(用于触控笔按钮或鼠标右键单击)是可点击的CONTEXT_CLICKABLE,都会消耗点击事件,尽管处于不可用状态,View只是不作出相应的响应。
接下来判断视图代理TouchDelegate,用于想要视图具有比其实际视图边界更大的触摸面积。触摸区域被更改的视图称为委托视图。mTouchDelegate的使用机制和mTouchListener,mOnClickListener等接口类似。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
动作处理
最后是对具体的动作进行处理,当View是可点击视图的时候,最后就一定会返回true,View的CLICKABLE属性状态要分情况看待,实质就是视图是否可以点击,而View的LONG_CLICKABLE属性状态默认是关闭的。
通过switch-case语句,对不同的动作状态进行不同的响应
ACTION_DOWN
初始化长按状态,即把mHasPerformedLongPress赋值为false,尚未执行长按动作;
判断View是否在一个可以滚动的容器中,比如ListView,进行延时,防止当用户实际上是要滑动容器时,出现按下的状态。
如果是在一个这样的容器中,把mPrivateFlags的PREPRESSED标识位置1;通过postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout())
函数短时间推延这个动作按下的反馈,设置推延的时长为 ViewConfiguration.getTapTimeout
(默认115ms),如果在这个时间段内,该Message没有从消息队列中取出,那么等到时间导到以后就运行CheckForTap
类内的run函数,内容包括:
- 设定视图的PREPRESSED状态位为0;
- 调用
setPressed
方法,把View的PRESSED状态位设置为1; - 执行
checkForLongClick
函数,检测View的LONG_CLICKABLE标志位,为1就把长按检测的函数通过postDelayed(mPendingCheckForLongPress,ViewConfiguration.getLongPressTimeout()-delayOffset)
加入MessageQueue中,设定延时的时间为长按的检测时间(默认500ms)- 之前延时检测按下状态的时间(默认为115ms),即385ms。
同样如果在这个时间段以内没有从消息队列中取出该Message,执行以下内容:
- 执行
performLongClick
函数,即检测视图的OnLongClickListener接口,如果有定义,就调用onLongClick
函数,根据它的返回结果,确定是否把mHasPerformedLongPress状态设为已执行。
当视图不在滚动容器内时,就立即显示按下的反馈,直接调用setPressed
方法,并且发出一个检测长按的延迟事件为0毫秒的任务checkForLongClick(0, x, y)
总之就是检测Tap和LongClick:把mPendingCheckForTap,mPendingCheckForLongPress对应的run添加到Message队列中
ACTION_MOVE
调用drawableHotspotChanged(x, y)
,表明View的热点hotspot发生变化,并将变化传播到视图管理的Drawable对象或子视图时。
然后用pointInView
方法判断确定给定触摸点(在局部坐标中)是否在视图内,其中视图的边界都扩展了一个最小滑动距离TOUCH_SLOP的大小。如果移出了范围:
-
removeTapCallback
函数,把PFLAG_PREPRESSED标志位置0,并把之前CheckForTap对象mPendingCheckForTap要延时执行的Runnable通过removeCallbacks
函数从消息队列中取消(如果还未执行的话); - 查看PRESSED状态位,为1,说明已经过了检测Tap的115ms,第一条中的Runnable已经执行了,要移除在run函数中添加的检测长按的CheckForLongPress类对象
mPendingCheckForLongPress
;并且执行setPressed
,把PRESSED标志位置为0。
即只要用户移出了对应的视图的坐标范围,就将所有关于轻触(tap)和长按(long press)的状态全部取消。
ACTION_UP:
- 动作结束了,对之前的所有标识进行一个总的判断,查看PREPRESSED和PRESSED状态位,不管哪一个是真,都进入下一阶段;
- 为视图请求焦点,并且进入触摸模式。如果View可以获得焦点,并且还没有获得焦点,就请求焦点;
- 把之前为预按下状态(PFLAG_PREPRESSED)的设置为按下,确保用户可以看到按下状态的出现。即如果 prepressed 值为true,调用
setPressed(true, x, y)
,把 PRESSED**标志位设定为1,对应于下边的第6点; - 如果表示长按状态的mHasPerformedLongPress为false,并且忽略下一次ACTION_UP事件的mIgnoreNextUpEvent状态标识为false,就移除长按的检测,因为手势已经到此结束了,不可能再有长按了;
- 判断mPerformClick,如果为null,初始化一个实例,该类实现了一个Runable接口,然后调用post,通过异步处理Handler发送run函数到消息队列尾部,如果添加Message失败则直接执行
performClick
函数,确保执行,不直接调用performClick
函数,可以让View的其他视觉状态在点击动作开始之前更新。 - 如果之前获取的prepressed值为true,64毫秒(
ViewConfiguration.getPresedStateDuration
)后执行UnsetPressedState类对象mUnsetPresedState,否则立即执行mUnsetPresedState;最后无论如何mUnsetPresedState.run()都会执行,其内部调用了·setPresed(false)·,把的PRESSED标志位重置为0,这样实际上是为了保证之前处于预按下状态的View,变为按下状态,有一个足够的延时(默认为64ms),来让用户观察到。 - 最后调用·removeTapCallback·函数,目的是移除PFLAG_PREPRESSED状态位,并且撤销在消息队列中对应的tap延时执行内容 。
总结
这就是Android的事件分发机制的主要流程,关键方法如下
方法 | 调用位置 | 描述 |
---|---|---|
dispatchTouchEvent | A, VG, V | 分发事件到子视图 |
onInterceptTouchEvent | VG | 在传递到子视图前拦截事件 |
onTouchEvent | V | 处理触摸事件 |
A代表Activity,VG代表ViewGroup, V 代表View
上述内容参照了网上一些博客的描述。