多View焦点冲突导致背景异常浅析

114 阅读6分钟

要理解 “焦点冲突时出现异常焦点背景色” 而 “正常焦点分发时无异常” 的本质,需要从焦点分发机制焦点状态稳定性背景色触发条件三个维度,结合ViewViewGroup源码深入分析。

一、核心概念:焦点冲突与正常焦点分发的本质区别

  • 正常焦点分发:焦点按照既定规则(如ViewGroup.focusSearch()View.requestFocus())有序传递,最终唯一确定一个焦点持有者,焦点状态(focused)稳定且单次切换。
  • 焦点冲突:多个 View 在同一时间争夺焦点,或焦点分发流程被异常中断 / 拦截,导致焦点状态不稳定(如焦点在多个 View 间快速切换、某个 View 在失去焦点后未正确更新状态),触发非预期的focused状态切换。

二、焦点冲突时出现异常焦点色的原因(结合源码)

焦点背景色的显示依赖focused状态的稳定性。冲突时,焦点状态的 “异常变化” 会导致StateListDrawable反复触发状态切换,从而显示异常焦点色。

1. 焦点冲突的本质:焦点状态的 “不确定性”

正常焦点分发中,ViewGrouprequestFocus流程会通过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重写onInterceptTouchEventrequestFocus时,强制返回true但未正确处理焦点传递,会导致焦点 “悬停”(无明确持有者),或某个 View 在onFocusChanged中未正确更新状态:

    java

    // 错误示例:拦截焦点但未处理
    @Override
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        return true; // 拦截焦点请求,但未设置自身为focused
    }
    

    此时,焦点状态可能停留在上一个 View,导致其焦点背景色异常残留。

三、正常焦点分发无异常的原因:状态稳定且唯一

正常焦点分发中,焦点状态的切换是 “单次、有序、唯一” 的,因此背景色显示符合预期:

  1. 唯一焦点持有者:通过focusSearchonRequestFocusInDescendants确保最终只有一个 View 的mFocusedtrue
  2. 状态切换单次触发setFocused仅调用一次,refreshDrawableState单次触发,StateListDrawable仅切换一次状态(无反复切换)。
  3. 遵循焦点层级规则:父 View 与子 View 的焦点状态通过descendantFocusability协调(如FOCUS_AFTER_DESCENDANTS确保子 View 优先获取焦点),避免冲突。

四、如何避免焦点冲突导致的异常焦点背景色

核心思路是消除焦点状态的不确定性,确保焦点分发有序、状态稳定。

1. 合理设置descendantFocusability,避免父子焦点冲突

ViewGroupdescendantFocusability属性控制子 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>

源码依据ViewGrouprequestFocus会根据descendantFocusability决定是否让子 View 获取焦点,避免无序争夺。

2. 限制可聚焦 View 的范围,减少冲突源

非交互 View(如纯展示的TextView)默认focusablefalse,但部分场景(如跑马灯)可能被设置为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);
        }
    }
);

五、总结

  1. 异常焦点色的根源:焦点冲突导致focused状态不稳定(快速切换、多 View 同时聚焦、状态残留),触发StateListDrawable异常切换。

  2. 正常分发无异常的原因:焦点状态唯一且稳定,focused状态单次切换,背景色显示符合预期。

  3. 避免方案

    • descendantFocusability规范父子焦点优先级;

    • 禁用非必要 View 的focusable属性;

    • 避免并发requestFocus()调用;

    • 重写焦点回调强制清除异常状态。

通过以上措施,可从根源减少焦点冲突,避免异常焦点背景色的出现。