一点见解: 焦点那点事(一)

Android开发使用的手机一般处于触摸模式, 因此默认情况下并不会有焦点, 所以之前一直对焦点不是很熟悉. 但是在电视端开发上, 焦点的处理可以说直接影响了用户体验, 因此借此熟悉下焦点处理的流程.

本文着重介绍焦点相关的一些关键方法, 先从局部了解下焦点的一些基础规则和行为特点.

获取焦点的前提

  1. View#isFocusable返回true, 如果在触摸模式, 则View#isFocusableInTouchMode也要返回true
  2. 控件必须可见
  3. 控件相关的父控件, 包括祖父控件等, ViewGroup#getDescendantFocusability()不能为ViewGroup#FOCUS_BLOCK_DESCENDANTS

View

获取焦点

调用View#requestFocus系列方法

进入View#requestFocusNoSearch

在该方法中会对控件的当前状态进行判断, 如果不符合获取焦点的前提则直接返回false告知调用方, 控件不会获取焦点

只要符合前提就会继续执行, 最终必定返回true, 不论当前控件的焦点状态是否有改变

符合前提则进入 View#handleFocusGainInternal

如果控件已经持有焦点, 则不会做任何事情, 直接结束流程

如果没有焦点,

  1. 改变焦点标志位, 此时View#isFocused就会返回true
  2. 通过ViewParent#requestChildFocus通知父控件即将获取焦点
  3. 通知其他部件焦点状态发生变化(略, 本文不关心)
  4. 触发OnGlobalFocusChangeListener的回调
  5. 触发OnFocusChangeListener回调
  6. 重绘, 结束流程

清除焦点

调用View#clearFocus主动放弃焦点

如果控件本身没有焦点, 则什么都不会发生

如果控件持有焦点

  1. 改变焦点标志位
  2. 通过ViewParent#clearChildFocus通知父控件, 当前控件放弃焦点
  3. 触发OnFocusChangeListener回调
  4. 调用当前控件的根控件(rootView)的requestFocus方法
  5. 如果步骤4中没有找到新的焦点控件, 则触发OnGlobalFocusChangeListener的回调, 注: 如果找到新的焦点控件, 那么新的控件获取焦点的过程中就会回调OnGlobalFocusChangeListener, 所以这里只有没找到才进行步骤5

注: 由上流程可以知道, 如果根控件查找控件的时候找到的控件还是这个控件, 那么OnFocusChangeListener就会被调用两次, 先失去焦点, 然后又获取到焦点

ViewGroup

焦点分发策略DescendantFocusability

  1. FOCUS_BLOCK_DESCENDANTS: 拦截焦点, 直接自己尝试获取焦点
  2. FOCUS_BEFORE_DESCENDANTS: 首先自己尝试获取焦点, 如果自己不能获取焦点, 则尝试让子控件获取焦点
  3. FOCUS_AFTER_DESCENDANTS: 首先尝试把焦点给子控件, 如果所有子控件都不要, 则自己尝试获取焦点

获取焦点

根据焦点分发策略决定下面两个方法的调用顺序

通过View#requestFocus自己获取焦点

ViewGroup看作View, 直接走View获取焦点的流程来获取焦点

进入onRequestFocusInDescendants

可以传入方向来改变遍历的顺序, 默认是从0递增

遍历子控件, 调用子控件的View#requestFocus来尝试把焦点给可见的子控件, 某个子控件成功获取到焦点后, 停止遍历

注: 重写该方法可以改变ViewGroup分发焦点给子控件的行为, 例如遍历顺序

清除焦点

如果焦点控件不是它的子控件, 那么直接把当前的ViewGroup看作ViewView#clearFocus流程, 反之则调用焦点控件的View#clearFocus.

注: 区别在于重新分发焦点时的选择范围.

ViewParent

ViewParent是一个接口, 表示了一个父控件应该具备的功能, ViewGroup实现了该接口.

与焦点相关的接口有4个

clearChildFocus

当子控件主动放弃焦点的时候会通过这个方法通知父控件.

ViewGroup的默认实现中, 会置空当前焦点控件, 表示该父控件下没有子控件获取焦点, 接着把这个事件通知给上级父控件.

注1: 这个方法名有点让人误解, 应该把这个方法看作一个回调, 表明了一个状态, 在这个方法中并没有做清除焦点的操作, 实际的清除动作是在View#clearFocus中完成的, 这个方法也是在这个流程中被调用的. 而且是在子控件已经放弃焦点后调用.
注2: 区分主动放弃和因为其他控件获取了焦点而被动丢失焦点的情况

