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({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