Android开发使用的手机一般处于触摸模式, 因此默认情况下并不会有焦点, 所以之前一直对焦点不是很熟悉. 但是在电视端开发上, 焦点的处理可以说直接影响了用户体验, 因此借此熟悉下焦点处理的流程.
本文着重介绍焦点相关的一些关键方法, 先从局部了解下焦点的一些基础规则和行为特点.
获取焦点的前提
-
View#isFocusable
返回true
, 如果在触摸模式, 则View#isFocusableInTouchMode
也要返回true
- 控件必须可见
- 控件相关的父控件, 包括祖父控件等,
ViewGroup#getDescendantFocusability()
不能为ViewGroup#FOCUS_BLOCK_DESCENDANTS
View
获取焦点
调用View#requestFocus
系列方法
进入View#requestFocusNoSearch
在该方法中会对控件的当前状态进行判断, 如果不符合获取焦点的前提则直接返回false
告知调用方, 控件不会获取焦点
只要符合前提就会继续执行, 最终必定返回true
, 不论当前控件的焦点状态是否有改变
符合前提则进入 View#handleFocusGainInternal
如果控件已经持有焦点, 则不会做任何事情, 直接结束流程
如果没有焦点,
- 改变焦点标志位, 此时
View#isFocused
就会返回true
了 - 通过
ViewParent#requestChildFocus
通知父控件即将获取焦点 - 通知其他部件焦点状态发生变化(略, 本文不关心)
- 触发
OnGlobalFocusChangeListener
的回调 - 触发
OnFocusChangeListener
回调 - 重绘, 结束流程
清除焦点
调用View#clearFocus
主动放弃焦点
如果控件本身没有焦点, 则什么都不会发生
如果控件持有焦点
- 改变焦点标志位
- 通过
ViewParent#clearChildFocus
通知父控件, 当前控件放弃焦点 - 触发
OnFocusChangeListener
回调 - 调用当前控件的根控件(
rootView
)的requestFocus
方法 - 如果步骤4中没有找到新的焦点控件, 则触发
OnGlobalFocusChangeListener
的回调, 注: 如果找到新的焦点控件, 那么新的控件获取焦点的过程中就会回调OnGlobalFocusChangeListener
, 所以这里只有没找到才进行步骤5
注: 由上流程可以知道, 如果根控件查找控件的时候找到的控件还是这个控件, 那么OnFocusChangeListener
就会被调用两次, 先失去焦点, 然后又获取到焦点
ViewGroup
焦点分发策略DescendantFocusability
-
FOCUS_BLOCK_DESCENDANTS
: 拦截焦点, 直接自己尝试获取焦点 -
FOCUS_BEFORE_DESCENDANTS
: 首先自己尝试获取焦点, 如果自己不能获取焦点, 则尝试让子控件获取焦点 -
FOCUS_AFTER_DESCENDANTS
: 首先尝试把焦点给子控件, 如果所有子控件都不要, 则自己尝试获取焦点
获取焦点
根据焦点分发策略决定下面两个方法的调用顺序
通过View#requestFocus
自己获取焦点
把ViewGroup
看作View
, 直接走View
获取焦点的流程来获取焦点
进入onRequestFocusInDescendants
可以传入方向来改变遍历的顺序, 默认是从0递增
遍历子控件, 调用子控件的View#requestFocus
来尝试把焦点给可见的子控件, 某个子控件成功获取到焦点后, 停止遍历
注: 重写该方法可以改变ViewGroup
分发焦点给子控件的行为, 例如遍历顺序
清除焦点
如果焦点控件不是它的子控件, 那么直接把当前的ViewGroup
看作View
走View#clearFocus
流程, 反之则调用焦点控件的View#clearFocus
.
注: 区别在于重新分发焦点时的选择范围.
ViewParent
ViewParent
是一个接口, 表示了一个父控件应该具备的功能, ViewGroup
实现了该接口.
与焦点相关的接口有4个
clearChildFocus
当子控件主动放弃焦点的时候会通过这个方法通知父控件.
在ViewGroup
的默认实现中, 会置空当前焦点控件, 表示该父控件下没有子控件获取焦点, 接着把这个事件通知给上级父控件.
注1: 这个方法名有点让人误解, 应该把这个方法看作一个回调, 表明了一个状态, 在这个方法中并没有做清除焦点的操作, 实际的清除动作是在View#clearFocus
中完成的, 这个方法也是在这个流程中被调用的. 而且是在子控件已经放弃焦点后调用.
注2: 区分主动放弃和因为其他控件获取了焦点而被动丢失焦点的情况
requestChildFocus
当子控件获取了焦点后, 通过这个方法通知父控件. 同clearChildFocus
类似, 应该把这个方法看作是一个回调.
在ViewGroup
的默认实现中, 因为同时只会有一个焦点, 因此在这里应该把旧焦点清除掉, 大致流程如下
- 如果焦点分发策略为
FOCUS_BLOCK_DESCENDANTS
则什么也不干 - 如果父控件自身有焦点, 通过
View#unFocus
清除焦点 - 如果父控件当前已经有焦点控件, 并且和新的控件不一致, 那么通过
View#unFocus
清除旧焦点控件的焦点 - 向上传递这个事件
内部清除焦点View#unFocus
这个方法和View#clearFocus
相同点在于都会执行View#clearFocusInternal
方法, 区别在于unFocus
只会执行clearFocus
中, 上文清除焦点中提到的1, 3步骤, 因此不会通知父控件, 不会触犯requestChildFocus
回调, 因为这个方法是在子控件被动失去焦点时调用的, 所以也不会触发焦点分发.
因此新旧焦点切换的大致流程是
- 新焦点控件获取焦点
- 新焦点控件通知父控件
- 父控件清除旧焦点控件的焦点
- 旧焦点控件回调
OnFocusChangeListener
- 触发
OnGlobalFocusChangeListener
的回调 - 新焦点控件回调
OnFocusChangeListener
focusableViewAvailable
通知父控件, 子控件的状态发生改变, 从不能获取焦点, 变成可能可以获取焦点.
有两种情况会被调用
- 子控件从unFocusable变为focusable
- 子控件从不可见变为可见, 即使它不是focusable也会调用, 因此它的子控件可能可以获取焦点.
而ViewGroup
中的默认实现只是在符合条件的情况下把这个事件向上传递给自己的父控件.
focusSearch(View, int)
查找指定方向中最近的, 想要获取焦点的控件.
这个方法直接决定了焦点的移动规则, 非常重要.
在ViewGroup
的默认实现中, 会一直向上传递, 直到根控件, 接着调用FocusFinder#findNextFocus
方法查找合适的控件. 稍后再分析这个方法.
View
中有一个同名的方法focusSearch(int)
, 该方法直接调用了父控件的focusSearch(View, int)
来查找下一个焦点控件
findNextFocus
查找步骤大致如下
手动指定
如果有通过android:nextFocusDown
等手动指定控件, 则返回对应方向的控件
动态计算
- 获取所有可以获取焦点的控件的集合
- 计算相对当前焦点控件的坐标
- 根据方向选择合适的控件
总结
- 分析的过程要注意区分
View
和ViewGroup
的差异和新焦点和旧焦点控件的方法调用. -
ViewParent
是一个接口, 其中一些方法应该看作是回调, 子控件通过这些回调通知父控件焦点状态发生了变化, 提醒父控件进行相关处理, 确保只有一个焦点存在 - 某个控件获取焦点的同时, 旧焦点控件也会失去焦点, 这个动作是在
requestChildFocus
中发生的. - 焦点移动的关键方法是
focusSearch(View, int)
, 下一篇文章一点见解: 焦点那点事(二)接着分析焦点移动的发起点和过程.