/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.content.res; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.pm.ActivityInfo.Config; import android.content.res.Resources.Theme; import android.graphics.Color; import com.android.internal.R; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.util.AttributeSet; import android.util.Log; import android.util.MathUtils; import android.util.SparseArray; import android.util.StateSet; import android.util.Xml; import android.os.Parcel; import android.os.Parcelable; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Arrays; /** * * Lets you map {@link android.view.View} state sets to colors. *

* {@link android.content.res.ColorStateList}s are created from XML resource files defined in the * "color" subdirectory directory of an application's resource directory. The XML file contains * a single "selector" element with a number of "item" elements inside. For example: *

 * <selector xmlns:android="http://schemas.android.com/apk/res/android">
 *   <item android:state_focused="true"
 *           android:color="@color/sample_focused" />
 *   <item android:state_pressed="true"
 *           android:state_enabled="false"
 *           android:color="@color/sample_disabled_pressed" />
 *   <item android:state_enabled="false"
 *           android:color="@color/sample_disabled_not_pressed" />
 *   <item android:color="@color/sample_default" />
 * </selector>
 * 
* * This defines a set of state spec / color pairs where each state spec specifies a set of * states that a view must either be in or not be in and the color specifies the color associated * with that spec. * * *

State specs

*

* Each item defines a set of state spec and color pairs, where the state spec is a series of * attributes set to either {@code true} or {@code false} to represent inclusion or exclusion. If * an attribute is not specified for an item, it may be any value. *

* For example, the following item will be matched whenever the focused state is set; any other * states may be set or unset: *

 * <item android:state_focused="true"
 *         android:color="@color/sample_focused" />
 * 
*

* Typically, a color state list will reference framework-defined state attributes such as * {@link android.R.attr#state_focused android:state_focused} or * {@link android.R.attr#state_enabled android:state_enabled}; however, app-defined attributes may * also be used. *

* Note: The list of state specs will be matched against in the order that they * appear in the XML file. For this reason, more-specific items should be placed earlier in the * file. An item with no state spec is considered to match any set of states and is generally * useful as a final item to be used as a default. *

* If an item with no state spec if placed before other items, those items * will be ignored. * * *

Item attributes

*

* Each item must define an {@link android.R.attr#color android:color} attribute, which may be * an HTML-style hex color, a reference to a color resource, or -- in API 23 and above -- a theme * attribute that resolves to a color. *

* Starting with API 23, items may optionally define an {@link android.R.attr#alpha android:alpha} * attribute to modify the base color's opacity. This attribute takes a either floating-point value * between 0 and 1 or a theme attribute that resolves as such. The item's overall color is * calculated by multiplying by the base color's alpha channel by the {@code alpha} value. For * example, the following item represents the theme's accent color at 50% opacity: *

 * <item android:state_enabled="false"
 *         android:color="?android:attr/colorAccent"
 *         android:alpha="0.5" />
 * 
* * *

Developer guide

*

* For more information, see the guide to * Color State * List Resource. * * @attr ref android.R.styleable#ColorStateListItem_alpha * @attr ref android.R.styleable#ColorStateListItem_color */ public class ColorStateList extends ComplexColor implements Parcelable { private static final String TAG = "ColorStateList"; private static final int DEFAULT_COLOR = Color.RED; private static final int[][] EMPTY = new int[][] { new int[0] }; /** Thread-safe cache of single-color ColorStateLists. */ private static final SparseArray> sCache = new SparseArray<>(); /** Lazily-created factory for this color state list. */ private ColorStateListFactory mFactory; private int[][] mThemeAttrs; private @Config int mChangingConfigurations; private int[][] mStateSpecs; private int[] mColors; private int mDefaultColor; private boolean mIsOpaque; private ColorStateList() { // Not publicly instantiable. } /** * Creates a ColorStateList that returns the specified mapping from * states to colors. */ public ColorStateList(int[][] states, @ColorInt int[] colors) { mStateSpecs = states; mColors = colors; onColorsChanged(); } /** * @return A ColorStateList containing a single color. */ @NonNull public static ColorStateList valueOf(@ColorInt int color) { synchronized (sCache) { final int index = sCache.indexOfKey(color); if (index >= 0) { final ColorStateList cached = sCache.valueAt(index).get(); if (cached != null) { return cached; } // Prune missing entry. sCache.removeAt(index); } // Prune the cache before adding new items. final int N = sCache.size(); for (int i = N - 1; i >= 0; i--) { if (sCache.valueAt(i).get() == null) { sCache.removeAt(i); } } final ColorStateList csl = new ColorStateList(EMPTY, new int[] { color }); sCache.put(color, new WeakReference<>(csl)); return csl; } } /** * Creates a ColorStateList with the same properties as another * ColorStateList. *

* The properties of the new ColorStateList can be modified without * affecting the source ColorStateList. * * @param orig the source color state list */ private ColorStateList(ColorStateList orig) { if (orig != null) { mChangingConfigurations = orig.mChangingConfigurations; mStateSpecs = orig.mStateSpecs; mDefaultColor = orig.mDefaultColor; mIsOpaque = orig.mIsOpaque; // Deep copy, these may change due to applyTheme(). mThemeAttrs = orig.mThemeAttrs.clone(); mColors = orig.mColors.clone(); } } /** * Creates a ColorStateList from an XML document. * * @param r Resources against which the ColorStateList should be inflated. * @param parser Parser for the XML document defining the ColorStateList. * @return A new color state list. * * @deprecated Use #createFromXml(Resources, XmlPullParser parser, Theme) */ @NonNull @Deprecated public static ColorStateList createFromXml(Resources r, XmlPullParser parser) throws XmlPullParserException, IOException { return createFromXml(r, parser, null); } /** * Creates a ColorStateList from an XML document using given a set of * {@link Resources} and a {@link Theme}. * * @param r Resources against which the ColorStateList should be inflated. * @param parser Parser for the XML document defining the ColorStateList. * @param theme Optional theme to apply to the color state list, may be * {@code null}. * @return A new color state list. */ @NonNull public static ColorStateList createFromXml(@NonNull Resources r, @NonNull XmlPullParser parser, @Nullable Theme theme) throws XmlPullParserException, IOException { final AttributeSet attrs = Xml.asAttributeSet(parser); int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Seek parser to start tag. } if (type != XmlPullParser.START_TAG) { throw new XmlPullParserException("No start tag found"); } return createFromXmlInner(r, parser, attrs, theme); } /** * Create from inside an XML document. Called on a parser positioned at a * tag in an XML document, tries to create a ColorStateList from that tag. * * @throws XmlPullParserException if the current tag is not <selector> * @return A new color state list for the current tag. */ @NonNull static ColorStateList createFromXmlInner(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException { final String name = parser.getName(); if (!name.equals("selector")) { throw new XmlPullParserException( parser.getPositionDescription() + ": invalid color state list tag " + name); } final ColorStateList colorStateList = new ColorStateList(); colorStateList.inflate(r, parser, attrs, theme); return colorStateList; } /** * Creates a new ColorStateList that has the same states and colors as this * one but where each color has the specified alpha value (0-255). * * @param alpha The new alpha channel value (0-255). * @return A new color state list. */ @NonNull public ColorStateList withAlpha(int alpha) { final int[] colors = new int[mColors.length]; final int len = colors.length; for (int i = 0; i < len; i++) { colors[i] = (mColors[i] & 0xFFFFFF) | (alpha << 24); } return new ColorStateList(mStateSpecs, colors); } /** * Fill in this object based on the contents of an XML "selector" element. */ private void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException { final int innerDepth = parser.getDepth()+1; int depth; int type; @Config int changingConfigurations = 0; int defaultColor = DEFAULT_COLOR; boolean hasUnresolvedAttrs = false; int[][] stateSpecList = ArrayUtils.newUnpaddedArray(int[].class, 20); int[][] themeAttrsList = new int[stateSpecList.length][]; int[] colorList = new int[stateSpecList.length]; int listSize = 0; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { if (type != XmlPullParser.START_TAG || depth > innerDepth || !parser.getName().equals("item")) { continue; } final TypedArray a = Resources.obtainAttributes(r, theme, attrs, R.styleable.ColorStateListItem); final int[] themeAttrs = a.extractThemeAttrs(); final int baseColor = a.getColor(R.styleable.ColorStateListItem_color, Color.MAGENTA); final float alphaMod = a.getFloat(R.styleable.ColorStateListItem_alpha, 1.0f); changingConfigurations |= a.getChangingConfigurations(); a.recycle(); // Parse all unrecognized attributes as state specifiers. int j = 0; final int numAttrs = attrs.getAttributeCount(); int[] stateSpec = new int[numAttrs]; for (int i = 0; i < numAttrs; i++) { final int stateResId = attrs.getAttributeNameResource(i); switch (stateResId) { case R.attr.color: case R.attr.alpha: // Recognized attribute, ignore. break; default: stateSpec[j++] = attrs.getAttributeBooleanValue(i, false) ? stateResId : -stateResId; } } stateSpec = StateSet.trimStateSet(stateSpec, j); // Apply alpha modulation. If we couldn't resolve the color or // alpha yet, the default values leave us enough information to // modulate again during applyTheme(). final int color = modulateColorAlpha(baseColor, alphaMod); if (listSize == 0 || stateSpec.length == 0) { defaultColor = color; } if (themeAttrs != null) { hasUnresolvedAttrs = true; } colorList = GrowingArrayUtils.append(colorList, listSize, color); themeAttrsList = GrowingArrayUtils.append(themeAttrsList, listSize, themeAttrs); stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec); listSize++; } mChangingConfigurations = changingConfigurations; mDefaultColor = defaultColor; if (hasUnresolvedAttrs) { mThemeAttrs = new int[listSize][]; System.arraycopy(themeAttrsList, 0, mThemeAttrs, 0, listSize); } else { mThemeAttrs = null; } mColors = new int[listSize]; mStateSpecs = new int[listSize][]; System.arraycopy(colorList, 0, mColors, 0, listSize); System.arraycopy(stateSpecList, 0, mStateSpecs, 0, listSize); onColorsChanged(); } /** * Returns whether a theme can be applied to this color state list, which * usually indicates that the color state list has unresolved theme * attributes. * * @return whether a theme can be applied to this color state list * @hide only for resource preloading */ @Override public boolean canApplyTheme() { return mThemeAttrs != null; } /** * Applies a theme to this color state list. *

* Note: Applying a theme may affect the changing * configuration parameters of this color state list. After calling this * method, any dependent configurations must be updated by obtaining the * new configuration mask from {@link #getChangingConfigurations()}. * * @param t the theme to apply */ private void applyTheme(Theme t) { if (mThemeAttrs == null) { return; } boolean hasUnresolvedAttrs = false; final int[][] themeAttrsList = mThemeAttrs; final int N = themeAttrsList.length; for (int i = 0; i < N; i++) { if (themeAttrsList[i] != null) { final TypedArray a = t.resolveAttributes(themeAttrsList[i], R.styleable.ColorStateListItem); final float defaultAlphaMod; if (themeAttrsList[i][R.styleable.ColorStateListItem_color] != 0) { // If the base color hasn't been resolved yet, the current // color's alpha channel is either full-opacity (if we // haven't resolved the alpha modulation yet) or // pre-modulated. Either is okay as a default value. defaultAlphaMod = Color.alpha(mColors[i]) / 255.0f; } else { // Otherwise, the only correct default value is 1. Even if // nothing is resolved during this call, we can apply this // multiple times without losing of information. defaultAlphaMod = 1.0f; } // Extract the theme attributes, if any, before attempting to // read from the typed array. This prevents a crash if we have // unresolved attrs. themeAttrsList[i] = a.extractThemeAttrs(themeAttrsList[i]); if (themeAttrsList[i] != null) { hasUnresolvedAttrs = true; } final int baseColor = a.getColor( R.styleable.ColorStateListItem_color, mColors[i]); final float alphaMod = a.getFloat( R.styleable.ColorStateListItem_alpha, defaultAlphaMod); mColors[i] = modulateColorAlpha(baseColor, alphaMod); // Account for any configuration changes. mChangingConfigurations |= a.getChangingConfigurations(); a.recycle(); } } if (!hasUnresolvedAttrs) { mThemeAttrs = null; } onColorsChanged(); } /** * Returns an appropriately themed color state list. * * @param t the theme to apply * @return a copy of the color state list with the theme applied, or the * color state list itself if there were no unresolved theme * attributes * @hide only for resource preloading */ @Override public ColorStateList obtainForTheme(Theme t) { if (t == null || !canApplyTheme()) { return this; } final ColorStateList clone = new ColorStateList(this); clone.applyTheme(t); return clone; } /** * Returns a mask of the configuration parameters for which this color * state list may change, requiring that it be re-created. * * @return a mask of the changing configuration parameters, as defined by * {@link android.content.pm.ActivityInfo} * * @see android.content.pm.ActivityInfo */ public @Config int getChangingConfigurations() { return super.getChangingConfigurations() | mChangingConfigurations; } private int modulateColorAlpha(int baseColor, float alphaMod) { if (alphaMod == 1.0f) { return baseColor; } final int baseAlpha = Color.alpha(baseColor); final int alpha = MathUtils.constrain((int) (baseAlpha * alphaMod + 0.5f), 0, 255); return (baseColor & 0xFFFFFF) | (alpha << 24); } /** * Indicates whether this color state list contains more than one state spec * and will change color based on state. * * @return True if this color state list changes color based on state, false * otherwise. * @see #getColorForState(int[], int) */ @Override public boolean isStateful() { return mStateSpecs.length > 1; } /** * Indicates whether this color state list is opaque, which means that every * color returned from {@link #getColorForState(int[], int)} has an alpha * value of 255. * * @return True if this color state list is opaque. */ public boolean isOpaque() { return mIsOpaque; } /** * Return the color associated with the given set of * {@link android.view.View} states. * * @param stateSet an array of {@link android.view.View} states * @param defaultColor the color to return if there's no matching state * spec in this {@link ColorStateList} that matches the * stateSet. * * @return the color associated with that set of states in this {@link ColorStateList}. */ public int getColorForState(@Nullable int[] stateSet, int defaultColor) { final int setLength = mStateSpecs.length; for (int i = 0; i < setLength; i++) { final int[] stateSpec = mStateSpecs[i]; if (StateSet.stateSetMatches(stateSpec, stateSet)) { return mColors[i]; } } return defaultColor; } /** * Return the default color in this {@link ColorStateList}. * * @return the default color in this {@link ColorStateList}. */ @ColorInt public int getDefaultColor() { return mDefaultColor; } /** * Return the states in this {@link ColorStateList}. The returned array * should not be modified. * * @return the states in this {@link ColorStateList} * @hide */ public int[][] getStates() { return mStateSpecs; } /** * Return the colors in this {@link ColorStateList}. The returned array * should not be modified. * * @return the colors in this {@link ColorStateList} * @hide */ public int[] getColors() { return mColors; } /** * Returns whether the specified state is referenced in any of the state * specs contained within this ColorStateList. *

* Any reference, either positive or negative {ex. ~R.attr.state_enabled}, * will cause this method to return {@code true}. Wildcards are not counted * as references. * * @param state the state to search for * @return {@code true} if the state if referenced, {@code false} otherwise * @hide Use only as directed. For internal use only. */ public boolean hasState(int state) { final int[][] stateSpecs = mStateSpecs; final int specCount = stateSpecs.length; for (int specIndex = 0; specIndex < specCount; specIndex++) { final int[] states = stateSpecs[specIndex]; final int stateCount = states.length; for (int stateIndex = 0; stateIndex < stateCount; stateIndex++) { if (states[stateIndex] == state || states[stateIndex] == ~state) { return true; } } } return false; } @Override public String toString() { return "ColorStateList{" + "mThemeAttrs=" + Arrays.deepToString(mThemeAttrs) + "mChangingConfigurations=" + mChangingConfigurations + "mStateSpecs=" + Arrays.deepToString(mStateSpecs) + "mColors=" + Arrays.toString(mColors) + "mDefaultColor=" + mDefaultColor + '}'; } /** * Updates the default color and opacity. */ private void onColorsChanged() { int defaultColor = DEFAULT_COLOR; boolean isOpaque = true; final int[][] states = mStateSpecs; final int[] colors = mColors; final int N = states.length; if (N > 0) { defaultColor = colors[0]; for (int i = N - 1; i > 0; i--) { if (states[i].length == 0) { defaultColor = colors[i]; break; } } for (int i = 0; i < N; i++) { if (Color.alpha(colors[i]) != 0xFF) { isOpaque = false; break; } } } mDefaultColor = defaultColor; mIsOpaque = isOpaque; } /** * @return a factory that can create new instances of this ColorStateList * @hide only for resource preloading */ public ConstantState getConstantState() { if (mFactory == null) { mFactory = new ColorStateListFactory(this); } return mFactory; } private static class ColorStateListFactory extends ConstantState { private final ColorStateList mSrc; public ColorStateListFactory(ColorStateList src) { mSrc = src; } @Override public @Config int getChangingConfigurations() { return mSrc.mChangingConfigurations; } @Override public ColorStateList newInstance() { return mSrc; } @Override public ColorStateList newInstance(Resources res, Theme theme) { return (ColorStateList) mSrc.obtainForTheme(theme); } } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { if (canApplyTheme()) { Log.w(TAG, "Wrote partially-resolved ColorStateList to parcel!"); } final int N = mStateSpecs.length; dest.writeInt(N); for (int i = 0; i < N; i++) { dest.writeIntArray(mStateSpecs[i]); } dest.writeIntArray(mColors); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public ColorStateList[] newArray(int size) { return new ColorStateList[size]; } @Override public ColorStateList createFromParcel(Parcel source) { final int N = source.readInt(); final int[][] stateSpecs = new int[N][]; for (int i = 0; i < N; i++) { stateSpecs[i] = source.createIntArray(); } final int[] colors = source.createIntArray(); return new ColorStateList(stateSpecs, colors); } }; }