Android 焦点机制
文章目录
问题
做过 Android TV 开发的同学基本上都碰上过焦点处理的问题,主要分为以下几类:
- 指定第一个获取焦点的View
- 指定 Next Focus View
- 对于 RecyclerView 、Fragment 各种嵌套,如何更优雅的进行焦点记忆?
- 界面上有焦点的View失焦后,系统是怎么进行焦点转移的?
第一点和第二点相对来说比较简单,利用 xml 配置可以轻松解决,但对于第三和第四如果通过业务逻辑代码去写的话会有很多问题,比如与业务强绑定,不好复用,与逻辑代码混杂在一起,不够优雅,可读性差等,那对于这些焦点问题,我们首先需要弄明白系统的焦点处理流程是怎样的?那我们通过 xml 属性和内部方法来进行讲解和说明
xml属性
focusable
决定是否可以获取焦点focusableInTouchMode
决定触屏模式下是否可获取焦点nextFocusUp
指定按键向上时获取焦点的 View IDnextFocusDown
指定按键向下时获取焦点的 View IDnextFocusRight|nextFocusLeft
向右、向左按键指定 View IDnextFocusFoward
回车键获取焦点的 View
类方法
hasFocus()
当前 View|ViewGroup 是否有焦点或者子View 是否有焦点isFocused()
当前 View 是否有焦点requestFocus()
请求获取焦点clearFocus()
清除焦点,同时会向上清除链路上涉及的View的焦点状态setFocusable()
参考focusable
说明setFocusableInTouchMode()
参考focusableInTouchMode
说明
hasFocus()
首先是 hasFocus() 方法,它在 View 和 ViewGroup 中的定义分别如下:
// View.java
/**
* Returns true if this view has focus itself, or is the ancestor of the
* view that has focus.
*
* @return True if this view has or contains focus, false otherwise.
*/
@ViewDebug.ExportedProperty(category = "focus")
public boolean hasFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}
// ViewGroup.java
/**
* Returns true if this view has or contains focus
*
* @return true if this view has or contains focus
*/
@Override
public boolean hasFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0 || mFocused != null;
}
PFLAG_FOCUSED
变量就是用来标识这个 View 是否具有焦点
mFocused
是 ViewGroup 特有的,用于记录该 Group 下获取到焦点的 View
通过看 View 和 ViewGroup 的 hasFocus() 方法,涉及的含义是有差异的。
isFocused()
isFocused() 函数只有在 View 中定义,获取当前 View 本身是否有焦点,其定义如下:
/**
* Returns true if this view has focus
*
* @return True if this view has focus, false otherwise.
*/
@ViewDebug.ExportedProperty(category = "focus")
public boolean isFocused() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}
会发现和 View 的 hasFocus() 一致,ViewGroup 中表示本身是否有焦点,和 hasFocus 有区别
requestFocus()
requestFocus() 在 View 中的定义如下:
public final boolean requestFocus() {
return requestFocus(View.FOCUS_DOWN);
}
public final boolean requestFocus(int direction) {
return requestFocus(direction, null);
}
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
return requestFocusNoSearch(direction, previouslyFocusedRect);
}
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
if ((mViewFlags & FOCUSABLE) != FOCUSABLE
|| (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
// need to be focusable in touch mode if in touch mode
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}
// need to not have any parents blocking us
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
requestFocus 返回 boolean 值,看是否成功获取到了焦点,可以看到 xml 属性中的配置在此处有所体现,获取焦点主要还得看 handleFocusGainInternal
方法。这个等下再看,我们来看下 ViewGroup 的 requestFocus 又是怎样?
requestFocus() 方法在 ViewGroup 中的定义如下:
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " ViewGroup.requestFocus direction="
+ direction);
}
int descendantFocusability = getDescendantFocusability();
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
return super.requestFocus(direction, previouslyFocusedRect);
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
return took ? took : super.requestFocus(direction, previouslyFocusedRect);
}
default:
throw new IllegalStateException("descendant focusability must be "
+ "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
+ "but is " + descendantFocusability);
}
}
ViewGroup 重写了 View 的 requestFocus() 方法,它会根据 descendantFocusability
的值的不同进行不同的处理,是不是发现 xml 的很多属性是怎么起作用的了,descendantFocusability
有三个可能的值:
FOCUS_BLOCK_DESCENDANTS
对焦点进行拦截,不给子 View 进行请求焦点的机会FOCUS_BEFORE_DESCENDANTS
先于子View进行焦点获取,只有自身没有获取到焦点才让子 View 去获取焦点FOCUS_AFTER_DESCENDANTS
先让子 View 获取焦点,子 View 未获取到焦点时,自身再去获取焦点
descendantFocusability
属性的默认值为 FOCUS_BEFORE_DESCENDANTS
当为 FOCUS_BLOCK_DESCENDANTS
时,通过调用父类 requestFocus() 方法为自己请求焦点;
当为 FOCUS_BEFORE_DESCENDANTS
时,先为自己请求焦点,如果未能成功,那么再调用 onRequestFocusInDescendants() 方法,这个方法从名字就能知道是为子 View 请求焦点,或者说是把请求焦点的任务交给了后代;
当为 FOCUS_BEFORE_DESCENDANTS
时,与 FOCUS_BEFORE_DESCENDANTS 逻辑刚好相反。
因此,对于 ViewGroup 来说,这个方法的返回值和 View 有点不同:如果返回 true,说明这个 ViewGroup 获取了焦点或者它的某个后代成功地获取了焦点;否则,这个 ViewGroup 未能成功获取焦点并且它的子 View 也未能成功获取焦点。
现在我们看下 onRequestFocusInDescendants() 的定义:
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
int index;
int increment;
int end;
int count = mChildrenCount;
if ((direction & FOCUS_FORWARD) != 0) {
index = 0;
increment = 1;
end = count;
} else {
index = count - 1;
increment = -1;
end = -1;
}
final View[] children = mChildren;
for (int i = index; i != end; i += increment) {
View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
if (child.requestFocus(direction, previouslyFocusedRect)) {
return true;
}
}
}
return false;
}
这个方法会遍历所有的子 View,寻找第一个自身或者后代能够成功获取焦点的 View,如果找到了,停止后续的遍历并返回 true;如果不存在这样的 View,返回 false。
unFocus()
这个方法是内部方法,应用程序不能直接调用,它在 View 和 ViewGroup 中有不同的定义:
// View.java
void unFocus(View focused) {
if (DBG) {
System.out.println(this + " unFocus()");
}
clearFocusInternal(focused, false, false);
}
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
mPrivateFlags &= ~PFLAG_FOCUSED;
if (propagate && mParent != null) {
mParent.clearChildFocus(this);
}
onFocusChanged(false, 0, null);
refreshDrawableState();
if (propagate && (!refocus || !rootViewRequestFocus())) {
notifyGlobalFocusCleared(this);
}
}
}
// ViewGroup.java
@Override
void unFocus(View focused) {
if (DBG) {
System.out.println(this + " unFocus()");
}
if (mFocused == null) {
super.unFocus(focused);
} else {
mFocused.unFocus(focused);
mFocused = null;
}
}
- View 中通过调用 clearFocusInternal() 方法清除 PFLAG_FOCUSED 标识,实现清除焦点
- ViewGroup 中相对更复杂,如果 mFocused 是 null 则直接调用父类 unFocus 方法,否则执行 mFocused 的 unFocus 方法,将焦点路径上的所有 mFocused 清空,并将真实获取焦点的View 清除焦点
clearFocus()
这个方法只在 View 中有定义:
public void clearFocus() {
if (DBG) {
System.out.println(this + " clearFocus()");
}
clearFocusInternal(null, true, true);
}
它也会调用 clearFocusInternal() 方法,只不过传入的参数和 unFocus() 中的不同。其作用是如果此 View 有焦点的话,清除自身的焦点并将焦点路径删除。
焦点路径
我们 DecorView 是一个窗口的根节点,所有的 View 都在以 DecorView 为根的一颗树形结构上,而一个窗口同时仅能最多一个 View 获取到焦点,那么从 DecorView 到拥有焦点的 View 经过的路径称为焦点路径。
而在每次焦点重新确定后,系统都会建立这样一条焦点路径,这个就是在 ViewGroup 中通过 mFocused 变量来进行标识这条路径。
一个窗口中只有一个焦点视图,因此焦路径也只有一条,而焦点视图是会发生变化的,所以可以推测,系统在建立新的焦点路径时,会将原来的那条焦点路径删除。
现在我们来分析系统建立和删除焦点路径的过程。我们知道,View 成功获取焦点是在 handleFocusGainInternal() 中进行的,因此我们可以以这个方法为切入点追踪系统建立焦点路径的过程。handleFocusGainInternal() 定义如下:
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " requestFocus()");
}
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
if (mParent != null) {
mParent.requestChildFocus(this, this);
updateFocusedInCluster(oldFocus, direction);
}
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
}
}
将这个 View 的 PFLAG_FOCUSED
标志位设为 1,也就是让其成功地获取了焦点;
然后调用 mParent 的 requestChildFocus()
方法。这个方法有两个参数,第一个参数传入的是调用 mParent 该方法的那个子 View,第二个参数表示具有焦点的那个 View。
requestChildFocus
这个方法的定义在 ViewGroup
中:
public void requestChildFocus(View child, View focused) {
if (DBG) {
System.out.println(this + " requestChildFocus()");
}
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
// Unfocus us, if necessary
super.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);
}
}
这个方法的逻辑很清晰:
- super.unFocus() 清除自身焦点
- 清理旧焦点 View focus 状态,并将
mFocused
重新赋值 - 向上递归调用 mParent 的
requestChildFocus
方法,完成焦点路径的切换
通过以上流程,即可实现将旧的焦点路径清除,并建立一条新的焦点路径
刚开始焦点是如何确定的?
我们知道 View 需要获取到焦点,就一定会调用 handleFocusGainInternal() 方法,那我们直接在该方法中加入断点,看他的调用栈是如何的?
根据方法栈我们可以一直追踪到 ViewRootImpl#focusableViewAvailable(),是这个方法触发了焦点的确定过程:
@Override
public void focusableViewAvailable(View v) {
checkThread();
if (mView != null) {
if (!mView.hasFocus()) {
if (sAlwaysAssignFocus) {
v.requestFocus();
}
} else {
// the one case where will transfer focus away from the current one
// is if the current view is a view group that prefers to give focus
// to its children first AND the view is a descendant of it.
View focused = mView.findFocus();
if (focused instanceof ViewGroup) {
ViewGroup group = (ViewGroup) focused;
if (group.getDescendantFocusability() == ViewGroup.FOCUS_AFTER_DESCENDANTS
&& isViewDescendantOf(v, focused)) {
v.requestFocus();
}
}
}
}
}
可以看到只是简单的调用了 v 的 requestFocus() 方法去获取焦点,v 的实际类型是 DecorView,这样是不是所有的流程都清晰了?
焦点移动
上面讲到的是焦点获取的流程,在系统窗口失焦或者主动调用 requestFocus 时可以参考以上流程,那么当按键(上下左右)触发时,又是怎么确定下一个焦点的呢?这个时候可以看 ViewRootImpl 的 processKeyEvent() 方法中 performFocusNavigation 方法的调用:
private boolean performFocusNavigation(KeyEvent event) {
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 view
focused.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 true;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return true;
}
} else {
if (mView.restoreDefaultFocus()) {
return true;
}
}
}
return false;
}
主要做了三个步骤:
- 如果View没有处理按键, 把上下左右tab等按键转换成对应方向
- 在当前焦点 View 上通过
focusSearch
方法查找对应方向的下一个View - 查找到的 View 调用 requestFocus ,因此主要的流程在
focusSearch
中
focusSearch()
View 的 focusSearch 方法:
public View focusSearch(@FocusRealDirection int direction) {
if (mParent != null) {
return mParent.focusSearch(this, direction);
} else {
return null;
}
}
ViewGroup 的 focusSearch 方法:
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 info.
return FocusFinder.getInstance().findNextFocus(this, focused, direction);
} else if (mParent != null) {
return mParent.focusSearch(focused, direction);
}
return null;
}
ViewRootImpl 的 focusSearch 方法:
@Override
public View focusSearch(View focused, int direction) {
checkThread();
if (!(mView instanceof ViewGroup)) {
return null;
}
return FocusFinder.getInstance().findNextFocus((ViewGroup) mView, focused, direction);
}
可以发现我们可以通过重写 focusSearch 方法来改变系统默认的焦点获取顺序
FocusFinder 工具类查找焦点
通过以上代码会发现焦点查找都是通过 FocusFinder 工具类进行的,我们来看下核心的 findNextFocus 方法:
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
if (focused != null) {
next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
}
if (next != null) {
return next;
}
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
effectiveRoot.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
核心步骤也就两步:
- findNextUserSpecifiedFocus 查询是否配置 nextFocusXXX ,有的话直接返回
- effectiveRoot 获取 focusables 列表,根据方向在 focusables 列表中查找最近的 View
总结
以上讲述了焦点是如何确定的以及如何转移的,那回到我们一开始的问题,我们该如何更优雅的去处理失焦和焦点记忆问题?
- 重写 ViewGroup requestChildFocus 方法记录最后获取焦点 View,实现焦点记忆
- 重写 ViewGroup focusSearch 方法,实现焦点转移顺序修改
- 重写 addFocusables 方法,处理失焦问题
提供一个重写的 FrameLayout 示例:
public class MFrameLayout extends FrameLayout {
private static final String TAG = "MFrameLayout";
private IFocusSearchCallback mCallback;
private View mLastFocusView = null;
public MFrameLayout(@NonNull Context context) {
super(context);
}
public MFrameLayout(@NonNull Context context,
@Nullable AttributeSet attrs) {
super(context, attrs);
}
public MFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void requestChildFocus(View child, View focused) {
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
super.requestChildFocus(child, focused);
mLastFocusView = child.findFocus();
}
@Override
public View focusSearch(View focused, int direction) {
// 先在本ViewGroup内部找,如果找到,则直接返回
View findChildView = FocusFinder.getInstance().findNextFocus(this, focused, direction);
if (findChildView != null) {
return findChildView;
}
View view = super.focusSearch(focused, direction);
if (mCallback != null && (view == null || !MyViewUtils.hasView(view, this))) {
view = mCallback.searchFailed(focused, view, direction);
}
return view;
}
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
// 当前无焦点的情况有两种
// 1.之前获取到过焦点,然后焦点移出,此时需要处理焦点记忆问题
// 2.一次都没有获取过焦点,需要指定第一个获取焦点的View,否则走默认系统行为
if (!hasFocus()) {
// step 1 焦点记忆
if (mLastFocusView != null && MyViewUtils.isViewReallyVisible(mLastFocusView)) {
mLastFocusView.addFocusables(views, direction, focusableMode);
} else if (mCallback != null) {
// step 2 获取第一个获取焦点的子View
View v = mCallback.firstFocusView();
if (v != null && v.getVisibility() == VISIBLE) {
v.addFocusables(views, direction, focusableMode);
} else {
super.addFocusables(views, direction, focusableMode);
}
}
} else {
super.addFocusables(views, direction, focusableMode);
}
}
public void setFocusSearchCallback(IFocusSearchCallback callback) {
this.mCallback = callback;
}
}
参考:
文章作者 Brook
上次更新 2021-01-07