Android View焦点总结
本来之前说view下篇是写onMeasure,onLayou,onDraw相关的,笔者做盒子开发,遥控器按键,碰到的都是焦点控制相关。所以先把焦点放到了onMeasure,onLayou,onDraw之前。
-
ViewRoot
-
View的焦点
-
ViewGroup的焦点
-
父容器焦点的处理
-
失去焦点或清除焦点
-
焦点移动
-
FocusFinder查找焦点
-
总结
Android View焦点
Android焦点相关逻辑大部分都在都在View, ViewGroup和FocusFinder三个类中.
ViewRoot
View对象都有一个mParent变量(添加到ViewGroup后), 代指其父容器. 绝大部分View的mParent都是ViewGroup类型, 除了根节点. 一个Window中View根节点DecorView的mParent称为ViewRoot, 在安卓4.0后ViewRoot对应ViewRootImpl, 它不是View的子类, 而是个ViewParent. ViewRootImpl是连接Window和DecorView的纽带, View的焦点, 按键, 布局, 渲染等流程都是从ViewRoot中开始的.
View的焦点
基本流程如下
View(包括ViewGroup)获取焦点都通过如下三个方法
View.java
从上面可以看到前两个最终会执行到第三个方法.
最后的requestFocusNoSearch先判断是否可以获取焦点, 然后进入下面的最后流程:
View.java
上面的流程比较简单: 如果当前没有焦点, 先置焦点标志, 再通知parent, 然后刷新图片.
-
主要的流程在mParent的requestChildFocus里面, 后面会分析. 那里会逐层向上修改焦点View并清除原来有焦点的View的焦点
-
onFocusChange会触发invalidate刷新, 然后调用onFocusChangeListener. 默认情况每个View只能设置一个onFocusChangeListener, 而开发中经常遇到需要设置多个Listener的情况, 我们就可以重写onFocusChange方法, 实现回调多个onFocusChangeListener的需求.
ViewGroup的焦点
ViewGroup获取焦点是在View获取焦点流程中多了内部焦点处理
ViewGroup.java
上面代码中descendantFocusability决定了是先按View焦点流程处理(自己处理焦点)还是先把给子View处理
-
FOCUS_BLOCK_DESCENDANTS 不允许子View获取焦点, 那么按照View的流程进行
-
FOCUS_BEFORE_DESCENDANTS 先按照View的流程处理, 如果自己不能获取焦点则给孩子处理
-
FOCUS_AFTER_DESCENDANTS 先尝试给孩子焦点, 如果没有可获取焦点再按照View流程自己获取焦点
默认值FOCUS_BEFORE_DESCENDANTS, 我们可以通过setDescendantFocusability(int d)
设置
onRequestFocusInDescendants方法是给子类重写使用, 可以控制子View处理焦点. 默认按照子View顺序处理, direction向下或向右则从第一个开始, 向上或向左则从最后一个开始, 直到某个子View获取焦点
注意此方法只在此ViewGroup及其上层View上调用requestFocus时会执行到
父容器焦点的处理
在View获取焦点流程中会调用mParent.requestChildFocus, 维护View树上焦点唯一, 在各层ViewGroup中保存有焦点的子View
ViewGroup.java
先清除自己的焦点, 如果原来内部有焦点, 先清除其焦点, 保存获取焦点的孩子, 然后调用上一层的requestChildFocus. 最后的调用可知, 这个方法会一直调用到View的树的root节点.
在当前ViewGroup内部, 任何一个孩子取得焦点都会执行到这个方法, 因此此方法也是ViewGroup得知孩子焦点变化的方法之一.(可惜不能得知孩子失去焦点)
失去焦点或清除焦点
获取焦点可以是主动的, 但失去焦点一般都是被动的(见上面的代码), 因此逻辑相对简单, 只要清除焦点状态即可.
ViewGroup.java
View.java
注意上面的方法是默认package访问级别的, 我们无法重写也不能调用
也可以主动清除焦点, 与获取焦点流程相似
ViewGroup.java
View.java
ViewGroup.java
以上是安卓View系统焦点处理的全部流程和涉及到的方法, ViewRootImpl的requestChildFocus和clearChildFocus实现我们不需要关注
另外还有以下一些辅助方法
-
boolean isFocusable() View是否可以获取焦点
-
boolean isFocused() View是否获取焦点
-
boolean hasFocus() View/ViewGroup内部是否有焦点
-
View findFocus() 取到View/ViewGroup内部的焦点View
-
View getFocusedChild() 取到ViewGroup内部有焦点的子View
-
View getRootView() 取到根节点View(一般是DecorView或顶层ViewGroup)
焦点移动
除了在代码里面控制焦点, 系统对没有处理的方向键等一些按键自动按照焦点移动来处理, 见下面代码
ViewRootImpl.java
代码比较上, 但是主要做了三个步骤
-
如果View没有处理按键, 把上下左右tab等按键转换成对应方向
-
在当前焦点View上通过focusSearch方法查找对应方向的下一个View
-
查找到的View调用requestFocus
因此主要的流程在focusSearch中
View.java
普通View查找什么都没做, 交给parent来完成.
ViewGroup.java
ViewRootImpl
我们可以重写focusSearch控制焦点移动顺序, 而默认的焦点移动顺序由FocusFinder决定
FocusFinder查找焦点
FocusFinder为public的工具类, 主要就两个方法, 可以在给定的View内在指定方向查找指定View或坐标的下一个焦点
如下:
核心逻辑就两步, 先查找setNextFocusXXId设置的View, 如果没有按照就近算法查找.
具体算法不再分析, SDK里面有源码.
总结
综合上面的流程分析, 我们在实现自定义View时, 对焦点的特殊需求有如下思路
-
requestFocus和clearFocus直接对View清除或转移焦点
-
除了onFocusChangeListener, 还可以在onFocusChange方法中实现一些View失去/获得焦点时通知
-
对ViewGroup, 如果只需要在子View获取焦点时得到通知, 有requestChildFocus方法.
-
重写onRequestFocusInDescendants方法可以控制某些情景下ViewGroup焦点
-
控制焦点移动可以重写focusSearch方法
-
另外还有FocusFinder工具和上面的辅助方法.