blob: dbf7d45b65d3deed4d49791722d478d4d227f42e [file] [log] [blame]
/*
* Copyright (C) 2018 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 androidx.core.content.res;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.util.StateSet;
import android.util.TypedValue;
import android.util.Xml;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.XmlRes;
import androidx.core.R;
import androidx.core.math.MathUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final class ColorStateListInflaterCompat {
private static final ThreadLocal<TypedValue> sTempTypedValue = new ThreadLocal<>();
private ColorStateListInflaterCompat() {
}
/**
* Creates a ColorStateList from an XML document using given a set of
* {@link Resources} and a {@link Resources.Theme}.
*
* @param resources Resources against which the ColorStateList should be inflated.
* @param resId the resource identifier of the ColorStateList to retrieve.
* @param theme Optional theme to apply to the color, may be {@code null}.
* @return A new color state list.
*/
@Nullable
public static ColorStateList inflate(@NonNull Resources resources, @XmlRes int resId,
@Nullable Resources.Theme theme) {
try {
XmlPullParser parser = resources.getXml(resId);
return createFromXml(resources, parser, theme);
} catch (Exception e) {
Log.e("CSLCompat", "Failed to inflate ColorStateList.", e);
}
return null;
}
/**
* Creates a ColorStateList from an XML document using given a set of
* {@link Resources} and a {@link android.content.res.Resources.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 Resources.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.
*
* @return A new color state list for the current tag.
* @throws XmlPullParserException if the current tag is not &lt;selector>
*/
@NonNull
public static ColorStateList createFromXmlInner(@NonNull Resources r,
@NonNull XmlPullParser parser, @NonNull AttributeSet attrs,
@Nullable Resources.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);
}
return inflate(r, parser, attrs, theme);
}
/**
* Fill in this object based on the contents of an XML "selector" element.
*/
private static ColorStateList inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Resources.Theme theme)
throws XmlPullParserException, IOException {
final int innerDepth = parser.getDepth() + 1;
int depth;
int type;
int[][] stateSpecList = new int[20][];
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 = obtainAttributes(r, theme, attrs, R.styleable.ColorStateListItem);
int resourceId = a.getResourceId(R.styleable.ColorStateListItem_android_color, -1);
int baseColor;
if (resourceId != -1 && !isColorInt(r, resourceId)) {
try {
baseColor = createFromXml(r, r.getXml(resourceId), theme).getDefaultColor();
} catch (Exception e) {
baseColor = a.getColor(R.styleable.ColorStateListItem_android_color,
Color.MAGENTA);
}
} else {
baseColor = a.getColor(R.styleable.ColorStateListItem_android_color, Color.MAGENTA);
}
float alphaMod = 1.0f;
if (a.hasValue(R.styleable.ColorStateListItem_android_alpha)) {
alphaMod = a.getFloat(R.styleable.ColorStateListItem_android_alpha, alphaMod);
} else if (a.hasValue(R.styleable.ColorStateListItem_alpha)) {
alphaMod = a.getFloat(R.styleable.ColorStateListItem_alpha, alphaMod);
}
final float lStar;
if (Build.VERSION.SDK_INT >= 31
&& a.hasValue(R.styleable.ColorStateListItem_android_lStar)) {
lStar = a.getFloat(R.styleable.ColorStateListItem_android_lStar, -1.0f);
} else {
lStar = a.getFloat(R.styleable.ColorStateListItem_lStar, -1.0f);
}
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);
if (stateResId != android.R.attr.color
&& stateResId != android.R.attr.alpha
&& stateResId != R.attr.alpha
&& stateResId != R.attr.lStar) {
// Unrecognized attribute, add to state set
stateSpec[j++] = attrs.getAttributeBooleanValue(i, false)
? stateResId : -stateResId;
}
}
stateSpec = StateSet.trimStateSet(stateSpec, j);
// Apply alpha and luminance 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, lStar);
colorList = GrowingArrayUtils.append(colorList, listSize, color);
stateSpecList = GrowingArrayUtils.append(stateSpecList, listSize, stateSpec);
listSize++;
}
int[] colors = new int[listSize];
int[][] stateSpecs = new int[listSize][];
System.arraycopy(colorList, 0, colors, 0, listSize);
System.arraycopy(stateSpecList, 0, stateSpecs, 0, listSize);
return new ColorStateList(stateSpecs, colors);
}
private static boolean isColorInt(@NonNull Resources r, @ColorRes int resId) {
final TypedValue value = getTypedValue();
r.getValue(resId, value, true);
return value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT;
}
@NonNull
private static TypedValue getTypedValue() {
TypedValue tv = sTempTypedValue.get();
if (tv == null) {
tv = new TypedValue();
sTempTypedValue.set(tv);
}
return tv;
}
private static TypedArray obtainAttributes(Resources res, Resources.Theme theme,
AttributeSet set, int[] attrs) {
return theme == null ? res.obtainAttributes(set, attrs)
: theme.obtainStyledAttributes(set, attrs, 0, 0);
}
@ColorInt
private static int modulateColorAlpha(@ColorInt int color,
@FloatRange(from = 0f, to = 1f) float alphaMod,
@FloatRange(from = 0f, to = 100f) float lStar) {
final boolean validLStar = lStar >= 0.0f && lStar <= 100.0f;
if (alphaMod == 1.0f && !validLStar) {
return color;
}
final int baseAlpha = Color.alpha(color);
final int alpha = MathUtils.clamp((int) (baseAlpha * alphaMod + 0.5f), 0, 255);
if (validLStar) {
final CamColor baseCam = CamColor.fromColor(color);
color = CamColor.toColor(baseCam.getHue(), baseCam.getChroma(), lStar);
}
return (color & 0xFFFFFF) | (alpha << 24);
}
}