焦点:

焦点(Focus)可以理解为选中态,在Android TV上起很重要的作用。一个视图控件只有在获得焦点的状态下,才能响应按键的Click事件。

上图中,外面有一个绿色光圈的视图,就是当前有焦点的视图。

相对于手机上用手指点击屏幕产生的Click事件, 在使用Android TV的过程中,遥控器是一个主流的操作工具,通过点击遥控器的方向键来控制焦点的移动。当焦点移动到目标控件上之后,按下遥控器的确定键,才会触发一个Click事件,进而去做下一步的处理。焦点的移动如下图所示。

基础的用法:

在处理焦点的时候,有一些基础的用法需要知道。

首先,isFocusable()需要为true,一个控件才有资格可以获取到焦点,可以通过setFocusable(boolean)方法来设置。如果想要在触摸模式下获取焦点(在我们用手机开发的过程中),需要isFocusableInTouchMode()为true,可以通过setFocusableInTouchMode(boolean)来设置。也可以直接在xml布局文件中指定:

<Button...android:focusable="true"android:focusableInTouchMode="true"/>

复制

然后,就是控制焦点的移动了。在谷歌官方文档中提到:

焦点移动的时候(默认的情况下),会按照一种算法去找在指定移动方向上最近的邻居。在一些情况下,焦点的移动可能跟开发者的意图不符,这时开发者可以在布局文件中使用下面这些XML属性来指定下一个焦点对象:

nextFocusDown
nextFocusLeft
nextFocusRight
nextFocusUp 

复制

在Java代码中,让一个指定的View获取焦点,可以调用它的requestFocus()方法。

遇到的问题:

尽管有了官方文档中提到的基础用法,但是在进行Android TV开发的过程中,还是经常会遇到一些焦点方面的问题或者疑问,如

  • “明明指定了焦点id,焦点却跑丢了”
  • “onKeyDown里居然截获不到按键事件”
  • “我没有做任何焦点处理,焦点是怎么自己跑到那个View上的”

接下来,带着这些问题,我们就从源码的角度出发,简单分析一下焦点的移动原理。本文以API 23作为参考。

KeyEvent

在手机上,当手指触摸屏幕时,会产生一个的触摸事件,MotionEvent,进而完成点击,长按,滑动等行为。

而当按下遥控器的按键时,会产生一个按键事件,就是KeyEvent,包含“上”,“下”,“左”,“右”,“返回”,“确定”等指令。焦点的处理就在KeyEvent的分发当中完成。

首先,KeyEvent会流转到ViewRootImpl中开始进行处理,具体方法是内部类ViewPostImeInputStage中的processKeyEvent。(在API 17之前,是deliverKeyEventPostIme这个方法,逻辑大体一致,本文仅以processKeyEvent作为参考)

