Android 焦点
最近在做 Android TV,和手机 APP 不同的是,TV 大都需要依赖遥控器来改变焦点而不是 Touch 事件(除非你的屏幕可以触控~~)
Leanback
当时做 TV 项目搜到的资料都是它,但是没几个人细讲,我也不打算细讲···
因为这个库需要你自己去源码编译,有很多东西写死了,或者不是 public 的,而且不一定符合你们设计的审美~~
我们最后就是用源码编译搞的,只用了一个 VerticalGridView, 还改了一些东西。
如何自定义我的焦点呢?
项目中有很多神奇的 UI 上的要求,要我去实现一些自定义的效果。比如,进入播放页,要按确认键或者下键,呼出一些菜单?比如,跳转 tab 栏,焦点永远先跑到第二个 tab 上(因为第一个 tab 是历史记录)等等。这就不得不研究一下,焦点是如何分发寻找的。
事件起源
如果非要讲···不知道有没有人说的清···反正我是说不清。要从硬件说起,从驱动程序说起,从操作系统说起,中断程序说起~~
但是对于我等渣渣,还是从 ViewRootImpl 看起来吧~~
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.
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
// This dispatch is for windows that don't have a Window.Callback. Otherwise,
// the Window.Callback usually will have already called this (see
// DecorView.superDispatchKeyEvent) leaving this call a no-op.
if (mUnhandledKeyManager.dispatch(mView, event)) {
return FINISH_HANDLED;
}
int groupNavigationDirection = 0;
if (event.getAction() == KeyEvent.ACTION_DOWN
&& event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {
groupNavigationDirection = View.FOCUS_FORWARD;
} else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {
groupNavigationDirection = View.FOCUS_BACKWARD;
}
}
// If a modifier is held, try to interpret the key as a shortcut.
if (event.getAction() == KeyEvent.ACTION_DOWN
&& !KeyEvent.metaStateHasNoModifiers(event.getMetaState())
&& event.getRepeatCount() == 0
&& !KeyEvent.isModifierKey(event.getKeyCode())
&& groupNavigationDirection == 0) {
if (mView.dispatchKeyShortcutEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
}
// Apply the fallback event policy.
if (mFallbackEventHandler.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
// Handle automatic focus changes.
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (groupNavigationDirection != 0) {
if (performKeyboardGroupNavigation(groupNavigationDirection)) {
return FINISH_HANDLED;
}
} else {
if (performFocusNavigation(event)) {
return FINISH_HANDLED;
}
}
}
return FORWARD;
}
代码相对于事件分发还不算太长~~ 可以看到前面都是对于 dispatchKeyEvent 相关的拦截,如果拦截了,那么就不会走后续的流程了,所以重写 dispatchKeyEvent 可以帮忙处理一些按键需求。
之后,如果不返回 true,还会有一些特殊按键处理,之后就到了 performFocusNavigation,这个就是一个默认的自动寻找焦点的逻辑了。
private boolean performFocusNavigation(KeyEvent event) {
int direction = 0;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (event.hasNoModifiers()) {
direction = View.FOCUS_UP;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.hasNoModifiers()) {
direction = View.FOCUS_DOWN;
}
break;
case KeyEvent.KEYCODE_TAB:
if (event.hasNoModifiers()) {
direction = View.FOCUS_FORWARD;
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
direction = View.FOCUS_BACKWARD;
}
break;
}
if (direction != 0) {
View focused = mView.findFocus();
if (focused != null) {
View v = focused.focusSearch(direction);
if (v != null && v != focused) {
// do the math the get the interesting rect
// of previous focused into the coord system of
// newly focused view
focused.getFocusedRect(mTempRect);
if (mView instanceof ViewGroup) {
((ViewGroup) mView).offsetDescendantRectToMyCoords(
focused, mTempRect);
((ViewGroup) mView).offsetRectIntoDescendantCoords(
v, mTempRect);
}
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return true;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return true;
}
} else {
if (mView.restoreDefaultFocus()) {
return true;
}
}
}
return false;
}
代码其实很好懂,判断方向,寻找焦点,请求焦点。核心方法,focusSearch 和 requestFocus,这两个都是 View 的方法,给了我们重写来实现自定义效果的可能。这里 requestFocus 如果是 ViewGroup 还会进入 onRequestFocusInDescendants 这个方法,会见到熟悉的 getDescendantFocusability。
经常会在网上看到文章说给 ViewGroup 添加 descendantFocusability 这个属性等等,其实就是在这类进行判断的。这个方法也可以重写,但是意义不大。
理一理思路
所以说,现在我们知道了,焦点事件或者说遥控器的按键事件的分发我们可以在 dispatchKeyEvent
拦截,如果拦截了事件被截断。如果不拦截,那么一般会走到寻找焦点的逻辑,这时候 focusSearch
和 requestFocus
和 onRequestFocusInDescendants
就出现了。可以重写 focusSearch 来返回当前情况下你想让哪个 view 获取到焦点,如果不重写,focusSearch 会一直到 isRootNamespace 返回 true,走FocusFinder.getInstance().findNextFocus(this, focused, direction)。FocusFinder这里就会有一套自己根据方向寻找焦点 view 的默认逻辑,这也是为什么,你不重写任何方法,有时候焦点也能达到你想要的效果。
那么来看看之前说的两个需求
-
进入播放页,要按确认键或者下键,呼出一些菜单
这个就很简单了,dispatchKeyEvent 中判断如果是确认键和下键,就做一个动画,return true,消耗掉这个事件就 OK -
跳转 tab 栏,焦点永远先跑到第二个 tab 上
这个也很简单,核心就是焦点本来要跑到这个 tab 的 ViewGroup 时,第一个焦点永远在 第二个子 View 上。onRequestFocusInDescendants,这个方法就发生在 ViewGroup 找焦点的时候,你可以设置 ViewGroup 为 FOCUS_AFTER_DESCENDANTS,并重写这个方法,当这个方法被调用时,寻找第二个 View,调用其 requestFocus 方法并返回 true 即可。
所以,想不让焦点分发下来,或者焦点变化中做一些动画,只需要 dispatchKeyEvent,想定制一些焦点逻辑则需要 focusSearch。
简单来说,这就是整个焦点分发的核心逻辑!当然,细节还有很多很多,但是对于一个应用工程师来说,做需求这些够用了,深入研究源码的每一个细节,有时候意义并不大。