ColorStateList 使用详解

1. 是什么?

ColorStateList(颜色状态列表)是一个能够定义在 XML 布局文件中,并最终根据 ColorStateList 应用的 View 的状态显示不一样颜色的对象。html

A ColorStateList is an object you can define in XML that you can apply as a color, but will actually change colors, depending on the state of the View object to which it is applied.android

最终效果以下:数组

界面中两按钮文字的颜色随着按钮的状态而改变。bash

2. 怎么用?

从 ColorStateList 的定义能够知道,建立 ColorStateList 的方式应该不止有一种。接下来,咱们就尝试从两方面建立 ColorStateList:app

  1. XML
  2. Java 代码

2.1 如何在 XML 中定义 ColorStateList

2.1.1 文件位置
res/color/filename.xml
复制代码
2.1.2 编译以后的数据类型
ColorStateList
复制代码
2.1.3 应用方式
  1. In Java: R.color.filename
  2. In XML: @[package:]color/filename
2.1.4 语法
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:color="hex_color"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>
复制代码
2.1.5 属性解析
属性 定义 取值范围
color 不一样状态的颜色值 十六进制的颜色值。
能够是以下格式:
#RGB
#ARGB
#RRGGBB
#AARRGGBB
state_pressed View 按下的状态 true,false。
true,按下;
false,默认状态,即没有按下以前的状态。
state_selected View 选中的状态 true,false。
true,选中;
false,未选中。

其余的属性相似,在此就不作赘述了。想要了解更多关于 state_xxx 的内容,请查看Color state list resourceide

2.1.6 示例
//1. text_color_state_list.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/green_700" android:state_pressed="true" />
    <item android:color="@color/grey_700" android:state_pressed="false" />
    <!--默认项-->
    <item android:color="@color/grey_700" />
</selector>
复制代码
//2. 在 XML 布局文件中应用 text_color_state_list
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/alphabet_a"
        android:layout_width="@dimen/avatar_size"
        android:layout_height="@dimen/padding_seventy_two"
        android:text="@string/alphabet_a"
        android:textColor="@color/text_color_state_list"
        android:textSize="@dimen/font_thirty_two" />
        
</LinearLayout>
复制代码

最终效果以下:布局

//3. 在 Java 代码中使用 text_color_state_list
public class MainActivity extends AppCompatActivity {

    private Button  mAlphaB;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView(){
        mAlphaB = findViewById(R.id.alphabet_b);

        Resources resources = getResources();
        ColorStateList colorStateList = resources.getColorStateList(R.color.text_color_state_list);
        mAlphaB.setTextColor(colorStateList);
    }

}
复制代码

在 Java 中使用在 XML 中定义的 ColorStateList 的效果与在 XML 中使用在 XML 中定义的 ColorStateList 的效果同样,因此就不赘述了。字体

2.1.7 注意事项
2.1.7.1 ColorStateList 中定义的默认 Item 必定要放在最下面

ColorStateList 中定义的默认 Item 必定要放在最下面,不然后面的 Item 将被忽略,Android Framework 在此处选择资源的时候,并非按照“最优选项”选择的,而是按照从上到下选择第一个匹配的。ui

Remember that the first item in the state list that matches the current state of the object will be applied. So if the first item in the list contains none of the state attributes above, then it will be applied every time, which is why your default value should always be last, as demonstrated in the following example.this

举个例子:

  1. 默认 Item 放在最下面:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/green_700" android:state_pressed="true" />
    <item android:color="@color/grey_700" android:state_pressed="false" />
    <!--默认项-->
    <item android:color="@color/grey_700" />
</selector>
复制代码

最终效果以下:

  1. 默认 Item 放在最上面:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!--默认项-->
    <item android:color="@color/grey_700" />
    <item android:color="@color/green_700" android:state_pressed="true" />
    <item android:color="@color/grey_700" android:state_pressed="false" />
</selector>
复制代码

最终效果以下:

由上面的运行效果可知:当默认的 Item 在最上面的时候,Button 的文字颜色并不会随着 Button 状态的改变而改变。所以在后面定义 ColorStateList 的时候,若是想要应用 ColorStateList 的 View 内容(字体或者其余)的颜色随着 View 的状态而改变,就须要把 ColorStateList 中默认的 Item 定义在最下面。

