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
17package android.content.res;
18
19import android.annotation.ColorInt;
20import android.annotation.IntDef;
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.content.pm.ActivityInfo.Config;
24import android.content.res.Resources.Theme;
25
26import com.android.internal.R;
27import com.android.internal.util.GrowingArrayUtils;
28
29import org.xmlpull.v1.XmlPullParser;
30import org.xmlpull.v1.XmlPullParserException;
31
32import android.graphics.LinearGradient;
33import android.graphics.RadialGradient;
34import android.graphics.Shader;
35import android.graphics.SweepGradient;
36import android.graphics.drawable.GradientDrawable;
37import android.util.AttributeSet;
38import android.util.Log;
39import android.util.Xml;
40
41import java.io.IOException;
42import java.lang.annotation.Retention;
43import 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 */
73public 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
124    private GradientColor() {
125    }
126
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.
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     */
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     */
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     */
244    public Shader getShader() {
245        return mShader;
246    }
247
248    /**
249     * A public method to create GradientColor from a XML resource.
250     */
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
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     */
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     */
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     */
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
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
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
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
506        public GradientColorFactory(GradientColor src) {
507            mSrc = src;
508        }
509
510        @Override
511        public @Config int getChangingConfigurations() {
512            return mSrc.mChangingConfigurations;
513        }
514
515        @Override
516        public GradientColor newInstance() {
517            return mSrc;
518        }
519
520        @Override
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
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     */
555    public int getChangingConfigurations() {
556        return super.getChangingConfigurations() | mChangingConfigurations;
557    }
558
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
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
592    public boolean canApplyTheme() {
593        return mThemeAttrs != null || mItemsThemeAttrs != null;
594    }
595
596}
597