/* * 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. * * *
* 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. * * *
* 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" /> ** * *
* 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
* 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