2.1.7.2 ColorStateList 是不能用于 View 的 Background
//1. View 部分源码  
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
    
    ...
    
    public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this(context);

        final TypedArray a = context.obtainStyledAttributes(
                attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
        
        ...
        
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case com.android.internal.R.styleable.View_background:
                    background = a.getDrawable(attr);
                    break;
                    
                    ...
                    
                }
            
            ...
            
        }
        
        ...
        
    }
    
    ...
    
}
复制代码

由 View 源码可知:View 的 Background 最终是经过 TypedArray 的 GetDrawable 方法获取的。

//2. TypedArray 部分源码  
public class TypedArray {
    
    ...
    
    /**
     * Retrieve the Drawable for the attribute at <var>index</var>.
     * <p>
     * This method will throw an exception if the attribute is defined but is
     * not a color or drawable resource.
     *
     * @param index Index of attribute to retrieve.
     *
     * @return Drawable for the attribute, or {@code null} if not defined.
     * @throws RuntimeException if the TypedArray has already been recycled.
     * @throws UnsupportedOperationException if the attribute is defined but is
     *         not a color or drawable resource.
     */
    @Nullable
    public Drawable getDrawable(@StyleableRes int index) {
        return getDrawableForDensity(index, 0);
    }
    
    ...
    
}
复制代码

由 TypedArray 源码可知,在 TypedArray 的 GetDrawable 中只能接收纯 Color 或者 Drawable Resource,而 ColorStateList 并未在此范围内,所以 ColorStateList 是不能用于 View 的 Background(若是在 View 的 Background 中引用 ColorStateList,应用程序将会 Crash)。

throws UnsupportedOperationException if the attribute is defined but is not a color or drawable resource.

2.1.7.2 StateListDrawable 是不能用于 TextView 系的 TextColor
//1. TextView 部分源码  
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
    ...
    
    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        
        ...
        
        readTextAppearance(context, a, attributes, true /* styleArray */);
        
        ...
    }
    
    ...
}
复制代码
//2. readTextAppearance 方法  
private void readTextAppearance(Context context, TypedArray appearance, TextAppearanceAttributes attributes, boolean styleArray) {
        
        ...
        
        for (int i = 0; i < n; i++) {
            
            ...
            
            switch (index) {
                case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
                    attributes.mTextColorHighlight = appearance.getColor(attr, attributes.mTextColorHighlight);
                    break;
                
                ...
                
            }
            
            ...
            
        }
        
        ...
        
}
复制代码

经过 TextView 源码可知,TextView 的 TextColor 最终是经过 TypedArray 的 GetColor 方法获取的。

//3. TypedArray 部分源码  
public class TypedArray {
    
    ...
    
    /**
     * Retrieve the color value for the attribute at <var>index</var>.  If
     * the attribute references a color resource holding a complex
     * {@link android.content.res.ColorStateList}, then the default color from
     * the set is returned.
     * <p>
     * This method will throw an exception if the attribute is defined but is
     * not an integer color or color state list.
     *
     * @param index Index of attribute to retrieve.
     * @param defValue Value to return if the attribute is not defined or
     *                 not a resource.
     *
     * @return Attribute color value, or defValue if not defined.
     * @throws RuntimeException if the TypedArray has already been recycled.
     * @throws UnsupportedOperationException if the attribute is defined but is
     *         not an integer color or color state list.
     */
    @ColorInt
    public int getColor(@StyleableRes int index, @ColorInt int defValue) {
        if (mRecycled) {
            throw new RuntimeException("Cannot make calls to a recycled instance!");
        }

        final int attrIndex = index;
        index *= STYLE_NUM_ENTRIES;

        final int[] data = mData;
        final int type = data[index + STYLE_TYPE];
        if (type == TypedValue.TYPE_NULL) {
            return defValue;
        } else if (type >= TypedValue.TYPE_FIRST_INT
                && type <= TypedValue.TYPE_LAST_INT) {
            return data[index + STYLE_DATA];
        } else if (type == TypedValue.TYPE_STRING) {
            final TypedValue value = mValue;
            if (getValueAt(index, value)) {
                final ColorStateList csl = mResources.loadColorStateList(
                        value, value.resourceId, mTheme);
                return csl.getDefaultColor();
            }
            return defValue;
        } else if (type == TypedValue.TYPE_ATTRIBUTE) {
            final TypedValue value = mValue;
            getValueAt(index, value);
            throw new UnsupportedOperationException(
                    "Failed to resolve attribute at index " + attrIndex + ": " + value);
        }

        throw new UnsupportedOperationException("Can't convert value at index " + attrIndex
                + " to color: type=0x" + Integer.toHexString(type));
    }
    
