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