一、点击事件 MotionEvent
当点击屏幕的时候就会产生点击事件,包括:按下、滑动、抬起、取消,这些事件被封装成 MotionEvent 对象。一个事件流以按下为开始,中间可能有若干次滑动,以抬起或取消作为结束,即DOWN+不确定个MOVE+UP或CANCEL。系统会将一次完整的点击事件传递给View层级,这个过程就是点击事件分发。
MotionEvent.ACTION_DOWN | 手指接触到屏幕产生。 |
MotionEvent.ACTION_MOVE | 手指在屏幕上移动产生。 |
MotionEvent.ACTION_UP | 手指离开屏幕产生。 |
MotionEvent.ACTION_CANCEL | 滑动超出控件边界、父控件拦截了MOVE或UP、UP不是在同一个控件上触发,子控件就会收到。 |
二、分发过程
Android 的 UI 由Activity、ViewGroup、View及其派生类组成,事件也就在它们中进行传递。响应机制是先分发(由外向内传递)再处理(由内向外回溯),过程由dispatchTouchEvent(事件分发)、onTouchEvent(事件处理)、onInterceptTouchEvent(事件拦截) 协作完成。主要是对 Down 事件作分发,进而找到能够处理Down事件的组件,对于事件流中后续的事件(如Move、Up等),则直接分发给能够处理按下事件的组件。
2.1 外部传递到ViewGroup中
详见:Activity、Window、DecorView、ViewRoot 之间的关系
Activity会通过 getWindow() 获取 PhoneWindow 对象并调用它的 superDispatchTouchEvent(),该方法会调用它PhoneWindow 的内部类 DecorView 的 superDispatchTouchEvent(),而它 DecorView 继承自 FrameLayout,因此 DecorView 是一个顶级的 ViewGroup,这就实现了事件从外部到 ViewGroup 的传递。
2.2 ViewGroup中传递
2.2.1 ACTION_DOWN事件
返回值\方法 | dispatchTouchEvent( ) 事件分发 | onTouchEvent( ) 事件处理 |
true | 一旦返回 true 事件就停止传递了,即该事件在此控件的该方法中被消费了。 | |
false | 返回false会将事件回传给父控件的onTouchEvent( )处理。 | |
停止向下传递并往父控件回溯。 | 不消费事件继续向上传递。 | |
super | ViewGroup:分发中会存在拦截的情况,会调用onInterceptTouchEvent( )来控制,返回super/false会继续向下传递给子控件(默认不拦截,因为子控件也需要该事件)。返回true会拦截事件给自己的onTouchEvent( )处理。 | |
View:没有子控件因此没有事件拦截方法,默认实现就是来调用自己的onTouchEvent( )。 | ||
如果没有重写,默认情况下dispatchTouchEvent( )到onTouchEvent( )连起来整个流程呈一个U型。 |
2.2.2 ACTION_MOVE、ACTION_UP事件
DOWN在哪个控件中消费,MOVE 和 UP 通过 dispatchTouchEvent( ) 就只传递到该控件为止,不会继续往下传递。该控件中 DOWN 如果是在 dispatchTouchEvent( ) 中消费的,那么就到此为止,如果是在 onTouchEvent( ) 中消费的,那么会传递过去。如果都不消费 DOWN 事件则会传递给 Activity 的 onTouchEvent( ) 处理。
2.2.3 点击事件监听
-
优先级:dispatchTouchEvent( ) > onTouchListener > onTouchEvent > onLongClickListener > onClickListener
-
父控件和子控件同时设置了监听,优先响应子控件的。如果父控件先响应必定拦截了事件,不会向下传递子控件便永远无法响应。
三、自定义控件
-
编写自定义 ViewGroup 的时候不要直接拦截所有事件,DOWN 一定要传递到最底层,否则拦得太死传递不下去,子控件没办法申请父控件不要拦截(子控件在 dispatchTouchEvent( ) 写入一行 getParent().resquestDisallowInterceptTouchEvent(true) )。
-
当ViewGroup 要拦截事件时,拦截逻辑写在onInterceptTouchEvent( ),处理逻辑写在onTouchEvent( )。非要在 dispatchTouchEvent( ) 中消费的话,判断如果事件为 DOWN 不拦截。
-
控件都没有消费 DOWN 事件,MOVE、UP则不会传递进来(Activity处理了)。
override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
//子控件请求父控件的onInterceptTouchEvent()不要拦截DOWN事件
parent.requestDisallowInterceptTouchEvent(true)
return super.dispatchTouchEvent(event)
}