    ...
    
}
    
复制代码

由 TypedArray 源码可知,在 TypedArray 的 getColor 中只能接收纯 Color 或者 Color State List,而 StateListDrawable 并未在此范围内,所以 StateListDrawable 是不能用于 TextView 系的 TextColor(若是在 TextView 的 TextColor 中引用 StateListDrawable 程序将会出 Bug,可是不会 Crash)。

throws UnsupportedOperationException if the attribute is defined but is not an integer color or color state list.

2.2 如何在代码中定义 ColorStateList

2.2.1 ColorStateList 源码解析

ColorStateList 部分源码以下:

public class ColorStateList extends ComplexColor implements Parcelable {
    
    ...

    /**
     * Creates a ColorStateList that returns the specified mapping from
     * states to colors.
     */
    public ColorStateList(int[][] states, @ColorInt int[] colors) {
        mStateSpecs = states;
        mColors = colors;

        onColorsChanged();
    }

    ...

}
复制代码

由上面的源码可知,在建立 ColorStateList 的时候,须要传入两个数组,第一个数组是存储状态值的,第二个数组是存储状态对应颜色值的。
简单对比一下 XML 中定义 ColorStateList 的语法,其实很容易就明白为何在 ColorStateList 构造方法中存储状态值的数组是二维数组。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:color="hex_color"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>
复制代码

由于在每个 Item 中能够有不少个状态(state_xxx),每个 Item 中的全部这些状态只对应一个颜色值。也就是说,ColorStateList 构造方法中的存储状态的数组的第一层数组的 Size 只要和存储状态对应颜色值的数组的 Size 一致就行了。

举个例子(伪代码):

//状态值(states 第一层 size 为 2)
int[][] states = new int[2][];
states[0] = new int[] {android.R.attr.state_xxx};
states[1] = new int[] {};
//不一样状态对应的颜色值(colors size 为 2)
int[] colors = new int[] { R.color.pressed, R.color.normal};
ColorStateList colorList = new ColorStateList(states, colors);
复制代码
2.2.2 示例
public class MainActivity extends AppCompatActivity {

    private Button  mAlphaB;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView(){
    
        mAlphaB = findViewById(R.id.alphabet_b);
        ColorStateList colorStateList = createColorStateList(getResources().getColor(R.color.green_700), getResources().getColor(R.color.grey_700));
        mAlphaB.setTextColor(colorStateList);
        
    }

    private ColorStateList createColorStateList(int pressed, int normal) {
        //状态
        int[][] states = new int[2][];
        //按下
        states[0] = new int[] {android.R.attr.state_pressed};
        //默认
        states[1] = new int[] {};
        
        //状态对应颜色值(按下,默认)
        int[] colors = new int[] { pressed, normal};
        ColorStateList colorList = new ColorStateList(states, colors);
        return colorList;
    }

}
复制代码

最终效果以下:

2.2.3 自定义 ColorStateList

除了上面的方式以外,还能够继承 ColorStateList 实现自定义 ColorStateList,但因为 ColorStateList 可更改的属性太少,因此自定义 ColorStateList 并无什么意义。

简单示例:

public class CustomColorStateList extends ColorStateList {

    public CustomColorStateList(int[][] states, int[] colors) {
        super(states, colors);
    }

}
复制代码

具体使用方法同《2.2.2 示例》同样,因此再次不作赘述。

