将结合Android源码深入分析焦点冲突导致异常背景色的机制,并提供专业解决方案。以下分析基于Android 11源码(API 30)。
源码级焦点冲突分析
1. 正常焦点分发流程(无背景色异常)
java
scss
// ViewGroup.java
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
// 步骤1:检查自身是否能获取焦点
if ((mViewFlags & FOCUSABLE) == FOCUSABLE) {
// 步骤2:清除当前焦点链
clearFocusWithin(previouslyFocusedRect);
// 步骤3:设置新焦点
return super.requestFocus(direction, previouslyFocusedRect);
}
// ... 子View焦点查找逻辑
}
// View.java
void handleFocusGainInternal(@FocusReal int direction, Rect previouslyFocusedRect) {
// 焦点状态标记更新
mPrivateFlags |= PFLAG_FOCUSED;
// 通知焦点变化
onFocusChanged(true, direction, previouslyFocusedRect);
// 刷新Drawable状态(关键!)
refreshDrawableState();
// 通知监听器
notifyGlobalFocusCleared(this);
}
正常流程特点:
- 严格的焦点链管理(
clearFocusWithin()
确保单焦点) - 状态变更有序(先清除旧焦点再设置新焦点)
refreshDrawableState()
仅在合法焦点变更时调用
2. 焦点冲突时的异常流程
java
scss
// ViewRootImpl.java
private void performTraversals() {
// 输入事件处理
processInputEvents();
// 布局阶段
performLayout();
// 焦点处理(可能冲突点)
handleFocusDuringLayout();
}
// ViewRootImpl.handleFocusDuringLayout()
void handleFocusDuringLayout() {
// 当多个View同时声明需要焦点时
if (mFocusedView != null && mNewFocusedView != null
&& mFocusedView != mNewFocusedView) {
// 冲突解决:根据Z-order和添加顺序
if (compareFocusOrder(mNewFocusedView, mFocusedView) > 0) {
// 旧焦点未正确清除!
mFocusedView.unFocus();
mFocusedView = mNewFocusedView;
mNewFocusedView.setFocus();
} else {
mNewFocusedView.clearFocus();
}
}
}
冲突导致背景色异常的根本原因:
- 状态未重置 :
unFocus()
调用时,原焦点View的PFLAG_FOCUSED
标志未立即清除 - 绘制时序问题 :
refreshDrawableState()
在冲突解决期间可能被多次调用 - Drawable状态污染 :焦点Drawable的
STATE_FOCUSED
状态残留
焦点背景色绘制机制
java
scss
// View.java
protected void drawableStateChanged() {
final int[] state = getDrawableState();
boolean changed = false;
// 背景Drawable状态更新
if (mBackground != null && mBackground.isStateful()) {
changed |= mBackground.setState(state);
}
// 默认焦点高亮
if (mDefaultFocusHighlight != null
&& mDefaultFocusHighlight.isStateful()
&& (changed |= mDefaultFocusHighlight.setState(state))) {
invalidate();
}
// 状态传播
if (changed) {
invalidate();
}
}
// 焦点状态定义
private static final int[] FOCUSED_STATE_SET = {
android.R.attr.state_focused
};
避免焦点冲突导致背景色的解决方案
方案1:全局焦点冲突防护(推荐)
java
scss
// 在Activity基类中重写
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 在输入事件前重置焦点状态
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
clearStaleFocus();
}
return super.dispatchTouchEvent(ev);
}
private void clearStaleFocus() {
final View currentFocus = getCurrentFocus();
if (currentFocus != null) {
// 检测焦点冲突
if (!isValidFocus(currentFocus)) {
// 强制清除无效焦点
currentFocus.clearFocus();
// 重要:手动重置Drawable状态
resetDrawableState(currentFocus);
}
}
}
private void resetDrawableState(View view) {
// 重置背景Drawable状态
if (view.getBackground() instanceof StateListDrawable) {
StateListDrawable bg = (StateListDrawable) view.getBackground();
bg.setState(StateSet.WILD_CARD);
}
// 重置默认焦点高亮(API 26+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Drawable highlight = view.getDefaultFocusHighlight();
if (highlight != null && highlight.isStateful()) {
highlight.setState(StateSet.WILD_CARD);
}
}
// 请求重绘
view.postInvalidate();
}
方案2:焦点获取拦截器
java
scala
public class FocusSafeViewGroup extends FrameLayout {
// 焦点冲突解决标志
private boolean mResolvingConflict = false;
@Override
public void requestChildFocus(View child, View focused) {
if (mResolvingConflict) {
// 冲突解决期间阻止焦点变更
return;
}
// 检测潜在冲突
if (getFocusedChild() != null && focused != null
&& getFocusedChild() != focused) {
mResolvingConflict = true;
// 安全转移焦点
getFocusedChild().clearFocus();
resetDrawableState(getFocusedChild());
super.requestChildFocus(child, focused);
// 重置冲突标志
post(() -> mResolvingConflict = false);
} else {
super.requestChildFocus(child, focused);
}
}
@Override
protected void onFocusChanged(boolean gainFocus, int direction,
Rect previouslyFocusedRect) {
// 焦点变化时强制更新状态
if (!gainFocus) {
resetDrawableState(this);
}
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
}
}
方案3:Drawable状态防护
java
java
public class FocusSafeDrawable extends StateListDrawable {
private final SparseBooleanArray mValidStates = new SparseBooleanArray();
public FocusSafeDrawable() {
// 标记合法状态组合
mValidStates.put(StateSet.get(StateSet.VIEW_STATE_FOCUSED), true);
mValidStates.put(StateSet.get(StateSet.VIEW_STATE_PRESSED), true);
// ... 添加其他合法组合
}
@Override
protected boolean onStateChange(int[] stateSet) {
// 过滤非法焦点状态
if (containsFocusState(stateSet) && !isValidStateCombination(stateSet)) {
// 拒绝应用焦点状态
return super.onStateChange(removeFocusState(stateSet));
}
return super.onStateChange(stateSet);
}
private boolean containsFocusState(int[] stateSet) {
for (int state : stateSet) {
if (state == android.R.attr.state_focused) {
return true;
}
}
return false;
}
private int[] removeFocusState(int[] stateSet) {
List<Integer> states = new ArrayList<>();
for (int state : stateSet) {
if (state != android.R.attr.state_focused) {
states.add(state);
}
}
return Ints.toArray(states);
}
}
针对特定场景的优化
悬浮窗焦点冲突解决
java
scss
// 悬浮窗初始化时
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
...,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
// 关键标志组合
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
PixelFormat.TRANSLUCENT
);
// 跑马灯无需焦点
textView.setFocusable(false);
textView.setFocusableInTouchMode(false);
textView.setSelected(true); // 激活跑马灯
// 焦点事件监听
textView.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
// 立即转移焦点
v.clearFocus();
mainActivity.getTabLayout().requestFocus();
}
});
TabLayout焦点防护
java
java
public class SafeTabLayout extends TabLayout {
@Override
public void selectTab(Tab tab, boolean updateIndicator) {
// 在Tab切换前清除焦点状态
if (getSelectedTab() != null) {
View tabView = getSelectedTab().view;
tabView.clearFocus();
resetDrawableState(tabView);
}
super.selectTab(tab, updateIndicator);
}
@Override
protected void onFocusChanged(boolean gainFocus, int direction,
Rect previouslyFocusedRect) {
if (!gainFocus) {
// 失去焦点时重置所有Tab状态
for (int i = 0; i < getTabCount(); i++) {
Tab tab = getTabAt(i);
if (tab != null) {
resetDrawableState(tab.view);
}
}
}
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
}
}
关键原理总结
-
焦点冲突本质:
- 多个View同时持有
PFLAG_FOCUSED
标志 refreshDrawableState()
在冲突解决期间被错误调用- Drawable的
STATE_FOCUSED
状态未及时清除
- 多个View同时持有
-
解决方案核心:
- 状态隔离:在焦点变更时强制重置Drawable状态
- 冲突拦截:在焦点分发路径中插入冲突检测逻辑
- 时序控制:确保焦点清除操作在状态更新前完成
- 防御式编程:对Drawable状态进行合法性校验
-
最佳实践:
java
scss// 在Activity的onWindowFocusChanged中 @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { // 窗口获得焦点时清理状态 clearStaleFocus(); // 延迟焦点设置 getWindow().getDecorView().post(() -> { if (getCurrentFocus() == null) { safeRequestFocus(tabLayout); } }); } } private void safeRequestFocus(View view) { // 先清除可能的残留焦点 View current = getCurrentFocus(); if (current != null && current != view) { current.clearFocus(); resetDrawableState(current); } // 设置新焦点 view.requestFocus(); }
这些解决方案直接从Android焦点系统的源码实现出发,通过干预焦点分发流程、控制Drawable状态变更时序和添加冲突防护层,彻底解决焦点冲突导致的背景色异常问题。实际应用中建议结合方案1(全局防护)+方案3(Drawable防护)实现最佳效果。