• 前言

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

  • Android焦点分发,拦截过程的实现

Android焦点事件的分发是从ViewRootImpl的processKeyEvent开始的,源码如下:

        private int processKeyEvent(QueuedInputEvent q) {final KeyEvent event = (KeyEvent)q.mEvent;// Deliver the key to the view hierarchy.if (mView.dispatchKeyEvent(event)) {return FINISH_HANDLED;}if (shouldDropInputEvent(q)) {return FINISH_NOT_HANDLED;}// If the Control modifier is held, try to interpret the key as a shortcut.if (event.getAction() == KeyEvent.ACTION_DOWN&& event.isCtrlPressed()&& event.getRepeatCount() == 0&& !KeyEvent.isModifierKey(event.getKeyCode())) {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) {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 viewfocused.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 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-focusView v = focusSearch(null, direction);if (v != null && v.requestFocus(direction)) {return FINISH_HANDLED;}}}}return FORWARD;}

源码比较长,下面我就慢慢来讲解一下具体的每一个细节。
* (1) 首先由dispatchKeyEvent进行焦点的分发,如果dispatchKeyEvent方法返回true,那么下面的焦点查找步骤就不会继续了。

dispatchKeyEvent方法返回true代表焦点事件被消费了。

  // Deliver the key to the view hierarchy.if (mView.dispatchKeyEvent(event)) {return FINISH_HANDLED;}
  • 首先会执行mView的dispatchKeyEvent方法,估计大家会好奇这个mView是个什么鬼?其实它就是Activity的顶层容器DecorView,它是一FrameLayout。所以这里的dispatchKeyEvent方法应该执行的是ViewGroup的dispatchKeyEvent()方法,而不是View的dispatchKeyEvent方法。

ViewGroup的dispatchKeyEvent()方法的源码如下:

 @Overridepublic boolean dispatchKeyEvent(KeyEvent event) {if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onKeyEvent(event, 1);}if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {if (super.dispatchKeyEvent(event)) {return true;}} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)== PFLAG_HAS_BOUNDS) {if (mFocused.dispatchKeyEvent(event)) {return true;}}if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);}return false;}

(2)ViewGroup的dispatchKeyEvent执行流程

  • 首先ViewGroup会一层一层往上执行父类的dispatchKeyEvent方法,如果返回true那么父类的dispatchKeyEvent方法就会返回true,也就代表父类消费了该焦点事件,那么焦点事件自然就不会往下进行分发。

  • 然后ViewGroup会判断mFocused这个view是否为空,如果为空就会return false,焦点继续往下传递;如果不为空,那就会return mFocused的dispatchKeyEvent方法返回的结果。这个mFocused是什么呢?其实
    是ViewGroup中当前获取焦点的子View,这个可以从requestChildFocus方法中得到答案。requestChildFocus()的源码如下:

 @Overridepublic void requestChildFocus(View child, View focused) {if (DBG) {System.out.println(this + " requestChildFocus()");}if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {return;}// Unfocus us, if necessarysuper.unFocus(focused);// We had a previous notion of who had focus. Clear it.if (mFocused != child) {if (mFocused != null) {mFocused.unFocus(focused);}mFocused = child;}if (mParent != null) {mParent.requestChildFocus(this, focused);}}

(3)下面再来瞧瞧view的dispatchKeyEvent方法的具体的执行过程

  public boolean dispatchKeyEvent(KeyEvent event) {if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onKeyEvent(event, 0);}// Give any attached key listener a first crack at the event.//noinspection SimplifiableIfStatementListenerInfo li = mListenerInfo;if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {return true;}if (event.dispatch(this, mAttachInfo != null? mAttachInfo.mKeyDispatchState : null, this)) {return true;}if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);}return false;}

惊奇的发现执行了onKeyListener中的onKey方法,如果onKey方法返回true,那么dispatchKeyEvent方法也会返回true

可以得出结论:如果想要修改ViewGroup焦点事件的分发,可以这么干:

  • 重写view的dispatchKeyEvent方法
  • 给某个子view设置onKeyListener监听

注意:实际开发中,理论上所有焦点问题都可以通过给dispatchKeyEvent方法增加监听来来拦截来控制。

  • 焦点没有被dispatchKeyEvent拦截的情况下的处理过程

(1)dispatchKeyEvent方法返回false后,先得到按键的方向direction值,这个值是一个int类型参数。这个direction值是后面来进行焦点查找的。

  // Handle automatic focus changes.if (event.getAction() == KeyEvent.ACTION_DOWN) {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;}

(2)接着会调用DecorView的findFocus()方法一层一层往下查找已经获取焦点的子View。
ViewGroup的findFocus方法如下:

 @Overridepublic View findFocus() {if (DBG) {System.out.println("Find focus in " + this + ": flags="+ isFocused() + ", child=" + mFocused);}if (isFocused()) {return this;}if (mFocused != null) {return mFocused.findFocus();}return null;}

View的findFocus方法

 public View findFocus() {return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;}

说明:判断view是否获取焦点的isFocused()方法, (mPrivateFlags & PFLAG_FOCUSED) != 0 和view 的isFocused()方法是一致的。

  @ViewDebug.ExportedProperty(category = "focus")public boolean isFocused() {return (mPrivateFlags & PFLAG_FOCUSED) != 0;}

其中isFocused()方法的作用是判断view是否已经获取焦点,如果viewGroup已经获取到了焦点,那么返回本身即可,否则通过mFocused的findFocus()方法来找焦点。mFocused其实就是ViewGroup中获取焦点的子view,如果mView不是ViewGourp的话,findFocus其实就是判断本身是否已经获取焦点,如果已经获取焦点了,返回本身。

(3)回到processKeyEvent方法中,如果findFocus方法返回的mFocused不为空,说明找到了当前获取焦点的view(mFocused),接着focusSearch会把direction(遥控器按键按下的方向)作为参数,找到特定方向下一个将要获取焦点的view,最后如果该view不为空,那么就让该view获取焦点。

 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 viewfocused.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 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-focusView v = focusSearch(null, direction);if (v != null && v.requestFocus(direction)) {return FINISH_HANDLED;}}}