3. 工做原理

下面是在代码中使用在 XML 布局文件中建立的 ColorStateList 的方法:

//1. text_color_state_list.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/green_700" android:state_pressed="true" />
    <item android:color="@color/grey_700" android:state_pressed="false" />
    <!--默认项-->
    <item android:color="@color/grey_700" />
</selector>
复制代码
//2. 在 Java 代码中使用 text_color_state_list
public class MainActivity extends AppCompatActivity {

    private Button  mAlphaB;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView(){
        mAlphaB = findViewById(R.id.alphabet_b);

        Resources resources = getResources();
        ColorStateList colorStateList = resources.getColorStateList(R.color.text_color_state_list);
        mAlphaB.setTextColor(colorStateList);
    }

}
复制代码

既然是经过 Button 的 SetTextColor 方法将 ColorStateList 应用到 Button 的字体颜色上的,那接下来就进到 Button 的 SetTextColor 方法一看究竟。

//3. 进入 Button 的 setTextColor 方法  
public class TextView{
    
    ...
    
    @android.view.RemotableViewMethod
    public void setTextColor(ColorStateList colors) {
        if (colors == null) {
            throw new NullPointerException();
        }

        mTextColor = colors;
        updateTextColors();
    }
    
    ...
    
}
复制代码

由于 Button 继承至 TextView,Button 的 SetTextColor 方法继承至 TextView,且未作任何更改,所以直接进入了 TextView 类中。

在 TextView 类的 SetTextColor 方法中调用了 UpdateTextColors 方法。

//4. 进入 updateTextColors 方法  
public class TextView{
    
    ...
    
    private void updateTextColors() {
        boolean inval = false;
        final int[] drawableState = getDrawableState();
        int color = mTextColor.getColorForState(drawableState, 0);
        if (color != mCurTextColor) {
            mCurTextColor = color;
            inval = true;
        }
        if (mLinkTextColor != null) {
            color = mLinkTextColor.getColorForState(drawableState, 0);
            if (color != mTextPaint.linkColor) {
                mTextPaint.linkColor = color;
                inval = true;
            }
        }
        if (mHintTextColor != null) {
            color = mHintTextColor.getColorForState(drawableState, 0);
            if (color != mCurHintTextColor) {
                mCurHintTextColor = color;
                if (mText.length() == 0) {
                    inval = true;
                }
            }
        }
        if (inval) {
            // Text needs to be redrawn with the new color
            if (mEditor != null) mEditor.invalidateTextDisplayList();
            invalidate();
        }
    }
    
    ...
    
}
复制代码

接着看下在 TextView 类中,哪里都调用了 TextView 的 UpdateTextColors 方法。

最终找到了 TextView 的 DrawableStateChanged 方法,即在 TextView 的 DrawableStateChanged 方法中调用了 TextView 的 UpdateTextColors 方法。

//5. 进入 drawableStateChanged 方法  
public class TextView{
    
    ...
    
    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();

        if (mTextColor != null && mTextColor.isStateful()
                || (mHintTextColor != null && mHintTextColor.isStateful())
                || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
            updateTextColors();
        }

        if (mDrawables != null) {
            final int[] state = getDrawableState();
            for (Drawable dr : mDrawables.mShowing) {
                if (dr != null && dr.isStateful() && dr.setState(state)) {
                    invalidateDrawable(dr);
                }
            }
        }
    }
    
    ...
    
}
复制代码

在 TextView 类的 DrawableStateChanged 方法中调用了父类的 DrawableStateChanged 方法,进入 TextView 的父类(View)中看下哪里都调用了 DrawableStateChanged 方法。

最终找到了 View 的 RefreshDrawableState 方法,即在 View 的 RefreshDrawableState 方法中调用了 DrawableStateChanged 方法。

//6. 进入 refreshDrawableState 方法  
public class View{
    ...
    
    /**
     * Call this to force a view to update its drawable state. This will cause
     * drawableStateChanged to be called on this view. Views that are interested
     * in the new state should call getDrawableState.
     *
     * @see #drawableStateChanged
     * @see #getDrawableState
     */
    public void refreshDrawableState() {
        mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
        drawableStateChanged();

        ViewParent parent = mParent;
        if (parent != null) {
            parent.childDrawableStateChanged(this);
        }
    }
    
