| /* |
| * 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: |
| * |
| * <pre> |
| * <selector xmlns:android="http://schemas.android.com/apk/res/android"> |
| * <item android:state_focused="true" android:color="@color/testcolor1"/> |
| * <item android:state_pressed="true" android:state_enabled="false" android:color="@color/testcolor2" /> |
| * <item android:state_enabled="false" android:color="@color/testcolor3" /> |
| * <item android:color="@color/testcolor5"/> |
| * </selector> |
| * </pre> |
| * |
| * 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. The list of state specs will be processed in order of the items in the XML 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. Note that if you have such an item before any other items |
| * in the list then any subsequent items will end up being ignored. |
| * <p>For more information, see the guide to <a |
| * href="{@docRoot}guide/topics/resources/color-list-resource.html">Color State |
| * List Resource</a>.</p> |
| */ |
| 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<WeakReference<ColorStateList>> 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. |
| * <p> |
| * 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. |
| * <p> |
| * <strong>Note:</strong> 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. |
| * <p> |
| * 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<ComplexColor> getConstantState() { |
| if (mFactory == null) { |
| mFactory = new ColorStateListFactory(this); |
| } |
| return mFactory; |
| } |
| |
| private static class ColorStateListFactory extends ConstantState<ComplexColor> { |
| 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<ColorStateList> CREATOR = |
| new Parcelable.Creator<ColorStateList>() { |
| @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); |
| } |
| }; |
| } |