ColorStateList 使用詳解

1. 是什麼?

ColorStateList(顏色狀態列表)是一個可以定義在 XML 佈局文件中,並最終根據 ColorStateList 應用的 View 的狀態顯示不同顏色的對象。

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.

最終效果如下:

界面中兩按鈕文字的顏色隨着按鈕的狀態而改變。

2. 怎麼用?

從 ColorStateList 的定義可以知道,創建 ColorStateList 的方式應該不止有一種。接下來,我們就嘗試從兩方面創建 ColorStateList:

  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 resource

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 在此處選擇資源的時候,並不是按照“最優選項”選擇的,而是按照從上到下選擇第一個匹配的。

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.

舉個例子:

  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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章