(4)focusSearch方法的具体实现。

focusSearch方法的源码如下:

  @Overridepublic 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;}

可以看出focusSearch其实是一层一层地网上调用父View的focusSearch方法,直到当前view是根布局(isRootNamespace()方法),通过注释可以知道focusSearch最终会调用DecorView的focusSearch方法。而DecorView的focusSearch方法找到的焦点view是通过FocusFinder来找到的。

  @Overridepublic 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;}

(5)FocusFinder是什么?

它其实是一个实现 根据给定的按键方向,通过当前的获取焦点的View,查找下一个获取焦点的view这样算法的类。焦点没有被拦截的情况下,Android框架焦点的查找最终都是通过FocusFinder类来实现的。

(6)FocusFinder是如何通过findNextFocus方法寻找焦点的。

下面就来看看FocusFinder类是如何通过findNextFocus来找焦点的。一层一层往下看,后面会执行findNextUserSpecifiedFocus()方法,这个方法会执行focused(即当前获取焦点的View)的findUserSetNextFocus方法,如果该方法返回的View不为空,且isFocusable = true && isInTouchMode() = true的话,FocusFinder找到的焦点就是findNextUserSpecifiedFocus()返回的View。

   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;}

(7)findNextFocus会优先根据XML里设置的下一个将获取焦点的View ID值来寻找将要获取焦点的View。

看看View的findUserSetNextFocus方法内部都干了些什么,OMG不就是通过我们xml布局里设置的nextFocusLeft,nextFocusRight的viewId来找焦点吗,如果按下Left键,那么便会通过nextFocusLeft值里的View Id值去找下一个获取焦点的View。

 View findUserSetNextFocus(View root, @FocusDirection int direction) {switch (direction) {case FOCUS_LEFT:if (mNextFocusLeftId == View.NO_ID) return null;return findViewInsideOutShouldExist(root, mNextFocusLeftId);case FOCUS_RIGHT:if (mNextFocusRightId == View.NO_ID) return null;return findViewInsideOutShouldExist(root, mNextFocusRightId);case FOCUS_UP:if (mNextFocusUpId == View.NO_ID) return null;return findViewInsideOutShouldExist(root, mNextFocusUpId);case FOCUS_DOWN:if (mNextFocusDownId == View.NO_ID) return null;return findViewInsideOutShouldExist(root, mNextFocusDownId);case FOCUS_FORWARD:if (mNextFocusForwardId == View.NO_ID) return null;return findViewInsideOutShouldExist(root, mNextFocusForwardId);case FOCUS_BACKWARD: {if (mID == View.NO_ID) return null;final int id = mID;return root.findViewByPredicateInsideOut(this, new Predicate<View>() {@Overridepublic boolean apply(View t) {return t.mNextFocusForwardId == id;}});}}return null;}

可以得出以下结论:

1. 如果一个View在XML布局中设置了focusable = true && isInTouchMode = true,那么这个View会优先获取焦点。

2. 通过设置nextFocusLeft,nextFocusRight,nextFocusUp,nextFocusDown值可以控制View的下一个焦点。

