1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.content.res;
18 
19 import android.annotation.ColorInt;
20 import android.annotation.IntDef;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.pm.ActivityInfo.Config;
24 import android.content.res.Resources.Theme;
25 
26 import com.android.internal.R;
27 import com.android.internal.util.GrowingArrayUtils;
28 
29 import org.xmlpull.v1.XmlPullParser;
30 import org.xmlpull.v1.XmlPullParserException;
31 
32 import android.graphics.LinearGradient;
33 import android.graphics.RadialGradient;
34 import android.graphics.Shader;
35 import android.graphics.SweepGradient;
36 import android.graphics.drawable.GradientDrawable;
37 import android.util.AttributeSet;
38 import android.util.Log;
39 import android.util.Xml;
40 
41 import java.io.IOException;
42 import java.lang.annotation.Retention;
43 import java.lang.annotation.RetentionPolicy;
44 
45 /**
46  * Lets you define a gradient color, which is used inside
47  * {@link android.graphics.drawable.VectorDrawable}.
48  *
49  * {@link android.content.res.GradientColor}s are created from XML resource files defined in the
50  * "color" subdirectory directory of an application's resource directory.  The XML file contains
51  * a single "gradient" element with a number of attributes and elements inside.  For example:
52  * <pre>
53  * &lt;gradient xmlns:android="http://schemas.android.com/apk/res/android"&gt;
54  *   &lt;android:startColor="?android:attr/colorPrimary"/&gt;
55  *   &lt;android:endColor="?android:attr/colorControlActivated"/&gt;
56  *   &lt;.../&gt;
57  *   &lt;android:type="linear"/&gt;
58  * &lt;/gradient&gt;
59  * </pre>
60  *
61  * This can describe either a {@link android.graphics.LinearGradient},
62  * {@link android.graphics.RadialGradient}, or {@link android.graphics.SweepGradient}.
63  *
64  * Note that different attributes are relevant for different types of gradient.
65  * For example, android:gradientRadius is only applied to RadialGradient.
66  * android:centerX and android:centerY are only applied to SweepGradient or RadialGradient.
67  * android:startX, android:startY, android:endX and android:endY are only applied to LinearGradient.
68  *
69  * Also note if any color "item" element is defined, then startColor, centerColor and endColor will
70  * be ignored.
71  * @hide
72  */
73 public class GradientColor extends ComplexColor {
74     private static final String TAG = "GradientColor";
75 
76     private static final boolean DBG_GRADIENT = false;
77 
78     @IntDef(prefix = { "TILE_MODE_" }, value = {
79             TILE_MODE_CLAMP,
80             TILE_MODE_REPEAT,
81             TILE_MODE_MIRROR
82     })
83     @Retention(RetentionPolicy.SOURCE)
84     private @interface GradientTileMode {}
85 
86     private static final int TILE_MODE_CLAMP = 0;
87     private static final int TILE_MODE_REPEAT = 1;
88     private static final int TILE_MODE_MIRROR = 2;
89 
90     /** Lazily-created factory for this GradientColor. */
91     private GradientColorFactory mFactory;
92 
93     private @Config int mChangingConfigurations;
94     private int mDefaultColor;
95 
96     // After parsing all the attributes from XML, this shader is the ultimate result containing
97     // all the XML information.
98     private Shader mShader = null;
99 
100     // Below are the attributes at the root element <gradient>.
101     // NOTE: they need to be copied in the copy constructor!
102     private int mGradientType = GradientDrawable.LINEAR_GRADIENT;
103 
104     private float mCenterX = 0f;
105     private float mCenterY = 0f;
106 
107     private float mStartX = 0f;
108     private float mStartY = 0f;
109     private float mEndX = 0f;
110     private float mEndY = 0f;
111 
112     private int mStartColor = 0;
113     private int mCenterColor = 0;
114     private int mEndColor = 0;
115     private boolean mHasCenterColor = false;
116 
117     private int mTileMode = 0; // Clamp mode.
118 
119     private float mGradientRadius = 0f;
120 
121     // Below are the attributes for the <item> element.
122     private int[] mItemColors;
123     private float[] mItemOffsets;
124 
125     // Theme attributes for the root and item elements.
126     private int[] mThemeAttrs;
127     private int[][] mItemsThemeAttrs;
128 
GradientColor()129     private GradientColor() {
130     }
131 
GradientColor(GradientColor copy)132     private GradientColor(GradientColor copy) {
133         if (copy != null) {
134             mChangingConfigurations = copy.mChangingConfigurations;
135             mDefaultColor = copy.mDefaultColor;
136             mShader = copy.mShader;
137             mGradientType = copy.mGradientType;
138             mCenterX = copy.mCenterX;
139             mCenterY = copy.mCenterY;
140             mStartX = copy.mStartX;
141             mStartY = copy.mStartY;
142             mEndX = copy.mEndX;
143             mEndY = copy.mEndY;
144             mStartColor = copy.mStartColor;
145             mCenterColor = copy.mCenterColor;
146             mEndColor = copy.mEndColor;
147             mHasCenterColor = copy.mHasCenterColor;
148             mGradientRadius = copy.mGradientRadius;
149             mTileMode = copy.mTileMode;
150 
151             if (copy.mItemColors != null) {
152                 mItemColors = copy.mItemColors.clone();
153             }
154             if (copy.mItemOffsets != null) {
155                 mItemOffsets = copy.mItemOffsets.clone();
156             }
157 
158             if (copy.mThemeAttrs != null) {
159                 mThemeAttrs = copy.mThemeAttrs.clone();
160             }
161             if (copy.mItemsThemeAttrs != null) {
162                 mItemsThemeAttrs = copy.mItemsThemeAttrs.clone();
163             }
164         }
165     }
166 
167     // Set the default to clamp mode.
parseTileMode(@radientTileMode int tileMode)168     private static Shader.TileMode parseTileMode(@GradientTileMode int tileMode) {
169         switch (tileMode) {
170             case TILE_MODE_CLAMP:
171                 return Shader.TileMode.CLAMP;
172             case TILE_MODE_REPEAT:
173                 return Shader.TileMode.REPEAT;
174             case TILE_MODE_MIRROR:
175                 return Shader.TileMode.MIRROR;
176             default:
177                 return Shader.TileMode.CLAMP;
178         }
179     }
180 
181     /**
182      * Update the root level's attributes, either for inflate or applyTheme.
183      */
updateRootElementState(TypedArray a)184     private void updateRootElementState(TypedArray a) {
185         // Extract the theme attributes, if any.
186         mThemeAttrs = a.extractThemeAttrs();
187 
188         mStartX = a.getFloat(
189                 R.styleable.GradientColor_startX, mStartX);
190         mStartY = a.getFloat(
191                 R.styleable.GradientColor_startY, mStartY);
192         mEndX = a.getFloat(
193                 R.styleable.GradientColor_endX, mEndX);
194         mEndY = a.getFloat(
195                 R.styleable.GradientColor_endY, mEndY);
196 
197         mCenterX = a.getFloat(
198                 R.styleable.GradientColor_centerX, mCenterX);
199         mCenterY = a.getFloat(
200                 R.styleable.GradientColor_centerY, mCenterY);
201 
202         mGradientType = a.getInt(
203                 R.styleable.GradientColor_type, mGradientType);
204 
205         mStartColor = a.getColor(
206                 R.styleable.GradientColor_startColor, mStartColor);
207         mHasCenterColor |= a.hasValue(
208                 R.styleable.GradientColor_centerColor);
209         mCenterColor = a.getColor(
210                 R.styleable.GradientColor_centerColor, mCenterColor);
211         mEndColor = a.getColor(
212                 R.styleable.GradientColor_endColor, mEndColor);
213 
214         mTileMode = a.getInt(
215                 R.styleable.GradientColor_tileMode, mTileMode);
216 
217         if (DBG_GRADIENT) {
218             Log.v(TAG, "hasCenterColor is " + mHasCenterColor);
219             if (mHasCenterColor) {
220                 Log.v(TAG, "centerColor:" + mCenterColor);
221             }
222             Log.v(TAG, "startColor: " + mStartColor);
223             Log.v(TAG, "endColor: " + mEndColor);
224             Log.v(TAG, "tileMode: " + mTileMode);
225         }
226 
227         mGradientRadius = a.getFloat(R.styleable.GradientColor_gradientRadius,
228                 mGradientRadius);
229     }
230 
231     /**
232      * Check if the XML content is valid.
233      *
234      * @throws XmlPullParserException if errors were found.
235      */
validateXmlContent()236     private void validateXmlContent() throws XmlPullParserException {
237         if (mGradientRadius <= 0
238                 && mGradientType == GradientDrawable.RADIAL_GRADIENT) {
239             throw new XmlPullParserException(
240                     "<gradient> tag requires 'gradientRadius' "
241                             + "attribute with radial type");
242         }
243     }
244 
245     /**
246      * The shader information will be applied to the native VectorDrawable's path.
247      * @hide
248      */
getShader()249     public Shader getShader() {
250         return mShader;
251     }
252 
253     /**
254      * A public method to create GradientColor from a XML resource.
255      */
createFromXml(Resources r, XmlResourceParser parser, Theme theme)256     public static GradientColor createFromXml(Resources r, XmlResourceParser parser, Theme theme)
257             throws XmlPullParserException, IOException {
258         final AttributeSet attrs = Xml.asAttributeSet(parser);
259 
260         int type;
261         while ((type = parser.next()) != XmlPullParser.START_TAG
262                 && type != XmlPullParser.END_DOCUMENT) {
263             // Seek parser to start tag.
264         }
265 
266         if (type != XmlPullParser.START_TAG) {
267             throw new XmlPullParserException("No start tag found");
268         }
269 
270         return createFromXmlInner(r, parser, attrs, theme);
271     }
272 
273     /**
274      * Create from inside an XML document. Called on a parser positioned at a
275      * tag in an XML document, tries to create a GradientColor from that tag.
276      *
277      * @return A new GradientColor for the current tag.
278      * @throws XmlPullParserException if the current tag is not &lt;gradient>
279      */
280     @NonNull
createFromXmlInner(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)281     static GradientColor createFromXmlInner(@NonNull Resources r,
282             @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)
283             throws XmlPullParserException, IOException {
284         final String name = parser.getName();
285         if (!name.equals("gradient")) {
286             throw new XmlPullParserException(
287                     parser.getPositionDescription() + ": invalid gradient color tag " + name);
288         }
289 
290         final GradientColor gradientColor = new GradientColor();
291         gradientColor.inflate(r, parser, attrs, theme);
292         return gradientColor;
293     }
294 
295     /**
296      * Fill in this object based on the contents of an XML "gradient" element.
297      */
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)298     private void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
299             @NonNull AttributeSet attrs, @Nullable Theme theme)
300             throws XmlPullParserException, IOException {
301         final TypedArray a = Resources.obtainAttributes(r, theme, attrs, R.styleable.GradientColor);
302         updateRootElementState(a);
303         mChangingConfigurations |= a.getChangingConfigurations();
304         a.recycle();
305 
306         // Check correctness and throw exception if errors found.
307         validateXmlContent();
308 
309         inflateChildElements(r, parser, attrs, theme);
310 
311         onColorsChange();
312     }
313 
314     /**
315      * Inflates child elements "item"s for each color stop.
316      *
317      * Note that at root level, we need to save ThemeAttrs for theme applied later.
318      * Here similarly, at each child item, we need to save the theme's attributes, and apply theme
319      * later as applyItemsAttrsTheme().
320      */
inflateChildElements(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @NonNull Theme theme)321     private void inflateChildElements(@NonNull Resources r, @NonNull XmlPullParser parser,
322             @NonNull AttributeSet attrs, @NonNull Theme theme)
323             throws XmlPullParserException, IOException {
324         final int innerDepth = parser.getDepth() + 1;
325         int type;
326         int depth;
327 
328         // Pre-allocate the array with some size, for better performance.
329         float[] offsetList = new float[20];
330         int[] colorList = new int[offsetList.length];
331         int[][] themeAttrsList = new int[offsetList.length][];
332 
333         int listSize = 0;
334         boolean hasUnresolvedAttrs = false;
335         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
336                 && ((depth = parser.getDepth()) >= innerDepth
337                 || type != XmlPullParser.END_TAG)) {
338             if (type != XmlPullParser.START_TAG) {
339                 continue;
340             }
341             if (depth > innerDepth || !parser.getName().equals("item")) {
342                 continue;
343             }
344 
345             final TypedArray a = Resources.obtainAttributes(r, theme, attrs,
346                     R.styleable.GradientColorItem);
347             boolean hasColor = a.hasValue(R.styleable.GradientColorItem_color);
348             boolean hasOffset = a.hasValue(R.styleable.GradientColorItem_offset);
349             if (!hasColor || !hasOffset) {
350                 throw new XmlPullParserException(
351                         parser.getPositionDescription()
352                                 + ": <item> tag requires a 'color' attribute and a 'offset' "
353                                 + "attribute!");
354             }
355 
356             final int[] themeAttrs = a.extractThemeAttrs();
357             int color = a.getColor(R.styleable.GradientColorItem_color, 0);
358             float offset = a.getFloat(R.styleable.GradientColorItem_offset, 0);
359 
360             if (DBG_GRADIENT) {
361                 Log.v(TAG, "new item color " + color + " " + Integer.toHexString(color));
362                 Log.v(TAG, "offset" + offset);
363             }
364             mChangingConfigurations |= a.getChangingConfigurations();
365             a.recycle();
366 
367             if (themeAttrs != null) {
368                 hasUnresolvedAttrs = true;
369             }
370 
371             colorList = GrowingArrayUtils.append(colorList, listSize, color);
372             offsetList = GrowingArrayUtils.append(offsetList, listSize, offset);
373             themeAttrsList = GrowingArrayUtils.append(themeAttrsList, listSize, themeAttrs);
374             listSize++;
375         }
376         if (listSize > 0) {
377             if (hasUnresolvedAttrs) {
378                 mItemsThemeAttrs = new int[listSize][];
379                 System.arraycopy(themeAttrsList, 0, mItemsThemeAttrs, 0, listSize);
380             } else {
381                 mItemsThemeAttrs = null;
382             }
383 
384             mItemColors = new int[listSize];
385             mItemOffsets = new float[listSize];
386             System.arraycopy(colorList, 0, mItemColors, 0, listSize);
387             System.arraycopy(offsetList, 0, mItemOffsets, 0, listSize);
388         }
389     }
390 
391     /**
392      * Apply theme to all the items.
393      */
applyItemsAttrsTheme(Theme t)394     private void applyItemsAttrsTheme(Theme t) {
395         if (mItemsThemeAttrs == null) {
396             return;
397         }
398 
399         boolean hasUnresolvedAttrs = false;
400 
401         final int[][] themeAttrsList = mItemsThemeAttrs;
402         final int N = themeAttrsList.length;
403         for (int i = 0; i < N; i++) {
404             if (themeAttrsList[i] != null) {
405                 final TypedArray a = t.resolveAttributes(themeAttrsList[i],
406                         R.styleable.GradientColorItem);
407 
408                 // Extract the theme attributes, if any, before attempting to
409                 // read from the typed array. This prevents a crash if we have
410                 // unresolved attrs.
411                 themeAttrsList[i] = a.extractThemeAttrs(themeAttrsList[i]);
412                 if (themeAttrsList[i] != null) {
413                     hasUnresolvedAttrs = true;
414                 }
415 
416                 mItemColors[i] = a.getColor(R.styleable.GradientColorItem_color, mItemColors[i]);
417                 mItemOffsets[i] = a.getFloat(R.styleable.GradientColorItem_offset, mItemOffsets[i]);
418                 if (DBG_GRADIENT) {
419                     Log.v(TAG, "applyItemsAttrsTheme Colors[i] " + i + " " +
420                             Integer.toHexString(mItemColors[i]));
421                     Log.v(TAG, "Offsets[i] " + i + " " + mItemOffsets[i]);
422                 }
423 
424                 // Account for any configuration changes.
425                 mChangingConfigurations |= a.getChangingConfigurations();
426 
427                 a.recycle();
428             }
429         }
430 
431         if (!hasUnresolvedAttrs) {
432             mItemsThemeAttrs = null;
433         }
434     }
435 
onColorsChange()436     private void onColorsChange() {
437         int[] tempColors = null;
438         float[] tempOffsets = null;
439 
440         if (mItemColors != null) {
441             int length = mItemColors.length;
442             tempColors = new int[length];
443             tempOffsets = new float[length];
444 
445             for (int i = 0; i < length; i++) {
446                 tempColors[i] = mItemColors[i];
447                 tempOffsets[i] = mItemOffsets[i];
448             }
449         } else {
450             if (mHasCenterColor) {
451                 tempColors = new int[3];
452                 tempColors[0] = mStartColor;
453                 tempColors[1] = mCenterColor;
454                 tempColors[2] = mEndColor;
455 
456                 tempOffsets = new float[3];
457                 tempOffsets[0] = 0.0f;
458                 // Since 0.5f is default value, try to take the one that isn't 0.5f
459                 tempOffsets[1] = 0.5f;
460                 tempOffsets[2] = 1f;
461             } else {
462                 tempColors = new int[2];
463                 tempColors[0] = mStartColor;
464                 tempColors[1] = mEndColor;
465             }
466         }
467         if (tempColors.length < 2) {
468             Log.w(TAG, "<gradient> tag requires 2 color values specified!" + tempColors.length
469                     + " " + tempColors);
470         }
471 
472         if (mGradientType == GradientDrawable.LINEAR_GRADIENT) {
473             mShader = new LinearGradient(mStartX, mStartY, mEndX, mEndY, tempColors, tempOffsets,
474                     parseTileMode(mTileMode));
475         } else {
476             if (mGradientType == GradientDrawable.RADIAL_GRADIENT) {
477                 mShader = new RadialGradient(mCenterX, mCenterY, mGradientRadius, tempColors,
478                         tempOffsets, parseTileMode(mTileMode));
479             } else {
480                 mShader = new SweepGradient(mCenterX, mCenterY, tempColors, tempOffsets);
481             }
482         }
483         mDefaultColor = tempColors[0];
484     }
485 
486     /**
487      * For Gradient color, the default color is not very useful, since the gradient will override
488      * the color information anyway.
489      */
490     @Override
491     @ColorInt
getDefaultColor()492     public int getDefaultColor() {
493         return mDefaultColor;
494     }
495 
496     /**
497      * Similar to ColorStateList, setup constant state and its factory.
498      * @hide only for resource preloading
499      */
500     @Override
getConstantState()501     public ConstantState<ComplexColor> getConstantState() {
502         if (mFactory == null) {
503             mFactory = new GradientColorFactory(this);
504         }
505         return mFactory;
506     }
507 
508     private static class GradientColorFactory extends ConstantState<ComplexColor> {
509         private final GradientColor mSrc;
510 
GradientColorFactory(GradientColor src)511         public GradientColorFactory(GradientColor src) {
512             mSrc = src;
513         }
514 
515         @Override
getChangingConfigurations()516         public @Config int getChangingConfigurations() {
517             return mSrc.mChangingConfigurations;
518         }
519 
520         @Override
newInstance()521         public GradientColor newInstance() {
522             return mSrc;
523         }
524 
525         @Override
newInstance(Resources res, Theme theme)526         public GradientColor newInstance(Resources res, Theme theme) {
527             return mSrc.obtainForTheme(theme);
528         }
529     }
530 
531     /**
532      * Returns an appropriately themed gradient color.
533      *
534      * @param t the theme to apply
535      * @return a copy of the gradient color the theme applied, or the
536      * gradient itself if there were no unresolved theme
537      * attributes
538      * @hide only for resource preloading
539      */
540     @Override
obtainForTheme(Theme t)541     public GradientColor obtainForTheme(Theme t) {
542         if (t == null || !canApplyTheme()) {
543             return this;
544         }
545 
546         final GradientColor clone = new GradientColor(this);
547         clone.applyTheme(t);
548         return clone;
549     }
550 
551     /**
552      * Returns a mask of the configuration parameters for which this gradient
553      * may change, requiring that it be re-created.
554      *
555      * @return a mask of the changing configuration parameters, as defined by
556      *         {@link android.content.pm.ActivityInfo}
557      *
558      * @see android.content.pm.ActivityInfo
559      */
getChangingConfigurations()560     public int getChangingConfigurations() {
561         return super.getChangingConfigurations() | mChangingConfigurations;
562     }
563 
applyTheme(Theme t)564     private void applyTheme(Theme t) {
565         if (mThemeAttrs != null) {
566             applyRootAttrsTheme(t);
567         }
568         if (mItemsThemeAttrs != null) {
569             applyItemsAttrsTheme(t);
570         }
571         onColorsChange();
572     }
573 
applyRootAttrsTheme(Theme t)574     private void applyRootAttrsTheme(Theme t) {
575         final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.GradientColor);
576         // mThemeAttrs will be set to null if if there are no theme attributes in the
577         // typed array.
578         mThemeAttrs = a.extractThemeAttrs(mThemeAttrs);
579         // merging the attributes update inside the updateRootElementState().
580         updateRootElementState(a);
581 
582         // Account for any configuration changes.
583         mChangingConfigurations |= a.getChangingConfigurations();
584         a.recycle();
585     }
586 
587 
588     /**
589      * Returns whether a theme can be applied to this gradient color, which
590      * usually indicates that the gradient color has unresolved theme
591      * attributes.
592      *
593      * @return whether a theme can be applied to this gradient color.
594      * @hide only for resource preloading
595      */
596     @Override
canApplyTheme()597     public boolean canApplyTheme() {
598         return mThemeAttrs != null || mItemsThemeAttrs != null;
599     }
600 
601 }
602