Home | History | Annotate | Download | only in res
      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 
    129     private GradientColor() {
    130     }
    131 
    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.
    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      */
    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      */
    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      */
    249     public Shader getShader() {
    250         return mShader;
    251     }
    252 
    253     /**
    254      * A public method to create GradientColor from a XML resource.
    255      */
    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
    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      */
    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      */
    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      */
    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 
    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
    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
    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 
    511         public GradientColorFactory(GradientColor src) {
    512             mSrc = src;
    513         }
    514 
    515         @Override
    516         public @Config int getChangingConfigurations() {
    517             return mSrc.mChangingConfigurations;
    518         }
    519 
    520         @Override
    521         public GradientColor newInstance() {
    522             return mSrc;
    523         }
    524 
    525         @Override
    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
    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      */
    560     public int getChangingConfigurations() {
    561         return super.getChangingConfigurations() | mChangingConfigurations;
    562     }
    563 
    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 
    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
    597     public boolean canApplyTheme() {
    598         return mThemeAttrs != null || mItemsThemeAttrs != null;
    599     }
    600 
    601 }
    602