blob: 35ad5033e39502c866c01216fe5045c43d36aa88 [file] [log] [blame]
/*
* Copyright (C) 2016 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.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.pm.ActivityInfo.Config;
import android.content.res.Resources.Theme;
import com.android.internal.R;
import com.android.internal.util.GrowingArrayUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.graphics.LinearGradient;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.graphics.drawable.GradientDrawable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Lets you define a gradient color, which is used inside
* {@link android.graphics.drawable.VectorDrawable}.
*
* {@link android.content.res.GradientColor}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 "gradient" element with a number of attributes and elements inside. For example:
* <pre>
* &lt;gradient xmlns:android="http://schemas.android.com/apk/res/android"&gt;
* &lt;android:startColor="?android:attr/colorPrimary"/&gt;
* &lt;android:endColor="?android:attr/colorControlActivated"/&gt;
* &lt;.../&gt;
* &lt;android:type="linear"/&gt;
* &lt;/gradient&gt;
* </pre>
*
* This can describe either a {@link android.graphics.LinearGradient},
* {@link android.graphics.RadialGradient}, or {@link android.graphics.SweepGradient}.
*
* Note that different attributes are relevant for different types of gradient.
* For example, android:gradientRadius is only applied to RadialGradient.
* android:centerX and android:centerY are only applied to SweepGradient or RadialGradient.
* android:startX, android:startY, android:endX and android:endY are only applied to LinearGradient.
*
* Also note if any color "item" element is defined, then startColor, centerColor and endColor will
* be ignored.
* @hide
*/
public class GradientColor extends ComplexColor {
private static final String TAG = "GradientColor";
private static final boolean DBG_GRADIENT = false;
@IntDef(prefix = { "TILE_MODE_" }, value = {
TILE_MODE_CLAMP,
TILE_MODE_REPEAT,
TILE_MODE_MIRROR
})
@Retention(RetentionPolicy.SOURCE)
private @interface GradientTileMode {}
private static final int TILE_MODE_CLAMP = 0;
private static final int TILE_MODE_REPEAT = 1;
private static final int TILE_MODE_MIRROR = 2;
/** Lazily-created factory for this GradientColor. */
private GradientColorFactory mFactory;
private @Config int mChangingConfigurations;
private int mDefaultColor;
// After parsing all the attributes from XML, this shader is the ultimate result containing
// all the XML information.
private Shader mShader = null;
// Below are the attributes at the root element <gradient>.
// NOTE: they need to be copied in the copy constructor!
private int mGradientType = GradientDrawable.LINEAR_GRADIENT;
private float mCenterX = 0f;
private float mCenterY = 0f;
private float mStartX = 0f;
private float mStartY = 0f;
private float mEndX = 0f;
private float mEndY = 0f;
private int mStartColor = 0;
private int mCenterColor = 0;
private int mEndColor = 0;
private boolean mHasCenterColor = false;
private int mTileMode = 0; // Clamp mode.
private float mGradientRadius = 0f;
// Below are the attributes for the <item> element.
private int[] mItemColors;
private float[] mItemOffsets;
// Theme attributes for the root and item elements.
private int[] mThemeAttrs;
private int[][] mItemsThemeAttrs;
private GradientColor() {
}
private GradientColor(GradientColor copy) {
if (copy != null) {
mChangingConfigurations = copy.mChangingConfigurations;
mDefaultColor = copy.mDefaultColor;
mShader = copy.mShader;
mGradientType = copy.mGradientType;
mCenterX = copy.mCenterX;
mCenterY = copy.mCenterY;
mStartX = copy.mStartX;
mStartY = copy.mStartY;
mEndX = copy.mEndX;
mEndY = copy.mEndY;
mStartColor = copy.mStartColor;
mCenterColor = copy.mCenterColor;
mEndColor = copy.mEndColor;
mHasCenterColor = copy.mHasCenterColor;
mGradientRadius = copy.mGradientRadius;
mTileMode = copy.mTileMode;
if (copy.mItemColors != null) {
mItemColors = copy.mItemColors.clone();
}
if (copy.mItemOffsets != null) {
mItemOffsets = copy.mItemOffsets.clone();
}
if (copy.mThemeAttrs != null) {
mThemeAttrs = copy.mThemeAttrs.clone();
}
if (copy.mItemsThemeAttrs != null) {
mItemsThemeAttrs = copy.mItemsThemeAttrs.clone();
}
}
}
// Set the default to clamp mode.
private static Shader.TileMode parseTileMode(@GradientTileMode int tileMode) {
switch (tileMode) {
case TILE_MODE_CLAMP:
return Shader.TileMode.CLAMP;
case TILE_MODE_REPEAT:
return Shader.TileMode.REPEAT;
case TILE_MODE_MIRROR:
return Shader.TileMode.MIRROR;
default:
return Shader.TileMode.CLAMP;
}
}
/**
* Update the root level's attributes, either for inflate or applyTheme.
*/
private void updateRootElementState(TypedArray a) {
// Extract the theme attributes, if any.
mThemeAttrs = a.extractThemeAttrs();
mStartX = a.getFloat(
R.styleable.GradientColor_startX, mStartX);
mStartY = a.getFloat(
R.styleable.GradientColor_startY, mStartY);
mEndX = a.getFloat(
R.styleable.GradientColor_endX, mEndX);
mEndY = a.getFloat(
R.styleable.GradientColor_endY, mEndY);
mCenterX = a.getFloat(
R.styleable.GradientColor_centerX, mCenterX);
mCenterY = a.getFloat(
R.styleable.GradientColor_centerY, mCenterY);
mGradientType = a.getInt(
R.styleable.GradientColor_type, mGradientType);
mStartColor = a.getColor(
R.styleable.GradientColor_startColor, mStartColor);
mHasCenterColor |= a.hasValue(
R.styleable.GradientColor_centerColor);
mCenterColor = a.getColor(
R.styleable.GradientColor_centerColor, mCenterColor);
mEndColor = a.getColor(
R.styleable.GradientColor_endColor, mEndColor);
mTileMode = a.getInt(
R.styleable.GradientColor_tileMode, mTileMode);
if (DBG_GRADIENT) {
Log.v(TAG, "hasCenterColor is " + mHasCenterColor);
if (mHasCenterColor) {
Log.v(TAG, "centerColor:" + mCenterColor);
}
Log.v(TAG, "startColor: " + mStartColor);
Log.v(TAG, "endColor: " + mEndColor);
Log.v(TAG, "tileMode: " + mTileMode);
}
mGradientRadius = a.getFloat(R.styleable.GradientColor_gradientRadius,
mGradientRadius);
}
/**
* Check if the XML content is valid.
*
* @throws XmlPullParserException if errors were found.
*/
private void validateXmlContent() throws XmlPullParserException {
if (mGradientRadius <= 0
&& mGradientType == GradientDrawable.RADIAL_GRADIENT) {
throw new XmlPullParserException(
"<gradient> tag requires 'gradientRadius' "
+ "attribute with radial type");
}
}
/**
* The shader information will be applied to the native VectorDrawable's path.
* @hide
*/
public Shader getShader() {
return mShader;
}
/**
* A public method to create GradientColor from a XML resource.
*/
public static GradientColor createFromXml(Resources r, XmlResourceParser parser, 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 GradientColor from that tag.
*
* @return A new GradientColor for the current tag.
* @throws XmlPullParserException if the current tag is not &lt;gradient>
*/
@NonNull
static GradientColor createFromXmlInner(@NonNull Resources r,
@NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
final String name = parser.getName();
if (!name.equals("gradient")) {
throw new XmlPullParserException(
parser.getPositionDescription() + ": invalid gradient color tag " + name);
}
final GradientColor gradientColor = new GradientColor();
gradientColor.inflate(r, parser, attrs, theme);
return gradientColor;
}
/**
* Fill in this object based on the contents of an XML "gradient" element.
*/
private void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @Nullable Theme theme)
throws XmlPullParserException, IOException {
final TypedArray a = Resources.obtainAttributes(r, theme, attrs, R.styleable.GradientColor);
updateRootElementState(a);
mChangingConfigurations |= a.getChangingConfigurations();
a.recycle();
// Check correctness and throw exception if errors found.
validateXmlContent();
inflateChildElements(r, parser, attrs, theme);
onColorsChange();
}
/**
* Inflates child elements "item"s for each color stop.
*
* Note that at root level, we need to save ThemeAttrs for theme applied later.
* Here similarly, at each child item, we need to save the theme's attributes, and apply theme
* later as applyItemsAttrsTheme().
*/
private void inflateChildElements(@NonNull Resources r, @NonNull XmlPullParser parser,
@NonNull AttributeSet attrs, @NonNull Theme theme)
throws XmlPullParserException, IOException {
final int innerDepth = parser.getDepth() + 1;
int type;
int depth;
// Pre-allocate the array with some size, for better performance.
float[] offsetList = new float[20];
int[] colorList = new int[offsetList.length];
int[][] themeAttrsList = new int[offsetList.length][];
int listSize = 0;
boolean hasUnresolvedAttrs = false;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlPullParser.END_TAG)) {
if (type != XmlPullParser.START_TAG) {
continue;
}
if (depth > innerDepth || !parser.getName().equals("item")) {
continue;
}
final TypedArray a = Resources.obtainAttributes(r, theme, attrs,
R.styleable.GradientColorItem);
boolean hasColor = a.hasValue(R.styleable.GradientColorItem_color);
boolean hasOffset = a.hasValue(R.styleable.GradientColorItem_offset);
if (!hasColor || !hasOffset) {
throw new XmlPullParserException(
parser.getPositionDescription()
+ ": <item> tag requires a 'color' attribute and a 'offset' "
+ "attribute!");
}
final int[] themeAttrs = a.extractThemeAttrs();
int color = a.getColor(R.styleable.GradientColorItem_color, 0);
float offset = a.getFloat(R.styleable.GradientColorItem_offset, 0);
if (DBG_GRADIENT) {
Log.v(TAG, "new item color " + color + " " + Integer.toHexString(color));
Log.v(TAG, "offset" + offset);
}
mChangingConfigurations |= a.getChangingConfigurations();
a.recycle();
if (themeAttrs != null) {
hasUnresolvedAttrs = true;
}
colorList = GrowingArrayUtils.append(colorList, listSize, color);
offsetList = GrowingArrayUtils.append(offsetList, listSize, offset);
themeAttrsList = GrowingArrayUtils.append(themeAttrsList, listSize, themeAttrs);
listSize++;
}
if (listSize > 0) {
if (hasUnresolvedAttrs) {
mItemsThemeAttrs = new int[listSize][];
System.arraycopy(themeAttrsList, 0, mItemsThemeAttrs, 0, listSize);
} else {
mItemsThemeAttrs = null;
}
mItemColors = new int[listSize];
mItemOffsets = new float[listSize];
System.arraycopy(colorList, 0, mItemColors, 0, listSize);
System.arraycopy(offsetList, 0, mItemOffsets, 0, listSize);
}
}
/**
* Apply theme to all the items.
*/
private void applyItemsAttrsTheme(Theme t) {
if (mItemsThemeAttrs == null) {
return;
}
boolean hasUnresolvedAttrs = false;
final int[][] themeAttrsList = mItemsThemeAttrs;
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.GradientColorItem);
// 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;
}
mItemColors[i] = a.getColor(R.styleable.GradientColorItem_color, mItemColors[i]);
mItemOffsets[i] = a.getFloat(R.styleable.GradientColorItem_offset, mItemOffsets[i]);
if (DBG_GRADIENT) {
Log.v(TAG, "applyItemsAttrsTheme Colors[i] " + i + " " +
Integer.toHexString(mItemColors[i]));
Log.v(TAG, "Offsets[i] " + i + " " + mItemOffsets[i]);
}
// Account for any configuration changes.
mChangingConfigurations |= a.getChangingConfigurations();
a.recycle();
}
}
if (!hasUnresolvedAttrs) {
mItemsThemeAttrs = null;
}
}
private void onColorsChange() {
int[] tempColors = null;
float[] tempOffsets = null;
if (mItemColors != null) {
int length = mItemColors.length;
tempColors = new int[length];
tempOffsets = new float[length];
for (int i = 0; i < length; i++) {
tempColors[i] = mItemColors[i];
tempOffsets[i] = mItemOffsets[i];
}
} else {
if (mHasCenterColor) {
tempColors = new int[3];
tempColors[0] = mStartColor;
tempColors[1] = mCenterColor;
tempColors[2] = mEndColor;
tempOffsets = new float[3];
tempOffsets[0] = 0.0f;
// Since 0.5f is default value, try to take the one that isn't 0.5f
tempOffsets[1] = 0.5f;
tempOffsets[2] = 1f;
} else {
tempColors = new int[2];
tempColors[0] = mStartColor;
tempColors[1] = mEndColor;
}
}
if (tempColors.length < 2) {
Log.w(TAG, "<gradient> tag requires 2 color values specified!" + tempColors.length
+ " " + tempColors);
}
if (mGradientType == GradientDrawable.LINEAR_GRADIENT) {
mShader = new LinearGradient(mStartX, mStartY, mEndX, mEndY, tempColors, tempOffsets,
parseTileMode(mTileMode));
} else {
if (mGradientType == GradientDrawable.RADIAL_GRADIENT) {
mShader = new RadialGradient(mCenterX, mCenterY, mGradientRadius, tempColors,
tempOffsets, parseTileMode(mTileMode));
} else {
mShader = new SweepGradient(mCenterX, mCenterY, tempColors, tempOffsets);
}
}
mDefaultColor = tempColors[0];
}
/**
* For Gradient color, the default color is not very useful, since the gradient will override
* the color information anyway.
*/
@Override
@ColorInt
public int getDefaultColor() {
return mDefaultColor;
}
/**
* Similar to ColorStateList, setup constant state and its factory.
* @hide only for resource preloading
*/
@Override
public ConstantState<ComplexColor> getConstantState() {
if (mFactory == null) {
mFactory = new GradientColorFactory(this);
}
return mFactory;
}
private static class GradientColorFactory extends ConstantState<ComplexColor> {
private final GradientColor mSrc;
public GradientColorFactory(GradientColor src) {
mSrc = src;
}
@Override
public @Config int getChangingConfigurations() {
return mSrc.mChangingConfigurations;
}
@Override
public GradientColor newInstance() {
return mSrc;
}
@Override
public GradientColor newInstance(Resources res, Theme theme) {
return mSrc.obtainForTheme(theme);
}
}
/**
* Returns an appropriately themed gradient color.
*
* @param t the theme to apply
* @return a copy of the gradient color the theme applied, or the
* gradient itself if there were no unresolved theme
* attributes
* @hide only for resource preloading
*/
@Override
public GradientColor obtainForTheme(Theme t) {
if (t == null || !canApplyTheme()) {
return this;
}
final GradientColor clone = new GradientColor(this);
clone.applyTheme(t);
return clone;
}
/**
* Returns a mask of the configuration parameters for which this gradient
* 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 int getChangingConfigurations() {
return super.getChangingConfigurations() | mChangingConfigurations;
}
private void applyTheme(Theme t) {
if (mThemeAttrs != null) {
applyRootAttrsTheme(t);
}
if (mItemsThemeAttrs != null) {
applyItemsAttrsTheme(t);
}
onColorsChange();
}
private void applyRootAttrsTheme(Theme t) {
final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.GradientColor);
// mThemeAttrs will be set to null if if there are no theme attributes in the
// typed array.
mThemeAttrs = a.extractThemeAttrs(mThemeAttrs);
// merging the attributes update inside the updateRootElementState().
updateRootElementState(a);
// Account for any configuration changes.
mChangingConfigurations |= a.getChangingConfigurations();
a.recycle();
}
/**
* Returns whether a theme can be applied to this gradient color, which
* usually indicates that the gradient color has unresolved theme
* attributes.
*
* @return whether a theme can be applied to this gradient color.
* @hide only for resource preloading
*/
@Override
public boolean canApplyTheme() {
return mThemeAttrs != null || mItemsThemeAttrs != null;
}
}