文章目录
- `1. 写在前面`
- `2. 按键焦点过程了解`
- 2.1 dispatchKeyEvent 过程了解
- 2.2 焦点查找请求过程了解
- 2.2.1 第一次获取焦点
- 2.2.2 按键焦点
- `3. 焦点控制`
- `4. 焦点记忆`
- `5. 应用场景`
- `6. 注意`
- `7. 参考资料`
1. 写在前面
欢迎大家入坑.
大家好,我是冰雪情缘,已经在 Android TV开发爬坑多年,也是一名TV开发开源爱好者.
工欲善其事必先利其器,了解按键的流程,焦点的搜索,请求过程等等
对于我们在开发中遇到的问题,可以去思考以及解决一些刺手问题.
下面我将和分享下按键焦点过程以及它的一些实际应用场景.
如果不喜欢看分析原理的小伙伴,可以直接去看3~5.
由于本人经验有限,有问题还请大家多多指教,互相讨论.
参考的 源码 android-24,图片截图为小米电视.
整体流程图参考下:
2. 按键焦点过程了解
假设Activity 的界面布局如下(activity_test.xml):
<FrameLayout ... ...>
<Button1
android:focusableInTouchMode="true"
... .../>
<Button2 ... .../>
</FrameLayout>
带着3个问题,围绕 activity_test.xml 和大家一起学习讨论:
- 按键的事件流程是如何跑的?
- 第一次进入界面,button 是如何获取焦点的?
- 按键的时候,button的下一个焦点是如何查找,并且请求的?
2.1 dispatchKeyEvent 过程了解
如果想深入了解 按键过程的,建议看看 《Android内核剖析》,这里不过多进行大篇幅的讲解.
图1:
界面层次图:
ViewRootlmpl 的 processKeyEvent 函数:
private int processKeyEvent(QueuedInputEvent q) {
.. ...
// 1. dispatchKeyEvent 处理.
// 返回 true,事件消耗,不往下执行焦点搜索与请求,返回 false,继续往下执行.
// mView 是 DecorView,DecorView为整个Window界面的最顶层View,
// 继承FrameLayout,它包含了 ActionBarOv...,还有 content... ...
// 想深入了解 DecorView,具体可以搜索了解下.
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
// 2. 下面焦点搜索以及请求的代码,后面会讲解
... ...
if (direction != 0) {
View focused = mView.findFocus();
if (focused != null) {
View v = focused.focusSearch(direction);
... ...
}
DecorView 的 dispatchKeyEvent 函数:
public boolean dispatchKeyEvent(KeyEvent event) {
... ...
if (!mWindow.isDestroyed()) {
// Activity实现了Window.Callback接口,具体可以参考 Activity.java 源码.
final Window.Callback cb = mWindow.getCallback();
// mFeatureId < 0,表示为 application 的 DecorView.
// cb.dispatchKeyEven 调用的是 Activity 的 dispatchKeyEven.
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);
}
Activity 的 dispatchKeyEvent 函数:
// 补充知识点:
// 这就是为何在 Activity 直接 return true,事件被消耗,就不执行焦点搜索等等操作了.
// 所以这里也是可以做 焦点控制的,最好是在 event.getAction() == KeyEvent.ACTION_DOWN 进行.
// 因为android 的 ViewRootlmpl 的 processKeyEvent 焦点搜索与请求的地方 进行了判断
// if (event.getAction() == KeyEvent.ACTION_DOWN)
// 后续详细讲解焦点控制.
public boolean dispatchKeyEvent(KeyEvent event) {
... ...
Window win = getWindow();
// 调用 PhoneWindow 的 superDispatchKeyEvent
// 里面又调用 mDecor.superDispatchKeyEvent(event)
// mDecor 为 DecorView.
if (win.superDispatchKeyEvent(event)) {
return true;
}
View decor = mDecor;
if (decor == null) decor = win.getDecorView();
// onKeyDown,onKeyUp,onKeyLongPress 等等回调的处理.
// 只有 onKeyDown return true 可以进行焦点控制,
// 因为android 的 ViewRootlmpl 的 processKeyEvent 焦点搜索与请求的地方 进行了判断
// if (event.getAction() == KeyEvent.ACTION_DOWN)
return event.dispatch(this, decor != null
? decor.getKeyDispatcherState() : null, this);
}
DecorView 的 superDispatchKeyEvent 函数:
public boolean superDispatchKeyEvent(KeyEvent event) {
... ...
// DecorView 继承的 FrameLayout
// 调用的是 ViewGroup.dispatchKeyEvent
return super.dispatchKeyEvent(event);
}
ViewGroup 的 dispatchKeyEvent 函数:
Override
public boolean dispatchKeyEvent(KeyEvent event) {
... ...
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
// 调用 view.dispatchKeyEvent
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
// 调用 mFocused 的 dispatchKeyEvent,参考 图2
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
... ...
return false;
}
mFocused 为空的时候,最后 按照 图1 调用的过程返回(view排除).
如果 mFocused 不为空的时候,假设 Button 已经获取焦点
流程参考 图1(ViewRootlmpl->DecorView->PhoneWindow…) + 图2.
图2:
Button 最后也调用了 View 的 dispatchKeyEvent:
public boolean dispatchKeyEvent(KeyEvent event) {
... ...
// onKey 的回调,如果这里也没有消耗事件,继续往下面执行.
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
// 主要是处理一些回调,比如 onKeyDown,onKeyLongPress,onKeyUp等等,具体看代码.
// 没有消耗事件继续往下执行.
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
... ...
return false;
}
图2中,mFocused 不为空的情况下,dispatchKeyEvent 一层层的调用下去(因为每一层的ViewGroup都保存了mFocused的,请求焦点会讲解),
如果这个过程没有事件消耗,直到尽头 Button_View return false,
最后一层层的返回上去(图1 + 图2 如何一层层调用下来的,那就如何原路返回),
直到回到 ViewRootlmpl 的 processKeyEvent 函数 上次执行的地方,
接下去执行,后面继续走 就是 焦点查找与请求相关的代码.
2.2 焦点查找请求过程了解
2.2.1 第一次获取焦点
界面第一次进入的时候,是如何获取到焦点的
先看下DecoreView的流程图:
ViewRootImpl类中有一个方法 performTraversals
... ...
if (mFirst) {
if (mView != null) {
if (!mView.hasFocus()) {
// 调用 View 的 requestFocus(int direction)
mView.requestFocus(View.FOCUS_FORWARD);
}
... ...
}
... ...
整体的过程省略为下面的步骤(差不多的一样的):
ViewRootlmpl.performTraversals==>
DecoreView.requestFocus==>
ActionBarOverlayLayout.requestFocus==>
FrameLayout(android:id/content).requestFocus==>
FrameLayout(activity_test.xml).requestFocus==>
Button1(activity_test.xml).requestFocus
基本上,ActionBarOverlayLayout 和 FrameLayout(andorid:id/content),FrameLayout(activity_test.xml) 基本步骤是一致的.
View.java
public final boolean requestFocus(int direction) {
// 因为 DecoreView 继承 ViewGroup
// ViewGroup 重写了此函数,
// 会调用 ViewGroup 的 requestFocus(int direction, Rect previouslyFocusedRect)
return requestFocus(direction, null);
}
ViewGroup.java
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
// 关注内容:
// 处理 DescendantFocusabilit
// 1)FOCUS_AFTER_DESCENDANTS 先分发给Child View进行处理,如果所有的Child View都没有处理,则自己再处理
// 2)FOCUS_BEFORE_DESCENDANTS ViewGroup先对焦点进行处理,如果没有处理则分发给child View进行处理
// 3)FOCUS_BLOCK_DESCENDANTS ViewGroup本身进行处理,不管是否处理成功,都不会分发给ChildView进行处理
// setDescendantFocusability 可以设置.
int descendantFocusability = getDescendantFocusability();
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
return super.requestFocus(direction, previouslyFocusedRect);
case FOCUS_BEFORE_DESCENDANTS: {
// 其它的 ActionBarOverlayLayout,Content等继承ViewGroup
// 默认进入 FOCUS_BEFORE_DESCENDANTS,因为 ViewGroup 初始化的时候设置了
// setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
// mViewFlags 判断 FOCUSABLE_MASK,FOCUSABLE_IN_TOUCH_MODE.
// Button 以上的父布局,不满足以上条件判断,全部都是 直接 return false.
final boolean took = super.requestFocus(
direction, previouslyFocusedRect);
// took=false, 调用 onRequestFocusInDescendants 遍历子控件进行请求
return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
case FOCUS_AFTER_DESCENDANTS: {
// DecoreView 进入这里,因为 PhoneWindow 给 DecoreView 初始化 设置
// setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
// setIsRootNamespace(true);
// 像 RecyclerView, Leanback 也会进入这里.
// 遍历子控件进行请求
final boolean took = onRequestFocusInDescendants(
direction, previouslyFocusedRect);
// took=true,子控件有焦点,不调用 super.request...,反之.
return took ? took : super.requestFocus(
direction, previouslyFocusedRect);
}
... ...
}
}
View.java
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
return requestFocusNoSearch(direction, previouslyFocusedRect);
}
ViewGroup.java
// 补充知识点: onRequestFocusInDescendants 是可以做焦点记忆控制的.
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
.. ...
for (int i = index; i != end; i += increment) {
View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
//
if (child.requestFocus(direction, previouslyFocusedRect)) {
return true;
}
}
}
return false;
}
我们来看看 Button1 最后的挣扎,如何获取到焦点的
关键代码是 View.java 的函数 handleFocusGainInternal : mPrivateFlags |= PFLAG_FOCUSED 和 mParent.requestChildFocus(this, this)
View.java
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
// Button 默认 android:focusable="true"
// button1 以上的父布局都没有设置此类属性,进入这里,直接就 return false.
if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
(mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
// need to be focusable in touch mode if in touch mode
// 当 button1 没有设置 android:focusableInTouchMode="true" 的时候,
// 直接 return false,那么界面上是没有任何控件获取到焦点的.
// 鼠标|触摸支持的属性.
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}
// need to not have any parents blocking us
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
// 关键函数
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
void handleFocusGainInternal(@FocusRealDirection int direction,
Rect previouslyFocusedRect) {
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
// 关键代码,设置 有焦点的标志位.
// 这个时候 button1 已经标志上焦点
mPrivateFlags |= PFLAG_FOCUSED;
// 获取父布局的老焦点.
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
// 调用此函数,告诉上一层父布局,让它做一些事情.
if (mParent != null) {
mParent.requestChildFocus(this, this);
}
// 此函数是全局焦点监听的回调.
// 调用方式: View.getViewTreeObserver().addOnGlobalFocusChangeListener
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
// 回调处理.
onFocusChanged(true, direction, previouslyFocusedRect);
// 刷新按键的 selector drawable state状态
refreshDrawableState();
}
}
ViewGroup.java
public void requestChildFocus(View child, View focused) {
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
// Unfocus us, if necessary
super.unFocus(focused);
// We had a previous notion of who had focus. Clear it.
if (mFocused != child) {
if (mFocused != null) {
mFocused.unFocus(focused);
}
// 保存上一级的焦点view.
mFocused = child;
}
// 一层层调用回去父布局,相当于
// FrameLayout(activity_test.xml) 的 mFocused 是 Button1.
// FrameLayout(android:id/content) 的 mFocused 是 FrameLayout(activity_test.xml)
// ActionBarOverlayLayout 的 mFocused 是 FrameLayout(android:id/content)
// 最后 DecoreView 的 mFocused 是 ActionBarOverlayLayout
// 在最后的后面,ViewRootImpl 会调用
// requestChildFocus,又会再次调用
// performTraversals刷新界面.(再执行 layout, draw)
// 形成了一个关联, dispatchKeyEvent 的 mFocused 也在使用.
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
// ViewRootImpl.java
@Override
public void requestChildFocus(View child, View focused) {
checkThread();
scheduleTraversals();
}
第一次请求的过程基本告一个段落,下面将分析遥控器按键后,
焦点是如何搜索并且请求的.
2.2.2 按键焦点
大概简单的讲解下按键焦点的搜索过程
当 focusView(2) 按下右键后,它经历了什么鬼?
focusSearch 一层层上去,调用 FocusFinder.getInstance().findNextFocus… … 后,在 …addFocusables 下,将所有 带 焦点属性的 view 全部加到数组里面去,然后通用 方向,位置等 查找相近的view. 最后找到的是 3.
继续我们上次的DEMO讲解
按键按下后,上面讲解的过程 没有消耗 dispatchKeyEvent,
那么就到了 KeyEvent.ACTION_DOWN 按键根据方向查找以及请求焦点view.
#假设 direction = 66,右键
private int processKeyEvent(QueuedInputEvent q) {
... ...
// 以上代码不消耗事件.
// 判断 action 为 ACTION_DOWN 才处理焦点搜索以及请求.
if (event.getAction() == KeyEvent.ACTION_DOWN) {
// 根据按键判断,设置 direction 属性.
if (direction != 0) {
// 一层层查找(根据mFocused),最后获取到 button1.
View focused = mView.findFocus();
if (focused != null) {
// button1_view 调用 focusSearch(), 右键,direction=66
View v = focused.focusSearch(direction);
// 最终返回 v = button2
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);
}
// button2 View 调用 requestFocus
// 这里的过程 和 第一次获取焦点button1请求是一样的.
if (v.requestFocus(direction, mTempRect)) {
// 播放音效
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return FINISH_HANDLED;
}
}
// 进行最后的垂死挣扎,
// 这里其实可以处理一些焦点问题或者滚动翻页问题.
// 滚动翻页的demo可以参考 原生 Launcher 的 Workspace.java
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return FINISH_HANDLED;
}
} else {
// 这里处理第一次无焦点 view 的情况.
// 基本上和有焦点view 的情况差不多.
View v = focusSearch(null, direction);
if (v != null && v.requestFocus(direction)) {
return FINISH_HANDLED;
}
}
}
}
... ...
}
button1下一个焦点搜索流程图:
View v = focused.focusSearch(direction); # focused=>button1 direction=>66
Button1_View->focusSearch(int direction)
FrameLayout(activity_test.xml)_ViewGroup->focusSearch(View focused, int direction)
FrameLayout(android:id/content)_ViewGroup->focusSearch(View focused, int direction)
… …
DecoreView_ViewGroup->FocusFinder.getInstance().findNextFocus(this, focused, direction)
View.java
public View focusSearch(@FocusRealDirection int direction) {
if (mParent != null) {
// button1 的父布局ViewGroup调用 focusSearch
return mParent.focusSearch(this, direction);
} else {
return null;
}
}
ViewGroup.java
// 像 RecyclerView 会重写 focusSearch 进行焦点搜索.
// 也是调用的 FocusFinder.getInstance().findNextFocus
// leanback 的 GridLayoutmanger 也重写了 onAddFocusables.
public View focusSearch(View focused, int direction) {
// 只有 DecoreView 设置了 setIsRootNamespace
// 最终由 DecoreView 进入这里.
if (isRootNamespace()) {
// 传入参数(this: DecoreView focused: button1 direction: 66)
return FocusFinder.getInstance().findNextFocus(this, focused, direction);
} else if (mParent != null) {
return mParent.focusSearch(focused, direction);
}
return null;
}
FocusFinder.java
findNextFocus(ViewGroup root, View focused, int direction)->findNextFocus(root, focused, null, direction)->
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
if (focused != null) {
// 关于XML布局中的 android:nextFocusRight 等等的查找.
next = findNextUserSpecifiedFocus(root, focused, direction);
}
if (next != null) {
return next;
}
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
// 要进行 findNextFocus,关键在于 addFocusables,一层层调用下去.
// DecorView_View.addFocusables
// DecorView_ViewGroup.addFocusables
// ActionBarOverlayLayout_ViewGroup.addFocusables
// FrameLayout(android:id/content)_ViewGroup.addFocusables
// FrameLayout(activity_test.xml)_ViewGroup.addFocusables
// 到最后 button1, button2 添加到 views 数组中,也就是 focusables .
root.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
// 关键函数 findNextFocus,想深入了解是如何查找到下一个焦点的,
// 可以去看看源码,这里不进行过多篇幅的讲解.
// focusables 数组有 button1, button2
// 内部调用 findNextFocusInAbsoluteDirection,这里进行了一些判断,查找某个方向比较近的view.
next = findNextFocus(root, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
ViewGroup.java
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
final int focusableCount = views.size();
final int descendantFocusability = getDescendantFocusability();
... ...
for (int i = 0; i < count; i++) {
final View child = children[i];
// 循环 child view 调用 addFocusables,一层层调用下去,将满足条件的添加进 views 数组.
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
child.addFocusables(views, direction, focusableMode);
}
}
}
if ... ...
// 调用 view 的 addFocusables,父布局是不满足条件的,直接返回了.
super.addFocusables(views, direction, focusableMode);
}
}
View.java
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
if (views == null) {
return;
}
if (!isFocusable()) {
return;
}
if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
&& isInTouchMode() && !isFocusableInTouchMode()) {
return;
}
// button1 以上条件满足,加入views数组.
// button2 以上条件也满足,加入views数组.
// 同理,焦点记忆的原理就很简单了,后续会讲解.
views.add(this);
}
最后 button2 请求焦点 的过程 与 button1最后的挣扎 是一致的.
总结,button1,右键,焦点搜索 focusSearch,
根布局 调用 FocusFinder.getInstance().findNextFocus,
然后父布局调用 addFocusables 将 button1, button2 添加到 views 数组,
最后根据 button1 的按键方向,搜索最近的 button2,最后button2请求焦点.
3. 焦点控制
在xml中,可以使用 nextFocusRight, …Left,…Top,…B 等等来简单控制界面的某些元素.
还有几个焦点控制的函数,也可以配合 FocusFinder.getInstance().findNextFocus 一起使用.
dispatchKeyEvent,可以在 Activity,继承 View的所有控件可以使用,下面搞个小demo.
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
// 因为 return true,前面讲过,事件会被消耗.
// 界面上的 焦点搜索与请求 不继续执行了,执行我们下面的代码.
// 1. 强制请求
findViewById(R.id.xxxx).requestFocus();
// 2. 配合 FocusFinder.getInstance().findNextFocus
ViewGroup contentView = findViewById(android.R.id.content);
View focusView = FocusFinder.getInstance().findNextFocus(contentView, contentView.findFocus(), View.FOCUS_RIGHT);
if (null != focusView) {
focusView.requestFocus();
}
return true;
}
return super.dispatchKeyEvent(event);
}
上面举了个小demo,onKeyDown, okKey 就不在这里过多描述了,可以自己写写demo实验下.
这里重点介绍下 focusSearch 这个函数,前面分析过 ,最终 focusSearch 会返回搜索到的焦点view.
这里举一个 focusSearch 控制的焦点的例子:
@Override
public View focusSearch(View focused, int direction) {
View focusView = super.focusSearch(focused, direction);
// 这里判断是否为标题栏的view,不是就强制返回标题栏的view,
// 最后getTitleView会进行requestFocus...,前面有focusSearch的分析过程.
if (getTitleView() != null && focused != getTitleView() &&
direction == View.FOCUS_UP) {
return getTitleView();
}
return focusView;
}
4. 焦点记忆
设置 需要焦点记忆 并且继承 viewGroup 的控件 2~3 个属性
android:descendantFocusability=“afterDescendants” // FOCUS_AFTER_DESCENDANTS
android:focusable=“true”
android:focusableInTouchMode=“true” // 可选
关键函数 addFocusables,onRequestFocusInDescendants
因为前面我们分析过焦点搜索以及请求过程,了解到:
前一个焦点view focusSearch,然后 findNextFocus 里面调用 addFocusables 添加 相关控件,用于搜索最近的焦点.
如果搜索到的是 LinearLayout ,那么将调用 ViewGroup_requestFocus,因为是 FOCUS_AFTER_DESCENDANTS 属性.
首先调用的是 onRequestFocusInDescendants,上一次的保存的焦点view 再次唤醒,请求一次,搞定,大概的逻辑就是这样.
这里用 LinearLayout 写了一个小小的demo,关于焦点记忆的:
@Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
if (mFocudView != null) {
boolean result = mFocudView.requestFocus(direction, previouslyFocusedRect);
return result;
}
return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
View mFocudView;
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
if (hasFocus()) {
mFocudView = getFocusedChild();
} else {
if (isFocusable()) {
views.add(this);
return;
}
}
super.addFocusables(views, direction, focusableMode);
}
前几天一个朋友问我,但是不想用Leanback的焦点记忆这么办?
我给他提供了两种解决方案:
第一种:使用Leanback的 setFocusScrollStrategy
第二种:如果你想保持中间滚动,又不想用焦点记录,这个就要改源码:
BaseGridView.onRequestFocusInDescendants
@Override
public boolean onRequestFocusInDescendants(
int direction, Rect previouslyFocusedRect) {
return mLayoutManager.gridOnRequestFocusInDescendants(this, direction, previouslyFocusedRect);
}
Leanback的GridLayoutManger.gridOnRequestFocusInDescendants -> gridOnRequestFocusInDescendantsAligned
boolean gridOnRequestFocusInDescendants(
RecyclerView recyclerView, int direction, Rect previouslyFocusedRect) {
switch (mFocusScrollStrategy) {
case BaseGridView.FOCUS_SCROLL_ALIGNED:
default:
// 下面这行代码去掉
return gridOnRequestFocusInDescendantsAligned(recyclerView,
direction, previouslyFocusedRect);
case BaseGridView.FOCUS_SCROLL_PAGE:
case BaseGridView.FOCUS_SCROLL_ITEM:
return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
direction, previouslyFocusedRect);
}
}
private boolean gridOnRequestFocusInDescendantsAligned(
RecyclerView recyclerView, int direction, Rect previouslyFocusedRect) {
View view = findViewByPosition(mFocusPosition);
if (view != null) {
boolean result = view.requestFocus(direction, previouslyFocusedRect);
return result;
}
return false;
}
至于为何Leanback如何做焦点记忆的,建议看看 GridLayoutManger.java.
@Override
public boolean onAddFocusables(
RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode) {
... ...
if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
... ...
if (recyclerView.isFocusable()) {
views.add(recyclerView);
}
}
return true;
}
5. 应用场景
多级菜单,上下,左右等等结构,等等 焦点错乱,焦点需要控制.
场景一:Leanback 或者 RecyclerView,到达右边边缘的时候,按右键,焦点需要跑到下面的一行焦点上.
ExecutorService mExecutorService = Executors.newFixedThreadPool(2);
@Override
public View focusSearch(View focused, int direction) {
View focusView = super.focusSearch(focused, direction);
if ((focusView== null) && (direction == View.FOCUS_RIGHT)) {
// 到达最右边,焦点下移.(注意:建议放到Executors的Runnable里面执行哈,这里简化代码)
new Instrumentation().sendKeyDownUpSync(KEYCODE_DPAD_DOWN);
} else if ((focusView == null) && (direction == View.FOCUS_LEFT)) {
// 到达最左边,焦点下移.
new Instrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_UP);
}
return focusView;
}
场景二:上下结构的标题栏与内容
对面下面图片的这种结构,可能需要做的就是焦点记忆,还有焦点控制.
焦点记忆可以参考我上面的demo或者使用 Leanback的 HorizontalGridView.
焦点控制可以在 FocusSearch 进行处理.
场景三: 左右结构的多级类似的菜单
这种的场景,处理方式一般在 focusSearch 去控制焦点,处理焦点错乱的问题
场景四: 按键速度控制,在Activity或者主布局添加都可以.
private final static long KEY_OUT_TIME = 150L;
long mTimeLast = 0;
long mTimeDelay = 0;
long mTimeSpace = 0;
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
long nowTime = System.currentTimeMillis();
this.mTimeDelay = nowTime - this.mTimeLast;
this.mTimeLast = nowTime;
if (this.mTimeSpace <= KEY_OUT_TIME && this.mTimeDelay <= KEY_OUT_TIME) {
this.mTimeSpace += this.mTimeDelay;
return true;
}
this.mTimeSpace = 0L;
}
return super.dispatchKeyEvent(event);
}
场景五:
VideoView播放器占满窗口并且有焦点(focusableInTouchMode=true,focusable=true),布局中的下方一个选集. 按键焦点下不去这么办?(这里原因是因为addFocusables已经将button添加到数据中,但是findNextFocus找不到下一个焦点)
当下面的显示出来的时候,如下代码:
button的xml设置:
android:focusableInTouchMode="true"
android:focusable="true"
代码中:
button.requestFocusFromTouch();
场景六:
进入界面的第一个焦点如何设置?
- 在xml中设置,想那个有焦点就设置focusableInTouchMode=true
<Button
android:focusableInTouchMode="true"
android:focusable="true"/>
<Button android:focusable="true" />
- 在代码中请求或者延时请求
testBtn.requestFocusFromTouch... ...
6. 注意
Leanback的焦点记忆问题
// Leanback的GridLayoutManger的 onAddFocusables 代码 !!
if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
... ...
} else {
// 这里没有进行判断你的ItemView是否隐藏或者无效
// 比如 你之前 是 pos=2 的位置,现在更新了数据,pos=2的位置已经隐藏或者无效拉
// 那么焦点将会出现在该位置上... ...
// 导致焦点丢失(其实也没有丢失,只不过焦点跑到无效的位置上去拉)!!
View view = findViewByPosition(mFocusPosition);
if (view != null) {
view.addFocusables(views, direction, focusableMode);
}
}
解决方案
在你的ItemView的 addFocusables 进行处理.
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
if ((!isEnabled() || getVisibility() == View.GONE)
&& null != prentView && prentView instanceof RecyclerView) {
// 处理你的逻辑,
// 1.获取当前的位置
// 2. 根据位置向前,向后查找有效的ItemView.
RecyclerView tv = (RecyclerView) prentView;
int position = tv.getChildLayoutPosition(this);
// 向前查找
for (int i = position; i >= 0; i--) {
final View tempView = tv.getChildAt(i);
if (tempView != null && tempView.isEnabled()
&& tempView.getVisibility() == View.VISIBLE) {
nextView = tempView;
break;
}
}
// 向后查找
for (int i = position; i < getChildCount(); i++) {
}
// 如果只剩下一个也是无效的,那就停留在原来的位置.
// 等等的逻辑,具体看自己的场景吧.
} else {
super.addFocusables(views, direction, focusableMode);
}
}
7. 参考资料
<<Android内核剖析>>
//设置视图是否可以获得焦点
public void setFocusable(boolean focusable)
//获取视图是否可以获取焦点
public final boolean isFocusable()
//设置视图是否在触摸模式下可以获得焦点
public void setFocusableInTouchMode(boolean focusableInTouchMode)
//获取视图是否在触摸模式下获得焦点
public final boolean isFocusableInTouchMode()
//当前视图是否是焦点视图,或者子视图里面有焦点视图。
public boolean hasFocus()
//是否当前视图就是焦点视图
public boolean isFocused()
//hasFocus和isFocused区别主要在ViewGroup上,
//前者只是自己或者儿子视图是焦点视图都返回true,
//而后者是一定要自己是焦点视图。
// 清除某个具有焦点视图的焦点属性
public void clearFocus()
public View findFocus()
// 下面的方法是ViewGroup中的方法,获取直接的焦点子视图,也就是返回mFocued数据成员。
public View getFocusedChild()
//这里的direction参数貌似没有什么作用。
public ArrayList<View> getFocusables(int direction)
public void setNextFocusDownId(int nextFocusDownId)
public final boolean requestFocus()
public final boolean requestFocusFromTouch()
public void setDescendantFocusability(int focusability)
FOCUS_BLOCK_DESCENDANTS: 阻止子视图成为焦点视图,这样即使子视图调用了requestFocus也不能成为焦点视图。
FOCUS_BEFORE_DESCENDANTS: 当ViewGroup调用requestFocus时总是优先让自己成为焦点视图。
FOCUS_AFTER_DESCENDANTS: 当ViewGroup调用requestFocus时优先让里面的子视图成为焦点,只有子视图无法成为焦点时才让自己成为焦点视图。这个特性也是默认特性。