    ...
}
复制代码

在 View 类中看下哪里都调用了 RefreshDrawableState 方法。

在 View 类中,发现有多个方法都调用了 RefreshDrawableState 方法,如:

  • setEnabled(boolean enabled)
  • setPressed(boolean pressed)
  • onWindowFocusChanged(boolean hasWindowFocus)
  • setHovered(boolean hovered)
  • setSelected(boolean selected)
  • setActivated(boolean activated)

是否是有一种似曾相识的感受:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:color="hex_color"
        android:state_pressed=["true" | "false"]
        android:state_focused=["true" | "false"]
        android:state_selected=["true" | "false"]
        android:state_checkable=["true" | "false"]
        android:state_checked=["true" | "false"]
        android:state_enabled=["true" | "false"]
        android:state_window_focused=["true" | "false"] />
</selector>
复制代码

接下来,咱们随便挑一个方法来分析——SetPressed 方法。

//7. 进入 setPressed 方法  
public class View{
    ...
    
    /**
     * Sets the pressed state for this view.
     *
     * @see #isClickable()
     * @see #setClickable(boolean)
     *
     * @param pressed Pass true to set the View's internal state to "pressed", or false to reverts * the View's internal state from a previously set "pressed" state.
     */
    public void setPressed(boolean pressed) {
        final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);

        if (pressed) {
            mPrivateFlags |= PFLAG_PRESSED;
        } else {
            mPrivateFlags &= ~PFLAG_PRESSED;
        }

        if (needsRefresh) {
            refreshDrawableState();
        }
        dispatchSetPressed(pressed);
    }
    
    ...
}
复制代码

接下来看下,在 View 类中哪里都调用了 SetPressed 方法。

在 View 类中,发现有多个方法都调用了 SetPressed 方法,如:

  • removeUnsetPressCallback
  • onFocusChanged
  • resetPressedState
  • dispatchGenericMotionEventInternal
  • onKeyDown
  • onKeyUp
  • onTouchEvent

在上面的这些方法中,有一个方法引发了咱们注意——onTouchEvent 处理触屏事件的方法。

Implement this method to handle touch screen motion events.

public class View{

    ...
    
    public boolean onTouchEvent(MotionEvent event) {
        
        ...

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                
                case MotionEvent.ACTION_UP:
                    ...
                    break;
                
                case MotionEvent.ACTION_DOWN:
                    
                    ...
                    
                    if (isInScrollingContainer) {
                        
                        ...
                        
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        ///////////////////////////////////////////////////////////////////
                        //                                                               //
                        //                       只看这里就好啦                          //
                        //                                                               //
                        ///////////////////////////////////////////////////////////////////
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;
                    
                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;

                case MotionEvent.ACTION_MOVE:
                    ...
                    break;
                    
            }

            return true;
        }

        return false;
    }
    
    ...
    
}
复制代码

到这里,咱们不难发现最终 ColorStateList 是如何起做用的:

在 View 的 OnTouchEvent 中根据用户操做肯定当前 View 的状态,选择与该状态对应的颜色值并将其设置到 View 的 Paint上,进而在刷新界面的时候应用新的颜色。在 TextView 系控件中表现为:根据 TextView 系控件的状态将与该状态对应的颜色值设置到当前控件的 TextPaint 上,进而在刷新界面的时候应用新的颜色。

4. ColorStateList 与 StateListDrawable 之间的关系

ColorStateList 与 StateListDrawable 其实并无什么关系。

ColorStateList 继承至 Object,而 StateListDrawable 间接继承至 Drawable。

若是非要从它们两个中间找到共同点,那就是它们都能根据当前 View 的状态改变本身的显示内容(ColorStateList 根据 View 状态显示不一样的 Color,StateListDrawable 根据 View 状态显示不一样的 Drawable)。

5. 参考文献

  1. Color State List Resource
  2. ColorStateList
相关文章
相关标签/搜索