要理解 “焦点冲突时出现异常焦点背景色” 而 “正常焦点分发时无异常” 的本质,需要从焦点分发机制、焦点状态稳定性和背景色触发条件三个维度,结合View和ViewGroup源码深入分析。
一、核心概念:焦点冲突与正常焦点分发的本质区别
- 正常焦点分发:焦点按照既定规则(如
ViewGroup.focusSearch()、View.requestFocus())有序传递,最终唯一确定一个焦点持有者,焦点状态(focused)稳定且单次切换。 - 焦点冲突:多个 View 在同一时间争夺焦点,或焦点分发流程被异常中断 / 拦截,导致焦点状态不稳定(如焦点在多个 View 间快速切换、某个 View 在失去焦点后未正确更新状态),触发非预期的
focused状态切换。
二、焦点冲突时出现异常焦点色的原因(结合源码)
焦点背景色的显示依赖focused状态的稳定性。冲突时,焦点状态的 “异常变化” 会导致StateListDrawable反复触发状态切换,从而显示异常焦点色。
1. 焦点冲突的本质:焦点状态的 “不确定性”
正常焦点分发中,ViewGroup的requestFocus流程会通过onRequestFocusInDescendants方法有序选择子 View,最终只有一个 View 会调用setFocused(true):
java
// ViewGroup.java:正常焦点分发逻辑
@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
// 检查是否允许子View获取焦点(由descendantFocusability控制)
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return super.requestFocus(direction, previouslyFocusedRect);
}
// 有序查找并让子View获取焦点
return onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
// 子View焦点选择逻辑(简化)
protected boolean onRequestFocusInDescendants(int direction, Rect rect) {
// 按照方向(如左/右/上/下)查找最合适的子View
View nextFocus = findFocusableViewInBounds(direction, rect);
if (nextFocus != null) {
return nextFocus.requestFocus(direction, rect); // 唯一子View获取焦点
}
return false;
}
- 正常情况:
nextFocus唯一,setFocused(true)仅触发一次,refreshDrawableState()单次调用,背景色正常显示或不显示(取决于状态定义)。
2. 焦点冲突时的状态异常:多次触发focused状态切换
焦点冲突通常源于以下场景,导致focused状态反复切换:
-
场景 1:多个 View 同时调用
requestFocus()
若两个 View 在短时间内先后调用requestFocus(true),会导致焦点快速从 A 切换到 B,两次触发setFocused:java
// View.java:setFocused方法(简化) public void setFocused(boolean focused) { if (mFocused != focused) { mFocused = focused; refreshDrawableState(); // 触发背景色更新 // ... } }快速切换时,用户可能看到 A 的焦点色还未消失,B 的焦点色已显示(视觉上的 “异常残留”)。
-
场景 2:ViewGroup 拦截焦点后未正确释放
若ViewGroup先调用requestFocus()获取焦点,随后又允许子 View 获取焦点,但未主动调用setFocused(false),会导致父子 View 同时处于focused状态(逻辑冲突):java
// 错误示例:父View未释放焦点 parent.requestFocus(); // 父View获取焦点(mFocused=true) child.requestFocus(); // 子View获取焦点(mFocused=true) // 此时父子View均为focused状态,背景色均可能显示源码中,
View的焦点状态本身不限制 “多 View 同时 focused”(仅逻辑上不合理),因此会同时触发各自的焦点背景色。 -
场景 3:焦点分发被异常中断
若ViewGroup重写onInterceptTouchEvent或requestFocus时,强制返回true但未正确处理焦点传递,会导致焦点 “悬停”(无明确持有者),或某个 View 在onFocusChanged中未正确更新状态:java
// 错误示例:拦截焦点但未处理 @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { return true; // 拦截焦点请求,但未设置自身为focused }此时,焦点状态可能停留在上一个 View,导致其焦点背景色异常残留。
三、正常焦点分发无异常的原因:状态稳定且唯一
正常焦点分发中,焦点状态的切换是 “单次、有序、唯一” 的,因此背景色显示符合预期:
- 唯一焦点持有者:通过
focusSearch和onRequestFocusInDescendants确保最终只有一个 View 的mFocused为true。 - 状态切换单次触发:
setFocused仅调用一次,refreshDrawableState单次触发,StateListDrawable仅切换一次状态(无反复切换)。 - 遵循焦点层级规则:父 View 与子 View 的焦点状态通过
descendantFocusability协调(如FOCUS_AFTER_DESCENDANTS确保子 View 优先获取焦点),避免冲突。
四、如何避免焦点冲突导致的异常焦点背景色
核心思路是消除焦点状态的不确定性,确保焦点分发有序、状态稳定。
1. 合理设置descendantFocusability,避免父子焦点冲突
ViewGroup的descendantFocusability属性控制子 View 与父 View 的焦点优先级,是解决冲突的关键:
-
FOCUS_BLOCK_DESCENDANTS:子 View 无法获取焦点(父 View 完全拦截)。 -
FOCUS_BEFORE_DESCENDANTS:父 View 优先获取焦点,子 View 次之。 -
FOCUS_AFTER_DESCENDANTS:子 View 优先获取焦点,父 View 次之(默认)。
示例:若父 View 需避免与子 View 冲突,可在 XML 中设置:
xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="FOCUS_AFTER_DESCENDANTS"> <!-- 子View优先 -->
<!-- 子View -->
</LinearLayout>
源码依据:ViewGroup的requestFocus会根据descendantFocusability决定是否让子 View 获取焦点,避免无序争夺。
2. 限制可聚焦 View 的范围,减少冲突源
非交互 View(如纯展示的TextView)默认focusable为false,但部分场景(如跑马灯)可能被设置为true,导致不必要的焦点争夺。需针对性禁用:
java
// 禁用非必要View的焦点能力
TextView marqueeText = findViewById(R.id.marquee_text);
marqueeText.setFocusable(false);
marqueeText.setFocusableInTouchMode(false);
// 若需跑马灯,用selected替代focusable:
marqueeText.setSelected(true);
3. 统一焦点请求时机,避免并发争夺
在动态添加 View 或用户交互时,确保同一时间只有一个requestFocus()调用。例如,在RecyclerView中,避免多个 Item 同时请求焦点:
java
// 错误:多个Item同时请求焦点
for (int i = 0; i < items.size(); i++) {
items.get(i).requestFocus(); // 导致焦点冲突
}
// 正确:只让第一个Item请求焦点
if (items.size() > 0) {
items.get(0).requestFocus();
}
4. 重写焦点回调,强制清除异常状态
若冲突导致焦点状态残留,可在onFocusChanged中强制刷新状态:
java
View冲突View = findViewById(R.id.conflict_view);
冲突View.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
// 失去焦点时,强制刷新背景状态
v.refreshDrawableState();
// 进一步清除可能的残留状态
v.setBackground(v.getBackground()); // 重置背景
}
});
5. 全局监控焦点状态,排查冲突源
通过ViewTreeObserver监控全局焦点变化,定位频繁切换焦点的 View:
java
getWindow().getDecorView().getViewTreeObserver().addOnGlobalFocusChangeListener(
(oldFocus, newFocus) -> {
Log.d("FocusConflict", "焦点从 " + oldFocus + " 切换到 " + newFocus);
// 若oldFocus和newFocus频繁切换,说明存在冲突
if (oldFocus != null && newFocus != null) {
// 临时禁用冲突View的焦点
newFocus.setFocusable(false);
}
}
);
五、总结
-
异常焦点色的根源:焦点冲突导致
focused状态不稳定(快速切换、多 View 同时聚焦、状态残留),触发StateListDrawable异常切换。 -
正常分发无异常的原因:焦点状态唯一且稳定,
focused状态单次切换,背景色显示符合预期。 -
避免方案:
-
用
descendantFocusability规范父子焦点优先级; -
禁用非必要 View 的
focusable属性; -
避免并发
requestFocus()调用; -
重写焦点回调强制清除异常状态。
-
通过以上措施,可从根源减少焦点冲突,避免异常焦点背景色的出现。