requestChildFocus

当子控件获取了焦点后, 通过这个方法通知父控件. 同clearChildFocus类似, 应该把这个方法看作是一个回调.

ViewGroup的默认实现中, 因为同时只会有一个焦点, 因此在这里应该把旧焦点清除掉, 大致流程如下

  1. 如果焦点分发策略为FOCUS_BLOCK_DESCENDANTS则什么也不干
  2. 如果父控件自身有焦点, 通过View#unFocus清除焦点
  3. 如果父控件当前已经有焦点控件, 并且和新的控件不一致, 那么通过View#unFocus清除旧焦点控件的焦点
  4. 向上传递这个事件

内部清除焦点View#unFocus

这个方法和View#clearFocus相同点在于都会执行View#clearFocusInternal方法, 区别在于unFocus只会执行clearFocus中, 上文清除焦点中提到的1, 3步骤, 因此不会通知父控件, 不会触犯requestChildFocus回调, 因为这个方法是在子控件被动失去焦点时调用的, 所以也不会触发焦点分发.

因此新旧焦点切换的大致流程是

  1. 新焦点控件获取焦点
  2. 新焦点控件通知父控件
  3. 父控件清除旧焦点控件的焦点
  4. 旧焦点控件回调OnFocusChangeListener
  5. 触发OnGlobalFocusChangeListener的回调
  6. 新焦点控件回调OnFocusChangeListener

focusableViewAvailable

通知父控件, 子控件的状态发生改变, 从不能获取焦点, 变成可能可以获取焦点.

有两种情况会被调用

  1. 子控件从unFocusable变为focusable
  2. 子控件从不可见变为可见, 即使它不是focusable也会调用, 因此它的子控件可能可以获取焦点.

ViewGroup中的默认实现只是在符合条件的情况下把这个事件向上传递给自己的父控件.

focusSearch(View, int)

查找指定方向中最近的, 想要获取焦点的控件.

这个方法直接决定了焦点的移动规则, 非常重要.

ViewGroup的默认实现中, 会一直向上传递, 直到根控件, 接着调用FocusFinder#findNextFocus方法查找合适的控件. 稍后再分析这个方法.

View中有一个同名的方法focusSearch(int), 该方法直接调用了父控件的focusSearch(View, int)来查找下一个焦点控件

findNextFocus

查找步骤大致如下

手动指定

如果有通过android:nextFocusDown等手动指定控件, 则返回对应方向的控件

动态计算
  1. 获取所有可以获取焦点的控件的集合
  2. 计算相对当前焦点控件的坐标
  3. 根据方向选择合适的控件

总结

  1. 分析的过程要注意区分ViewViewGroup的差异和新焦点和旧焦点控件的方法调用.
  2. ViewParent是一个接口, 其中一些方法应该看作是回调, 子控件通过这些回调通知父控件焦点状态发生了变化, 提醒父控件进行相关处理, 确保只有一个焦点存在
  3. 某个控件获取焦点的同时, 旧焦点控件也会失去焦点, 这个动作是在requestChildFocus中发生的.
  4. 焦点移动的关键方法是focusSearch(View, int), 下一篇文章一点见解: 焦点那点事(二)接着分析焦点移动的发起点和过程.

推荐阅读更多精彩内容

  • 上一篇文章, 一点见解: 焦点那点事(一), 了解了焦点相关的一些基本知识, 提到焦点切换的关键方法ViewPar...
    AssIstne阅读 2,492评论 7 6
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 166,753评论 24 703
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 5,401评论 0 17
  • 相信每个人都有时间过的好快的感觉。平时事务繁忙的人这种感觉会更快一些,每个时间点都安排着事情,他们的感觉是时间太快...
    不莱梅阅读 174评论 0 2
  • >Linker中主要的两个源点是dlopen和dlsym。 * dlopen传入两个参数,返回一个文件句柄。传入的...
    sakuradream阅读 586评论 0 0
  • 欢迎共同学习Python编程!运行环境 https://www.tutorialspoint.com/execut...
    蜗牛0718阅读 233评论 0 1
  • 多肉植物有很多细节需要注意,如果做不到,就会造成植株死亡!对于新养的花友来说,肯定会有不周到的地方,所以觉得特别难...
    乐安呀阅读 551评论 0 0