一、申请聚光灯:演员的 "上台资格" 审查
想象一个 Android 舞台(Activity)上,每个 View 都是等待聚光灯的演员。当你调用button.requestFocus()时,就像演员对导演说:"我想站在聚光灯下!"
但导演不会随便给灯,首先会检查演员的 "资格证":
java
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// 1. 必须有"可聚焦"资格证(focusable=true)且在舞台上可见
if ((mViewFlags & FOCUSABLE) != FOCUSABLE || (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false; // 没资格,直接淘汰
}
// 2. 如果是触摸模式(观众用手指点选),还得有"触摸模式资格证"
if (isInTouchMode() && (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false; // 触摸模式下没资格,淘汰
}
// 3. 检查有没有上级导演拦着(父容器设置了FOCUS_BLOCK_DESCENDANTS)
if (hasAncestorThatBlocksDescendantFocus()) {
return false; // 被上级拦住,不让上台
}
handleFocusGainInternal(direction, previouslyFocusedRect);
return true; // 资格通过,准备上台
}
故事版解读:
- 就像演员需要带齐证件(focusable 和 visibility),触摸模式下还得额外带 "触摸许可证"(focusableInTouchMode)
- 如果演员的爸爸(父容器)对导演说 "别让我儿子上台"(FOCUS_BLOCK_DESCENDANTS),直接被拒
二、舞台调度:旧演员下台,新演员登记
通过资格审查后,演员会走到舞台中央,但导演需要先让原来的聚光灯演员下台:
java
void handleFocusGainInternal(...) {
// 给自己贴一个"聚光灯演员"的标签
mPrivateFlags |= PFLAG_FOCUSED;
// 找到当前站在聚光灯下的旧演员
View oldFocus = getRootView().findFocus();
// 告诉爸爸(父容器):"我要上台了!"
if (mParent != null) {
mParent.requestChildFocus(this, this); // 通知父容器更新焦点
}
// 通知观众(ViewTreeObserver)聚光灯换演员了
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
// 演员自己换衣服(刷新样式)
onFocusChanged(true, ...);
refreshDrawableState();
}
父容器(导演组)接到通知后,会执行调度逻辑:
java
@Override
public void requestChildFocus(View child, View focused) {
// 如果导演组老大说"不让小孩上台",直接不管
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
// 让原来的演员下台
super.unFocus(focused);
// 登记新演员为当前焦点
if (mFocused != child) {
if (mFocused != null) {
mFocused.unFocus(focused); // 旧演员下台
}
mFocused = child; // 新演员登记
}
// 告诉导演组更高层:"现在焦点在我这儿"
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
故事版解读:
- 新演员(View)给自己戴一个 "当前焦点" 的徽章(mPrivateFlags 标记位)
- 导演组(ViewGroup)先让旧演员(oldFocus)下台,刷新他的服装(失焦样式)
- 然后把新演员的名字记在导演组的小本本上(mFocused 变量),并层层上报给总导演(ViewRootImpl)
- 总导演收到通知后,喊一声:"灯光师准备,换焦点啦!"(scheduleTraversals 触发 UI 重绘)
三、导演组的三种 "偏心" 策略
不同的导演组(ViewGroup)有不同的选角策略,决定谁优先上台:
java
@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
int strategy = getDescendantFocusability();
switch (strategy) {
case FOCUS_BLOCK_DESCENDANTS:
// 导演自己想上台,不让小孩上
return super.requestFocus(direction, previouslyFocusedRect);
case FOCUS_BEFORE_DESCENDANTS:
// 导演先自己试试,不行再让小孩上
if (super.requestFocus(direction, previouslyFocusedRect)) {
return true;
} else {
return onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
case FOCUS_AFTER_DESCENDANTS:
// 先让所有小孩试,都不行导演自己上
if (onRequestFocusInDescendants(direction, previouslyFocusedRect)) {
return true;
} else {
return super.requestFocus(direction, previouslyFocusedRect);
}
}
}
其中onRequestFocusInDescendants是遍历所有小演员(子 View):
java
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
// 根据方向决定从左到右还是从右到左遍历演员
int index = (direction & FOCUS_FORWARD) != 0 ? 0 : mChildrenCount - 1;
int increment = (direction & FOCUS_FORWARD) != 0 ? 1 : -1;
int end = (direction & FOCUS_FORWARD) != 0 ? mChildrenCount : -1;
// 遍历所有可见的小演员,让他们申请焦点
for (int i = index; i != end; i += increment) {
View child = mChildren[i];
if (child.isVisible() && child.requestFocus(direction, previouslyFocusedRect)) {
return true; // 有小演员成功上台,结束选拔
}
}
return false;
}
故事版解读:
- FOCUS_BLOCK_DESCENDANTS:导演组老大说 "我自己上,小孩别抢",直接自己申请焦点
- FOCUS_BEFORE_DESCENDANTS:导演先试镜,如果自己演不好(requestFocus 失败),再让小演员一个个试
- FOCUS_AFTER_DESCENDANTS:先让所有小演员排队试镜,直到没人能演,导演才自己上
四、关键角色总结
| 故事角色 | Android 术语 | 核心作用 |
|---|---|---|
| 演员 | View | 申请焦点的控件,需满足资格条件 |
| 导演组 | ViewGroup | 管理子 View 焦点,执行调度策略 |
| 总导演 | ViewRootImpl | 最终触发 UI 重绘,更新焦点显示 |
| 聚光灯徽章 | mPrivateFlags(PFLAG_FOCUSED) | 标记当前焦点状态 |
| 导演组小本本 | mFocused | 记录当前焦点所在的子 View |
| 资格证 | focusable/focusableInTouchMode | 决定 View 能否获取焦点 |
| 导演组命令 | FOCUS_BLOCK_DESCENDANTS 等 | 控制焦点分配策略 |
五、小白可操作的代码示例
java
// 1. 给按钮设置焦点资格证
Button button = findViewById(R.id.my_button);
button.setFocusable(true); // 普通资格证
button.setFocusableInTouchMode(true); // 触摸模式资格证
// 2. 让按钮申请聚光灯
button.requestFocus();
// 3. 监听聚光灯变化(演员换装通知)
button.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
v.setBackgroundColor(Color.YELLOW); // 获得焦点时变黄
} else {
v.setBackgroundColor(Color.GRAY); // 失去焦点时变灰
}
});
// 4. 给父容器设置导演策略(比如先让小孩上)
ViewGroup parent = findViewById(R.id.parent_layout);
parent.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
通过这个故事,你可以理解:焦点获取本质是 View 向父容器申请资格,父容器按策略调度,最终更新状态并通知 UI 的过程。就像舞台上的聚光灯,只有满足条件的演员才能站上去,而导演组决定了谁优先获得这个机会。