Android焦点的原理实现就这些。总结一下:

  • 首先DecorView会调用dispatchKey一层一层进行焦点的分发,如果dispatchKeyEvent方法返回true的话,那么焦点就不会往下分发了。

  • 中途可以给某个子View设置OnKeyListener进行焦点的拦截。

  • 如果焦点没有被拦截的话,那么焦点就会交给系统来处理

  • Android底层先会记录按键的方向,后面DecorView会一层一层往下调用findFocus方法找到当前获取焦点的View

  • 后面系统又会根据按键的方向,执行focusSearch方法来寻找下一个将要获取焦点的View

  • focusSearch内部其实是通过FocusFinder来查找焦点的。FocusFinder会优先通过View在XML布局设置的下一个焦点的ID来查找焦点。

  • 最终如果找到将要获取焦点的View,就让其requestFocus。

Android TV 焦点分发原理解析相关推荐

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

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

  2. java eventbus 原理_本文为 Android 开源项目实现原理解析 EventBus 部分,从源码分析 EventBus 的实现原理...

    之前太忙导致 Android 开源项目实现原理解析 一度搁浅,目前一期进行中,我也完成了 EventBus 分析的初稿,大家可以稍微看看后面会继续润色下. PS:本文直接复制 Markdown,格式有 ...

  3. Android TV 的焦点移动原理

    焦点: 焦点(Focus)可以理解为选中态,在Android TV上起很重要的作用.一个视图控件只有在获得焦点的状态下,才能响应按键的Click事件. 上图中,外面有一个绿色光圈的视图,就是当前有焦点 ...

  4. Android 插件换肤原理解析

    转至:http://blog.csdn.net/jiangwei0910410003/article/details/47679843 一.前言 今天又到周末了,感觉时间过的很快呀.又要写blog了. ...

  5. Android 推送实现原理解析

    目录 一.什么是推送? 二.解决数据同步的问题 三.移动无线网络的特点 四.Android 平台上长连接的实现 五.服务器设计 六.消息推送的一般有的方式 七.参考资料 一.什么是推送? 消息推送最简 ...

  6. Android 获取ROOT权限原理解析

    一. 概述 本文介绍了android中获取root权限的方法以及原理,让大家对android玩家中常说的"越狱"有一个更深层次的认识. 二. Root的介绍 1.       Ro ...

  7. android tv 焦点居中自定义listview控件的实现

    因为项目需要,花了一天做了一个自定义listview,和google官方效果图上的控件类似,效果图: 即焦点始终在屏幕中央,焦点选中的item被放大,且颜色被改变,遥控器可以指挥listview上下滚 ...

  8. 【Binder】Android 跨进程通信原理解析

    前言 在Android开发的过程中,用到跨进程通信的地方非常非常多,我们所使用的Activity.Service等组件都需要和AMS进行跨进程通信,而这种跨进程的通信都是由Binder完成的. 甚至一 ...

  9. android xml 焦点,android TV 焦点选中放大效果

    使用github开源项目(非常感谢作者): 这里是项目地址:https://github.com/evilbinary/TvWidget 1.个人只需要RelativeLayout 布局,所以只是用了 ...

最新文章

  1. DC workshop指导篇1- Setup and Synthesis Flow
  2. IntelliJ IDEA导入一个已经存在的子模块
  3. esxi直通 gen8_HP MicroServer Gen8与ESXI采坑之旅
  4. python的赋值与参数传递(python和linux切换)
  5. 关于mmdetection上手的几点说明
  6. 页面用ajax实现简单的连接数据库
  7. Python中的问卷调查(华为机测题)
  8. 学会使用JDK API
  9. Win10电脑如何设置环境变量
  10. iOS开发--保存数据到本地
  11. Installer - 使用Maven自动布署至外部Tomcat
  12. 青县计算机学校,青县将建设新学校(中小学各一所)今年3月开工,明年即可完工!...
  13. html常用长度度量单位,度量长度的单位是什么 毫米以下的五个计量单位分别是什么?...
  14. 经典的双响io电平转换电路仿真
  15. 全新版大学英语综合教程第一册学习笔记(原文及全文翻译)——7 - Kids On The Track(生死时刻)
  16. 《微观经济学》第八章 博弈论与寡头市场初步笔记
  17. vue---父组件实时取到子元素的v-model
  18. vue路由第三篇-导航守卫、路由元信息、动态路由
  19. Sql server找不到启动图标
  20. pymol配体平移与旋转

热门文章

  1. Issue问题模板_v1.0.3
  2. 段码液晶屏应用于大片中的呼叫机——手咪
  3. php学生成绩管理系统,在线录入、统计学生成绩,多种图表展示对比学生成绩
  4. 【RS-485收发切换】使用RS-485要注意的一个问题
  5. 判断一个数是否是完数
  6. 艳情女星精通编程 自己设计个人网站(多图)
  7. Sequel Pro下载及使用方法
  8. java 审核流程思路_java中的工作流流程管理和流转思路
  9. 进军硅谷——程序员面试揭秘
  10. 解决ORCLE-ORA-01122 01110 01210