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