2.3 View事件分发机制
从上面ViewGroup
事件分发机制知道,View事件分发机制从dispatchTouchEvent()
开始
源码分析
/** * 源码分析:View.dispatchTouchEvent() */ public boolean dispatchTouchEvent(MotionEvent event) { if ( (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); } // 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true;否则执行onTouchEvent() // 1. (mViewFlags & ENABLED_MASK) == ENABLED // 2. mOnTouchListener != null // 3. mOnTouchListener.onTouch(this, event) // 下面对这3个条件逐个分析 /** * 条件1:(mViewFlags & ENABLED_MASK) == ENABLED * 说明: * 1. 该条件是判断当前点击的控件是否enable * 2. 由于很多View默认enable,故该条件恒定为true(除非手动设置为false) */ /** * 条件2:mOnTouchListener != null * 说明: * 1. mOnTouchListener变量在View.setOnTouchListener()里赋值 * 2. 即只要给控件注册了Touch事件,mOnTouchListener就一定被赋值(即不为空) */ public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; } /** * 条件3:mOnTouchListener.onTouch(this, event) * 说明: * 1. 即回调控件注册Touch事件时的onTouch(); * 2. 需手动复写设置,具体如下(以按钮Button为例) */ button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return false; // 若在onTouch()返回true,就会让上述三个条件全部成立,从而使得View.dispatchTouchEvent()直接返回true,事件分发结束 // 若在onTouch()返回false,就会使得上述三个条件不全部成立,从而使得View.dispatchTouchEvent()中跳出If,执行onTouchEvent(event) // onTouchEvent()源码分析 -> 分析1 } }); /** * 分析1:onTouchEvent() */ public boolean onTouchEvent(MotionEvent event) { ... // 仅展示关键代码 // 若该控件可点击,则进入switch判断中 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { // 根据当前事件类型进行判断处理 switch (event.getAction()) { // a. 事件类型=抬起View(主要分析) case MotionEvent.ACTION_UP: performClick(); // ->>分析2 break; // b. 事件类型=按下View case MotionEvent.ACTION_DOWN: postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; // c. 事件类型=结束事件 case MotionEvent.ACTION_CANCEL: refreshDrawableState(); removeTapCallback(); break; // d. 事件类型=滑动View case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { removeLongPressCallback(); mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } // 若该控件可点击,就一定返回true return true; } // 若该控件不可点击,就一定返回false return false; } /** * 分析2:performClick() */ public boolean performClick() { if (mOnClickListener != null) { // 只要通过setOnClickListener()为控件View注册1个点击事件 // 那么就会给mOnClickListener变量赋值(即不为空) // 则会往下回调onClick() & performClick()返回true playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
源码总结
这里需要特别注意的是,onTouch()
的执行 先于onClick()
核心方法总结
主要包括: dispatchTouchEvent()
、onTouchEvent()
实例分析
在本示例中,将分析2种情况:
- 注册Touch事件监听 且 在
onTouch()
返回false
- 注册Touch事件监听 且 在
onTouch()
返回true
分析1:注册Touch事件监听 且 在onTouch()返回false
代码示例
// 1. 注册Touch事件监听setOnTouchListener 且 在onTouch()返回false button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { System.out.println("执行了onTouch(), 动作是:" + event.getAction()); return false; } }); // 2. 注册点击事件OnClickListener() button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("执行了onClick()"); } });
测试结果
执行了onTouch(), 动作是:0 执行了onTouch(), 动作是:1 执行了onClick()
测试结果说明
- 点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次
onTouch()
; - 因为
onTouch()
返回了false
,所以事件无被消费,会继续往下传递,即调用View.onTouchEvent()
; - 调用
View.onTouchEvent()
时,对于抬起View事件,在调用performClick()
时,因为设置了点击事件,所以会回调onClick()
。
分析2:注册Touch事件监听 且 在onTouch()返回true
代码示例
// 1. 注册Touch事件监听setOnTouchListener 且 在onTouch()返回false button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { System.out.println("执行了onTouch(), 动作是:" + event.getAction()); return true; } }); // 2. 注册点击事件OnClickListener() button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("执行了onClick()"); } });
测试结果
执行了onTouch(), 动作是:0 执行了onTouch(), 动作是:1
测试结果说明
- 点击按钮会产生两个类型的事件-按下
View
与抬起View
,所以会回调两次onTouch()
; - 因为
onTouch()
返回true
,所以事件被消费,不会继续往下传递,View.dispatchTouchEvent()
直接返回true
; - 所以最终不会调用
View.onTouchEvent()
,也不会调用onClick()
。
三. 事件分发机制流程总结
这个三个方法均有一个 boolean(布尔) 类型的返回值,通过返回 true 和 false 来控制事件传递的流程。
PS: 从上表可以看到 Activity
和 View
都是没有事件拦截的,这是因为:
- Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。
- View最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。
View相关
Question: 为什么 View 会有 dispatchTouchEvent ?
A:View
可以注册很多事件监听器,例如:单击事件(onClick
)、长按事件(onLongClick
)、触摸事件(onTouch
),并且View自身也有onTouchEvent
方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent
,所以 View 也会有事件分发。
Question: 与 View 事件相关的各个方法调用顺序是怎样的?
A:如果不去看源码,想一下让自己设计会怎样?
- 单击事件(
onClickListener
) 需要两个事件(ACTION_DOWN
和ACTION_UP
)才能触发,如果先分配给onClick
判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后) - 长按事件(
onLongClickListener
) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP
,应该排在onClick
前面。(onLongClickListener
>onClickListener
) - 触摸事件(
onTouchListener
) , 如果用户注册了触摸事件,说明用户要自己处理触摸事件,这个应该排在最前面。(最前)、 - View自身处理(
onTouchEvent
) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在onTouchListener
后面。(onTouchListener
>onTouchEvent
)
所以事件的调度顺序应该是 onTouchListener
> onTouchEvent
> onLongClickListener
> onClickListener
。
ViewGroup相关
ViewGroup
(通常是各种Layout
) 的事件分发相对来说就要麻烦一些,因为 ViewGroup
不仅要考虑自身,还要考虑各种ChildView
,一旦处理不好就容易引起各种事件冲突,正所谓养儿方知父母难啊。
VIewGroup
的事件分发流程又是如何的呢?
我们了解到事件是通过ViewGroup
一层一层传递的,最终传递给View
,ViewGroup
要比它的 ChildView
先拿到事件,并且有权决定是否告诉要告诉ChildView
。在默认的情况下 ViewGroup
事件分发流程是这样的。
- 判断自身是否需要(询问
onInterceptTouchEvent
是否拦截),如果需要,调用自己的onTouchEvent
。 - 自身不需要或者不确定,则询问
ChildView
,一般来说是调用手指触摸位置的ChildView
。 - 如果子
ChildView
不需要则调用自身的onTouchEvent
。
用伪代码应该是这样子的:
// 点击事件产生后 // 步骤1:调用dispatchTouchEvent() public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; //代表 是否会消费事件 // 步骤2:判断是否拦截事件 if (onInterceptTouchEvent(ev)) { // a. 若拦截,则将该事件交给当前View进行处理 // 即调用onTouchEvent()去处理点击事件 consume = onTouchEvent (ev) ; } else { // b. 若不拦截,则将该事件传递到下层 // 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程 // 直到点击事件被最终处理为止 consume = child.dispatchTouchEvent (ev) ; } // 步骤3:最终返回通知 该事件是否被消费(接收 & 处理) return consume; } }
安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL
,表示当前事件已经结束,后续事件不会再传递过来。
核心要点
- 事件分发原理: 责任链模式,事件层层传递,直到被消费。
- View 的
dispatchTouchEvent
主要用于调度自身的监听器和onTouchEvent
。- View的事件的调度顺序是
onTouchListener
>onTouchEvent
>onLongClickListener
>onClickListener
。- 不论
View
自身是否注册点击事件,只要View
是可点击的就会消费事件。- 事件是否被消费由返回值决定,
true
表示消费,false
表示不消费,与是否使用了事件无关。- ViewGroup 中可能有多个
ChildView
时,将事件分配给包含点击位置的 ChildView。- ViewGroup 和 ChildView 同时注册了事件监听器(
onClick
等),由 ChildView 消费。- 一次触摸流程中产生事件应被同一
View
消费,全部接收或者全部拒绝。- 只要接受
ACTION_DOWN
就意味着接受所有的事件,拒绝ACTION_DOWN
则不会收到后续内容。- 如果当前正在处理的事件被上层 View 拦截,会收到一个
ACTION_CANCEL
,后续事件不会再传递过来。