Android 按键事件分发一直是比较复杂的,终于得见大佬的文章。感觉相比其他文章清晰不少
简单整理下,便于自己再次学习。感谢作者:请叫我大苏
Android KeyEvent 点击事件分发处理流程(一) - 云+社区 - 腾讯云
Android 对 keyevent 的处理:dispatchKeyEvent、onKeyDown/Up、onKeyLisenter 等
分发流程
流程图涉及的主要方法和类:
- (PhoneWindow$)DecorView -> dispatchKeyEvent()
- Activity -> dispatchKeyEvent()
- ViewGroup -> dispatchKeyEvent()
- View -> dispatchKeyEvent()
- KeyEvent -> dispatch()
- View -> onKeyDown/Up()
流程解析
分发流程
DecorView
从(PhoneWindow$)DecorView 的 dispatchKeyEvent() 分发,而 DecorView 会去调用 Activity 的 dispatchKeyEvent(),交给 Activity 继续分发。
Activity
在Activity中,除ActionBar上的Menu键,都是先获取 PhoneWindow 对象,然后调用 PhoneWindow 的 superDispatchKeyEvent()
PhoneWindow
PhoneWindow 转而调用 DecorView 的 superDispatchKeyEvent()
ViewGroup
而 DecorView 则调用了 super.dispatchKeyEvent() 将事件交给父类分发 DecorView 继承自 FrameLayout,但 FrameLayout 没有实现 dispatchKeyEvent(),所以实际上是交给 ViewGroup 的 dispatchKeyEvent() 来分发。
同:ViewGroup 分发的逻辑我还不大理解,不过大体上知道 ViewGroup 递归寻找当前焦点的子 View,将事件传给焦点子 View 的 dispatchKeyEvent() 分发,具体是如何递归寻找的这部分代码待研究。
以上就是一个 KeyEvent 事件的分发流程,跟触屏手机事件传递有些不同的是,如果你没重写以上分发事件的相关类的相关分发方法的话,一个 KeyEvent 事件是肯定会从顶层 DecorView 分发到具体的子 View 的,因为它并没有像 onInterceptTouchEvent 这种在某一层拦截的操作。
处理流程
KeyEvent 事件的处理只有两个地方,一个是 Activity,另一个则是具体的 View。ViewGroup 只负责分发,不会消耗事件。同 TouchEvent 一样,返回 true 表示事件已消耗掉,返回 false 则表示事件还在。
当 KeyEvent 事件分到到具体的子 View 的 dispatchKeyEvent() 里时,View 会先去看下有没有设置 OnKeyListener 监听器,有则回调 OnKeyListener.onKey() 方法来处理事件。
如果 View 没有设置 OnKeyListener 或者 onKey() 返回 false 时,View 会通过调用 KeyEvent 的 dispatch() 方法来回调 View 自己的 onKeyDown/Up() 来处理事件。
(因为View 调用KeyEvent 的dispatch时,传入的Callback是 this)
如果没有重写 View 的 onKeyUp 方法,而且事件是 ok(确认)按键的 Action_Up 事件时,View 会再去检查看是否有设置 OnClickListener 监听器,有则调用 OnClickListener.onClick() 来消费事件,注意是消费,也就是说如果有对 View 设置 OnClickListener 监听器的话,而且事件没有在上面两个步骤中消费掉的话,那么就一定会在 onClick() 中被消耗掉,OnClickListener.onClick() 虽然并没有 boolean 返回值,但是 View 在内部 dispatchKeyEvent() 里分发事件给 onClick 时已经默认返回 true 表示事件被消耗掉了。
如果 View 没有处理事件,也就是没有设置 OnKeyListener 也没有设置 OnClickListener,而且 onKeyDown/Up() 返回的是 false 时,将会通过分发事件的原路返回告知 Activity 当前事件还未被消耗,Activity 接收到 ViewGroup 返回的 false 消息时就会去通过 KeyEvent 的 dispatch() 来调用 Activity 自己的 onKeyDown/Up() 事件,将事件交给 Activity 自己处理。这就是我们常见的在 Activity 里重写 onKeyDown/Up() 来处理点击事件,但注意,这里的处理是最后才会接收到的,所以很有可能事件在到达这里之前就被消耗掉了。
小结
整体的分发处理流程就如上图(手抖了,不然是直线的)所示,有些较重要的点我们可以来总结下:
- 如果对 DecorView 不大了解,那么可以只侧重我们较常接触的点,如 Activity、 ViewGroup、 View,基于此:
- 事件分发:Activity 最先拿到 KeyEvent 事件,但没办法拦截自己处理(这里你们肯定有反对意见,我下面解释),然后将事件分发给 ViewGroup,而 ViewGroup 就只能是递归不断的分发给子 View,事件绝不会在 ViewGroup 中被消耗掉的,最后子 View 接收到事件,分发流程结束,开始事件的处理。
- 事件处理:只有 Activity 和 View 能处理事件,View 根据情况选择是在 OnKeyListener、 OnClickListener 还是在 onKeyDown/Up() 里处理,Activity 只能在 onKeyDown/Up() 里处理。
- 事件处理归纳一下其实就是四个地方,按处理顺序排列如下:View 的 OnKeyListener.onKey()、onKeyDown/Up()、 OnClickListener.onClick()、 Activity 的 onKeyDown/Up()。一旦在四个地方的某处,事件被消耗了,也就是返回 true 了,事件将不会传递到后面的处理方法中去了。
为什么我说 Activity 不能拦截事件交由自己处理呢? 在触屏的 TouchEvent 点击事件机制中,我们可以通过重写 onInterceptTouchEvent() 返回 true 来停止拦截事件的分发并自己处理事件,但在 KeyEvent 中并没有这个方法,所以如果 dispatchKeyEvent() 只干事件分发的事,事件处理都在 onKeyDwon/Up、onKey()、onClick() 中完成,这样的话,Activity 确实没办法拦截事件分发交由自己的 onKeyDown/Up() 来处理。
但谁规定 dispatchKeyEvent() 只能干事件传递的事呢,所以理论上按标准来说,Activity 无法拦截事件分发自己处理,但实际编程中,我经常碰见有人在 Activity 里重写 dispatchKeyEvent() 来处理事件,然后让其返回 true 或 false,停止事件的分发。
分发到这里结束。后面大佬写的很好,还是移动保留下来吧。
使用场景
KeyEvent 事件的分发处理流程大体上知道是怎么走的就行了,有兴趣的可以再去看看源码,然后自己画画流程图,就会更明白了。先把分发处理流程梳理清楚了,我们才知道该怎么用,怎么去重写分发处理的方法,下面就讲些使用场景:
1. 在 Activity 里重写 dispatchKeyEvent()----最常用
举个栗子:
homeActivity_dispatchKeyEvent.png
这在 Tv 开发中是很常见的,经常会在 Activity 里重写 dispatchKeyEvent(),然后要么去预先处理一些工作,要么就是对特定的按键进行拦截。
上面这段代码能看懂么?如果你已经清楚这代码是对左右方向按键的拦截,那么你清楚各种 return 的作用么,为什么又有 return true,又有 return false,还有 return super.dispatchKeyEvent() 的?
先说结论:这里的 return true 和 return false 都能起到按键拦截的作用,也就是子 View 不会接收到事件的分发或处理,Activity 的 onKeyDown/Up() 也不会收到任何消息。
要明白这点,先得搞清楚什么是 return, return 是返回的意思,什么情况下需要返回,不就是调用你的那个方法需要你给个反馈,所以 return 的消息是给上一级的调用者的,所以 return 只会对上一级的调用者的行为有影响。调用 Activity.dispatchKeyEvent() 的是 DecorView 的 dispatchKeyEvent() 里,如下图:
DecorView_dispatchKeyEvent行为.png
那么,既然 Activity 返回 true 或 false 都只对 DecorView 的行为有影响,那么为什么都能起到拦截事件分发的作用呢?
这是因为,事件的分发逻辑其实是在 Activity.java 的 dispatchKeyEvent() 里实现的,如果你重写了 Activity 的 dispatchKeyEvent() 方法,那么根据 Java 的特性程序就会执行你写的 dispatchKeyEvent(),而不会执行基类 Activity.java 的方法,因此你在重写的方法里没有自己实现事件的分发逻辑,事件当然就停止分发了啊。这也是为什么返回 super.dispatchKeyEvent() 时事件会继续分发,因为这最终会调用到基类 Activity.java 的 dispatchKeyEvent() 方法来执行事件分发的逻辑。
既然在 Activity 里返回 true 或 false 都表示拦截,那么有什么区别么?
当然有,因为会影响 DecorView 的行为,比如我们点击遥控器的方向键时界面上的焦点会跟随着移动,这部分逻辑其实是在 DecorView 的上一级调用者中实现的,Activity 返回 true 的话,会导致 DecorView 也返回 true,那么上一级将根据 DecorView 返回 true 的结果停止焦点的移动,这就是我们常见的在 Activity 里重写 dispatchKeyEvent() 返回 true 来实现停止焦点移动的原理。那么,如果 Activity 返回的是 false,DecorView 也跟随着返回 false,那么上一级会继续执行焦点移动的逻辑,表现出来的效果就是,界面上的焦点仍然会移动,但不会触发 Activity 和 View 的事件分发和处理方法,因为已经被 Activity 拦截掉了。
最后,还有一个问题,在 View 或 ViewGroup 里面重写 dispatchKeyEvent() 作用会跟 Activity 一样么?
return true 或 false 或 super 的含义还是一样的,但这里要明白一个层次结构。上层:Activity,中层:ViewGroup,下层:View。
不管在哪一层重写 dispatchKeyEvent(),如果返回 true 或 false,那么它下层包括它本层都不会接收到事件的分发处理,但是它的上层会接收。因为拦截的效果只作用于该层及下层,而上层只会根据你返回的值,行为受到影响。
比如在 ViewGroup 中返回 true,Activity 的 onKeyDown/Up() 就不会被触发,因为被消费了;如果返回 false,那么事件就交由 Activity 处理。但不管返回 true 或 false,子 View 的 dispatchKeyEvent()、各种 onClick() 等事件处理方法都不会被触发到了。
2. 在 Activity 里重写 onKeyDown/Up()----最常用 事件能走到这里表示没有被子 View 消费掉,这里是我们能接触到的层次里面最后对事件进行处理的地方。而且就算我们在这里做了一些工作,也没有必要一定要返回 true。比如如果是方向键事件的话,你在这里返回 true 会影响到上级停止焦点的移动,所以视情况而定。
3. 为某个具体的 View (如 TextView) 设置 OnKeyListener()----一般常用 这个应该也挺常见的,在 Activity 里获取某个控件的对象,然后设置点击事件监听,然后去做一些事。
4. 为某个具体的 View (如 Button) 设置 OnClickListener()----一般常用 这个应该是更常见的了,setOnClickListener,很多场景都需要监听某个控件的点击事件,明确一点就是:该监听器监听的是 ok(确认)键的 Action_Up 事件。
小结一下:
- dispatchKeyEvent(): 比较常见的是在 Activity 或自定义的 ViewGroup 类型控件里面重写该方法,有时是需要在事件开始分发前预处理一些工作,有时则是需要对特定按键进行拦截,注意一下拦截的作用域以及各种 return 值的作用即可。通常情况下,都会含有 return super,因为我们没有必要对所有按键都进行拦截,有些按键仍旧需要继续分发处理,因为 Android 系统默认对很多特殊按键都进行了处理。
- 明确 super 的含义,重写的方法一般都会执行一下默认的逻辑工作,比如 dispatchKeyEvent 执行事件的分发,重写的时候注意是否还需要使用父类的逻辑即可。