| /* |
| * 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> |
| * <gradient xmlns:android="http://schemas.android.com/apk/res/android"> |
| * <android:startColor="?android:attr/colorPrimary"/> |
| * <android:endColor="?android:attr/colorControlActivated"/> |
| * <.../> |
| * <android:type="linear"/> |
| * </gradient> |
| * </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. |
| * androd: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. |
| */ |
| public class GradientColor extends ComplexColor { |
| private static final String TAG = "GradientColor"; |
| |
| private static final boolean DBG_GRADIENT = false; |
| |
| @IntDef({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 <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; |
| } |
| |
| } |