View源码解析焦点态背景色的来源及移除方法

45 阅读2分钟

一、焦点态默认背景色的源码分析

1. View 状态管理核心逻辑(View.java)

java

// View.java
public void setFocused(boolean focused) {
    final boolean previouslyFocused = hasFocus();
    if (focused != previouslyFocused) {
        mPrivateFlags = (mPrivateFlags & ~PFLAG_FOCUSED) | (focused ? PFLAG_FOCUSED : 0);
        
        // 焦点状态变化时,触发Drawable状态更新
        refreshDrawableState();
        notifyViewAccessibilityStateChangedIfNeeded(
                AccessibilityEvent.TYPE_VIEW_FOCUSED);
                
        // ... 其他逻辑 ...
    }
}

public void refreshDrawableState() {
    final int[] drawableState = getDrawableState();
    boolean needRefresh = false;
    
    // 更新背景Drawable的状态
    final Drawable bg = mBackground;
    if (bg != null && bg.isStateful()) {
        needRefresh |= bg.setState(drawableState);
    }
    
    // ... 其他Drawable的状态更新 ...
    
    if (needRefresh) {
        invalidate(); // 重绘View
    }
}

2. StateListDrawable 状态匹配机制(StateListDrawable.java)

java

// StateListDrawable.java
@Override
public boolean setState(int[] stateSet) {
    if (!Arrays.equals(mStateSet, stateSet)) {
        int newIndex = selectState(stateSet);
        if (newIndex < 0) {
            newIndex = mDefaultIndex;
        }
        if (newIndex != mCurIndex) {
            mCurIndex = newIndex;
            onStateChange(stateSet);
            return true;
        }
    }
    return false;
}

// 核心方法:选择与当前状态匹配的Drawable
protected int selectState(int[] stateSet) {
    for (int i = 0; i < mStateSpecs.length; i++) {
        int[] stateSpec = mStateSpecs[i];
        if (StateSet.stateSetMatches(stateSpec, stateSet)) {
            return i; // 找到匹配的状态集合,返回对应Drawable的索引
        }
    }
    return -1; // 未找到匹配项
}

二、移除焦点态背景色的 Java 实现方案

方案 1:为 View 设置不含 focused 状态的自定义背景

java

// 方法1:使用透明背景
Button button = findViewById(R.id.button);
button.setBackgroundColor(Color.TRANSPARENT);

// 方法2:自定义StateListDrawable(不含focused状态)
StateListDrawable stateList = new StateListDrawable();
stateList.addState(new int[]{android.R.attr.state_pressed}, 
        getResources().getDrawable(R.drawable.pressed_bg));
stateList.addState(StateSet.WILD_CARD, 
        getResources().getDrawable(R.drawable.default_bg));
button.setBackground(stateList);

方案 2:禁用 View 的焦点能力

java

Button button = findViewById(R.id.button);
button.setFocusable(false);
button.setFocusableInTouchMode(false);

方案 3:覆盖主题中的默认焦点背景

java

// 在Activity的onCreate中动态应用主题属性
TypedValue outValue = new TypedValue();
getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);

// 创建不含focused状态的替代Drawable
StateListDrawable newDrawable = new StateListDrawable();
newDrawable.addState(new int[]{android.R.attr.state_pressed}, 
        getResources().getDrawable(R.drawable.pressed_bg));
newDrawable.addState(StateSet.WILD_CARD, 
        getResources().getDrawable(R.drawable.default_bg));

// 应用到当前View
button.setBackground(newDrawable);

方案 4:手动清除 focused 状态的 Drawable(反射实现)

java

public static void removeFocusedStateFromBackground(View view) {
    Drawable background = view.getBackground();
    if (background instanceof StateListDrawable) {
        StateListDrawable stateListDrawable = (StateListDrawable) background;
        
        try {
            // 获取StateListDrawable的私有字段mStateList
            Field stateListField = StateListDrawable.class.getDeclaredField("mStateList");
            stateListField.setAccessible(true);
            Object stateList = stateListField.get(stateListDrawable);
            
            // 获取mStateList的类型(通常是一个数组)
            Class<?> stateListClass = stateList.getClass();
            int length = Array.getLength(stateList);
            
            // 创建新的状态列表,过滤掉包含focused的状态
            List<Object> newStateList = new ArrayList<>();
            for (int i = 0; i < length; i++) {
                Object entry = Array.get(stateList, i);
                
                // 获取每个条目的状态集
                Field stateSetField = entry.getClass().getDeclaredField("mStateSet");
                stateSetField.setAccessible(true);
                int[] stateSet = (int[]) stateSetField.get(entry);
                
                // 检查是否包含focused状态
                boolean containsFocused = false;
                for (int state : stateSet) {
                    if (state == android.R.attr.state_focused) {
                        containsFocused = true;
                        break;
                    }
                }
                
                // 保留不包含focused的状态
                if (!containsFocused) {
                    newStateList.add(entry);
                }
            }
            
            // 将新的状态列表设置回StateListDrawable
            Object newStateListArray = Array.newInstance(stateListClass.getComponentType(), newStateList.size());
            for (int i = 0; i < newStateList.size(); i++) {
                Array.set(newStateListArray, i, newStateList.get(i));
            }
            stateListField.set(stateListDrawable, newStateListArray);
            
            // 触发Drawable重绘
            stateListDrawable.invalidateSelf();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 使用示例
Button button = findViewById(R.id.button);
removeFocusedStateFromBackground(button);

三、全局应用方案(自定义主题)

java

// styles.xml
<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <!-- 覆盖默认的可选择背景 -->
    <item name="android:selectableItemBackground">@drawable/no_focus_background</item>
    <item name="android:selectableItemBackgroundBorderless">@drawable/no_focus_background</item>
</style>

// no_focus_background.xml
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:drawable="@color/pressed_color"/>
    <item android:drawable="@color/default_color"/>
</selector>

四、关键源码总结

  1. 焦点状态触发流程
    setFocused(true) → refreshDrawableState() → bg.setState(drawableState) → 匹配focused状态的 Drawable

  2. 背景色来源
    主题默认属性android:selectableItemBackground,通常是包含focused状态的StateListDrawable

  3. 移除核心

    • 确保背景 Drawable 不包含focused状态定义
    • 或禁用 View 的焦点能力
    • 或通过反射修改现有 Drawable 的状态定义