目录
前言
文章主旨
注意:文章分析基于ANDROID8.0
在Android手机开发中我们通过长按、点击屏幕上的某个区域触发Android系统的事件。而TV端开发一般而言是通过遥控器进行触发。并且不同于手机开发,TV开发中有一个比较重要的概念焦点。通俗电解释就是能让使用者清楚当前操作的页面元素是哪一个。
如下图焦点在 高亮元素上面。图片来自leanback框架
android系统为我们提供了几个常见的焦点相关的属性和方法
android:focusable:设置一个控件能否获得焦点
android:nextFocusDown:(当按下键时)下一个获得焦点的控件
android:nextFocusDown:(当按下键时)下一个获得焦点的控件
android:nextFocusLeft:(当按下键时)下一个获得焦点的控件
android:nextFocusRight:(当按下键时)下一个获得焦点的控
View.requestFocus()元素请求焦点。
既然系统已经提供了焦点的处理相关属性为什么我们还要自己来分析Android的焦点处理?
- 系统虽然提供了一些属性来处理焦点,但是在复杂的Tv页面我们不可能为每一个View指定这些属性。
- 理解系统的焦点处理原理有助于实现我们自己的特殊业务。
KeyEvent事件的传递机制
我们知道在手机开发中有MotionEvent的事件传递(不清楚的可自行搜索,View的事件分发机制)。 同样的在遥控器的按键处理也有自己的事件: KeyEvent。
当系统接收到遥控器的按键经过系统层的封装处理会调用到ViewRootImpl内部类ViewPostImeInputStage的processKeyevent方法。它的相关代码如下。
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
if (mUnhandledKeyManager.preViewDispatch(event)) {
return FINISH_HANDLED;
}
// Deliver the key to the view hierarchy.
//关注点1
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
//省略了系统的部分代码
// Handle automatic focus changes.
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (groupNavigationDirection != 0) {
if (performKeyboardGroupNavigation(groupNavigationDirection)) {
return FINISH_HANDLED;
}
} else {
//关注点2
if (performFocusNavigation(event)) {
return FINISH_HANDLED;
}
}
}
return FORWARD;
}
可以这样来简单理解上面的代码逻辑,首先将KeyEvent事件交给View进行事件分发,如果没有进行处理那么进行后续的焦点查找处理。这个里面的方法我们要关注的有下面两个
- mView.dispatchKeyEvent(event) 将KeyEvent进行View的分发
- performFocusNavigation(event) 进行页面的焦点处理
在这一小节我们主要分析View.dispatchKeyEvent对KeyEvent的分发过程。
mView究竟是什么?
在ViewRootImpl中唯一对mView进行非空赋值就是在ViewRootImpl的setView方法中。而对Window和WindowManager比较熟悉的同学可能会知道。在通过WindowManager添加View的时候会在WindowManagerGlobal的addView中会调用ViewRootImpl的setView方法。这里没有对WindowManager添加View的过程做太多详细的描述,因为这个不是我们的文章重点。
通过阅读源码发现在Activity的makeVisiable中调用了WindowManager的addView方法
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
可以看到addView方法将mDecor对象添加了进去。那么mDecor对象是什么呢?
阅读源码发现mDecor对象的赋值在ActivityThread的handleResumeActivity方法中。因为源码比较复杂我们直接看看关键的赋值代码
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
//省略代码
final Activity a = r.activity;
//省略代码
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
//省略代码
}
可以得出结论Activity的mDecor是Window中的DecorView。那么ViewRootImpl中的mView也是Window中的DecorView。
DecorView的dispatchKeyEvent
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
//省略代码
if (!mWindow.isDestroyed()) {
final Window.Callback cb = mWindow.getCallback();
final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
: super.dispatchKeyEvent(event);
if (handled) {
return true;
}
}
return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
: mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
}
可以看到在省略部分代码的情况下DecorView的dispatchKeyEvent的逻辑分厂简单。如果能够获取到mWindow.getCallback()返回不为空这次KeyEvent时间交给它处理。在cb不处理的情况下我们分别调用Window的onKeyDown和onKeyUp。我们看看Window.getCallback()返回的对象是什么。
public final Callback getCallback() {
return mCallback;
}
在Activity的attach方法中会调用Window的setCallback方法设置mCallback对象为当前的activity。因此KeyEvent事件调用到了activity中。代码如下