private int processKeyEvent(QueuedInputEvent q) {final KeyEvent event = (KeyEvent)q.mEvent;...// Deliver the key to the view hierarchy.// 1. 先去执行mView的dispatchKeyEventif (mView.dispatchKeyEvent(event)) {return FINISH_HANDLED;}...// Handle automatic focus changes.if (event.getAction() == KeyEvent.ACTION_DOWN) {int direction = 0;...if (direction != 0) {View focused = mView.findFocus();if (focused != null) {// 2. 之后会通过focusSearch去找下一个焦点视图View v = focused.focusSearch(direction);if (v != null && v != focused) {...if (v.requestFocus(direction, mTempRect)) {...return FINISH_HANDLED;}}// Give the focused view a last chance to handle the dpad key.if (mView.dispatchUnhandledMove(focused, direction)) {return FINISH_HANDLED;}} else {// find the best view to give focus to in this non-touch-mode with no-focus// 3. 如果当前本来就没有焦点视图,也会通过focusSearch找一个视图View v = focusSearch(null, direction);if (v != null && v.requestFocus(direction)) {return FINISH_HANDLED;}}}}return FORWARD;}

复制

从几处关键的代码,可以看到这里的逻辑是:

  1. 先去执行mView的dispatchKeyEvent
  2. 之后会通过focusSearch去找下一个焦点视图
  3. 如果当前本来就没有焦点View,也会通过focusSearch找一个视图

ViewRootImpl就是ViewRoot,继承了ViewParent,但本身并不是一个View,可以看作是View树的管理者。而这里的成员变量mView就是DecorView,它指向的对象跟Window和Activity的mDecor指向的对象是同一个对象。所有的View组成了一个View树,每一个View都是树中的一个节点,如下图所示:

正在上传…重新上传取消

最上层的根是DecorView,中间是各ViewGroup,最下层是View。

本文的分析都是基于View树的。

在processKeyEvent中,首先走了mView的dispatchKeyEvent,也就是从DecorView开始进行KeyEvent的分发。

1. dispatchKeyEvent

首先走DecorView的dispatchKeyEvent,之后会依次从Activity->ViewGroup->View的方向分发KeyEvent。

有兴趣的话可以通过trace看一下KeyEvent的流转方向:

对于KeyEvent的分发,之后会另开一篇细讲,包括KeyEvent的处理优先级,长按的识别等,这里只简单看一下ViewGroup和View的dispatchKeyEvent。

首先看ViewGroup的dispatchKeyEvent。

@Override
public boolean dispatchKeyEvent(KeyEvent event) {...if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {// 1.1 以View的身份处理KeyEventif (super.dispatchKeyEvent(event)) {return true;}} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)== PFLAG_HAS_BOUNDS) {// 1.2 以ViewGroup的身份把KeyEvent交给mFocused处理if (mFocused.dispatchKeyEvent(event)) {return true;}}...return false;
}

复制

通过flag的判断,有两个处理路径,也可以看到在处理keyEvent时,ViewGroup扮演两个角色:

  1. View的角色,也就是此时keyEvent需要在自己与其他View之间流转
  2. ViewGroup的角色,此时keyEvent需要在自己的子View之间流转

当作View的时候,会调用自己View的dispatchKeyEvent。

当作ViewGroup的时候,会调用当前焦点View的dispatchKeyEvent。

其实,从概念上来看,都是调用当前有焦点View的dispatchKeyEvent,只不过有时是自己本身,有时是他的子View。

再看看View的dispatchKeyEvent

public boolean dispatchKeyEvent(KeyEvent event) {...ListenerInfo li = mListenerInfo;// 1.3 如果设置了mOnKeyListener,则优先走onKey方法if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {return true;}// 1.4 把View自己当作参数传入,调用KeyEvent的dispatch方法if (event.dispatch(this, mAttachInfo != null? mAttachInfo.mKeyDispatchState : null, this)) {return true;}...return false;
}

复制

View这里,会优先处理OnKeyListener的onKey回调。

然后才可能会走KeyEvent的dispatch,最终走到View的OnKeyDown或者OnKeyUp。

将大体的流转顺序总结如下图

其中任何一步都可以通过return true的方式来消费掉这个KeyEvent,结束这个分发过程。

2. focusSearch

如果dispatchKeyEvent没有消费掉这个KeyEvent,会由系统来处理焦点的移动。

通过View的focusSearch方法找到下一个获取焦点的View,然后调用requestFocus

那focusSearch是如何找到下一个焦点视图的呢?

// View.java
public View focusSearch(@FocusRealDirection int direction) {if (mParent != null) {return mParent.focusSearch(this, direction);} else {return null;}
}

复制

View并不会直接去找,而是交给它的parent去找。

// ViewGroup.java
public View focusSearch(View focused, int direction) {if (isRootNamespace()) {// root namespace means we should consider ourselves the top of the// tree for focus searching; otherwise we could be focus searching// into other tabs.  see LocalActivityManager and TabHost for more inforeturn FocusFinder.getInstance().findNextFocus(this, focused, direction);} else if (mParent != null) {return mParent.focusSearch(focused, direction);}return null;
}

复制

判断是否为顶层布局,若是则执行对应方法,若不是则继续向上寻找,说明会从内到外的一层层进行判断,直到最外层的布局为止。

有意思的是,Android提供了设置isRootNamespace的方法,但又hide了起来不让使用,看来这个逻辑还有待优化。

/*** {@hide}** @param isRoot true if the view belongs to the root namespace, false*        otherwise*/
public void setIsRootNamespace(boolean isRoot) {if (isRoot) {mPrivateFlags |= PFLAG_IS_ROOT_NAMESPACE;} else {mPrivateFlags &= ~PFLAG_IS_ROOT_NAMESPACE;}
}

复制

最后的算法交给了FocusFinder

FocusFinder.getInstance().findNextFocus(this, focused, direction);

复制

isRootNamespace()的ViewGroup把自己和当前焦点(View)以及方向传入。

// FocusFinder.javapublic final View findNextFocus(ViewGroup root, View focused, int direction) {return findNextFocus(root, focused, null, direction);
}private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {View next = null;if (focused != null) {// 2.1 优先从xml或者代码中指定focusid的View中找next = findNextUserSpecifiedFocus(root, focused, direction);}if (next != null) {return next;}ArrayList<View> focusables = mTempList;try {focusables.clear();root.addFocusables(focusables, direction);if (!focusables.isEmpty()) {// 2.2 其次,根据算法去找,原理就是找在方向上最近的Viewnext = findNextFocus(root, focused, focusedRect, direction, focusables);}} finally {focusables.clear();}return next;
}

复制

这里root是上面isRootNamespace()为true的ViewGroup,focused是当前焦点视图

优先找开发者指定的下一个focus的视图 ,就是在xml或者代码中指定NextFocusDirection Id的视图

其次,根据算法去找,原理就是找在方向上最近的视图

2.1 findNextUserSpecifiedFocus

// FocusFinder.java
private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {// check for user specified next focusView userSetNextFocus = focused.findUserSetNextFocus(root, direction);if (userSetNextFocus != null && userSetNextFocus.isFocusable()&& (!userSetNextFocus.isInTouchMode()|| userSetNextFocus.isFocusableInTouchMode())) {return userSetNextFocus;}return null;
}

复制

首先执行View的findUserSetNextFocus方法

// View.java
View findUserSetNextFocus(View root, @FocusDirection int direction) {switch (direction) {case FOCUS_LEFT:if (mNextFocusLeftId == View.NO_ID) return null;return findViewInsideOutShouldExist(root, mNextFocusLeftId);...}}return null;
}

复制

比如,按了“左”方向键,如果设置了mNextFocusLeftId,则会通过findViewInsideOutShouldExist去找这个View。

mNextFocusLeftId一般是在xml里面设置的,比如

<Buttonandroid:id="@+id/btn_1"android:nextFocusLeft="@+id/btn_2"... />

复制

也可以在java代码里设置

mBtn1.setNextFocusLeftId(R.id.btn_2);

复制

来看看findViewInsideOutShouldExist做了什么。

private View findViewInsideOutShouldExist(View root, int id) {if (mMatchIdPredicate == null) {// 可以理解为一个判定器,如果id匹配则判定成功mMatchIdPredicate = new MatchIdPredicate();}mMatchIdPredicate.mId = id;View result = root.findViewByPredicateInsideOut(this, mMatchIdPredicate);...return result;
}public final View findViewByPredicateInsideOut(View start, Predicate<View> predicate) {View childToSkip = null;for (;;) {// 从当前起始节点开始寻找(ViewGroup是遍历自己的child),寻找id匹配的View,跳过childToSkip,具体可去看View和ViewGroup中该方法的具体实现View view = start.findViewByPredicateTraversal(predicate, childToSkip);if (view != null || start == this) {return view;}ViewParent parent = start.getParent();if (parent == null || !(parent instanceof View)) {return null;}// 如果如果当前节点没有,则往上一级,从自己的parent中查找,并跳过自己childToSkip = start;start = (View) parent;}
}protected View findViewByPredicateTraversal(Predicate<View> predicate, View childToSkip) {if (predicate.apply(this)) {return this;}return null;
}

复制

ViewGroup的findViewByPredicateTraversal

// ViewGroup
@Override
protected View findViewByPredicateTraversal(Predicate<View> predicate, View childToSkip) {if (predicate.apply(this)) {return this;}final View[] where = mChildren;final int len = mChildrenCount;for (int i = 0; i < len; i++) {View v = where[i];if (v != childToSkip && (v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {v = v.findViewByPredicate(predicate);if (v != null) {return v;}}}return null;
}

复制

可以看到,findViewInsideOutShouldExist这个方法从当前指定视图去寻找指定id的视图。首先从自己开始向下遍历,如果没找到则从自己的parent开始向下遍历,直到找到id匹配的视图为止。

这里要注意的是,也许存在多个相同id的视图(比如ListView,RecyclerView,ViewPager等场景),但是这个方法只会返回在View树中节点范围最近的一个视图,这就是为什么有时候看似指定了focusId,但实际上焦点却丢失的原因,因为焦点跑到了另一个“意想不到”的相同id的视图上。

Android TV 的焦点移动原理相关推荐

  1. android 焦点分发,Android TV 焦点分发原理解析

    前言 相信很多刚接触AndroidTV开发的开发者,都会被各种焦点问题给折磨的不行.不管是学技术还是学习其他知识,都要学习和理解其中原理,碰到问题我们才能得心应手.下面就来探一探Android的焦点分 ...

  2. Android TV 焦点分发原理解析

    前言 相信很多刚接触AndroidTV开发的开发者,都会被各种焦点问题给折磨的不行.不管是学技术还是学习其他知识,都要学习和理解其中原理,碰到问题我们才能得心应手.下面就来探一探Android的焦点分 ...

  3. android 按键分析,Android TV开发按键与焦点深入分析(四)

    8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? 前面三篇都是从源码的角度分析按键事件.焦点变换的原理,作为应用层的开发者, 分析源码都是带着实际的开发困惑的,要不然谁没 ...

  4. Android TV UI开发常用知识

    导入依赖 Google官方为Android TV的UI开发提供了一系列的规范组件,在leanback的依赖库中,这里介绍一些常用的组件,使用前需要导入leanback库. implementation ...

  5. Android TV开发总结(三)构建一个TV app的焦点控制及遇到的坑

    原文:Android TV开发总结(三)构建一个TV app的焦点控制及遇到的坑 版权声明:我已委托"维权骑士"(rightknights.com)为我的文章进行维权行动.转载务必 ...

  6. 智能会议系统(3)---Android VoIP系统实现原理

    Android VoIP系统实现原理 VoIP(Voice over Internet Protocol)即首先数字化语音信号并压缩成帧,再转换为IP数据包在网络上传输,以此完成语音通话的业务,是一种 ...

  7. android tv webview,Android TV开发---WebView焦点处理

    背景 开发的Android TV应用时,有一个做题模块用到了WebView做为题目和选项的展示容器 问题 对于正常的文字内容来说,在相应元素中使用tabindex属性即可实现焦点的简单控制,但是此处展 ...

  8. 电视不正常Android镜像投屏,Mirror for Android TV(安卓电视投屏软件) V2.4 Mac版

    Mirror for Android TV是一款适用于Mac的安卓电视投屏工具,它可以帮助用户在Mac上即可将视频.电影投屏到安卓电视上,适用于任何电视,设置框或媒体播放器与Android电视操作系统 ...

  9. android tv 菜单键,Android TV开发总结(三)构建一个TV app的焦点控制及遇到的坑

    前言:关于<TV Metro界面(仿泰捷视频TV版)源码解析>由于都是相关代码,就不发公众号了,有兴趣的可以看链接:http://blog.csdn.net/hejjunlin/artic ...

最新文章

  1. Android 第三方图表类 MPChart 的使用
  2. configparser模块
  3. ubuntu查看内存或cpu使用情况
  4. JavaWeb——内置对象session与httpSession对象是同一个东西么?
  5. CTAssetsPickerController 选中图片不显示对号的问题解决
  6. java序列化和反序列化_Java恶意序列化背后的历史和动机
  7. 学C语言好,还是学C++好呢?这两个专业在哪些领域用得最多?
  8. 实战:通过组策略为用户部署软件
  9. MFC1、动态创建CButton
  10. 蔚来es6_国产Model Y订单挤爆官网,蔚来ES 6惨遭大量退订?
  11. 2020印象笔记日记模板及更改印象笔记背景色教程
  12. SHELLEXECUTEINFO,ShellExecuteEx
  13. 大数据舆情监测平台_大数据舆情监测与分析平台有哪些?舆情大数据监测软件排名2020...
  14. 开源让这位 00 后逆袭成为各类大奖收割者
  15. (转)TTime, TDateTime
  16. Flask框架学习:蓝图的使用
  17. HTML5 视频直播那些事儿+吕小鸣博客
  18. C:\Users\123\AppData\Roaming\Python\Python38\Scripts which is not on PATH
  19. linux 2.6.28.7 各驱动代码位置(待验证,已验证为蓝色标识)
  20. 前端实现浏览器自动弹开三屏、一键关闭效果

热门文章

  1. 传奇之路——国际化的中国人
  2. 向“生物力学之父”冯元桢先生学习什么?【转载】
  3. ipad原始邮箱服务器端口,如何在iPhone/iPad/iPod touch邮件应用程序中创建帐户(默认POP3)?...
  4. 怎样将word中的图片另存为jpg格式的图片
  5. 视频消重技术.批量处理去重消重去水印去logo软件批量处理去重消重去水印去log...
  6. 如何让PPT演讲更精彩
  7. 怎么把一张暗的照片调亮_怎么把色彩太暗的照片变清晰?
  8. 【一】Java快速入门
  9. MINI2440裸机实验之SDRAM
  10. Mysql中where和having用法及区别