1 /*
2  * Copyright (C) 2015 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.graphics.drawable;
18 
19 import org.xmlpull.v1.XmlPullParser;
20 import org.xmlpull.v1.XmlPullParserException;
21 
22 import android.annotation.DrawableRes;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.content.Context;
26 import android.content.res.Resources;
27 import android.content.res.Resources.Theme;
28 import android.util.AttributeSet;
29 import android.view.InflateException;
30 
31 import java.io.IOException;
32 import java.lang.reflect.Constructor;
33 import java.util.HashMap;
34 
35 /**
36  * Instantiates a drawable XML file into its corresponding
37  * {@link android.graphics.drawable.Drawable} objects.
38  * <p>
39  * For performance reasons, inflation relies heavily on pre-processing of
40  * XML files that is done at build time. Therefore, it is not currently possible
41  * to use this inflater with an XmlPullParser over a plain XML file at runtime;
42  * it only works with an XmlPullParser returned from a compiled resource (R.
43  * <em>something</em> file.)
44  *
45  * @hide Pending API finalization.
46  */
47 public final class DrawableInflater {
48     private static final HashMap<String, Constructor<? extends Drawable>> CONSTRUCTOR_MAP =
49             new HashMap<>();
50 
51     private final Resources mRes;
52     private final ClassLoader mClassLoader;
53 
54     /**
55      * Loads the drawable resource with the specified identifier.
56      *
57      * @param context the context in which the drawable should be loaded
58      * @param id the identifier of the drawable resource
59      * @return a drawable, or {@code null} if the drawable failed to load
60      */
61     @Nullable
loadDrawable(@onNull Context context, @DrawableRes int id)62     public static Drawable loadDrawable(@NonNull Context context, @DrawableRes int id) {
63         return loadDrawable(context.getResources(), context.getTheme(), id);
64     }
65 
66     /**
67      * Loads the drawable resource with the specified identifier.
68      *
69      * @param resources the resources from which the drawable should be loaded
70      * @param theme the theme against which the drawable should be inflated
71      * @param id the identifier of the drawable resource
72      * @return a drawable, or {@code null} if the drawable failed to load
73      */
74     @Nullable
loadDrawable( @onNull Resources resources, @Nullable Theme theme, @DrawableRes int id)75     public static Drawable loadDrawable(
76             @NonNull Resources resources, @Nullable Theme theme, @DrawableRes int id) {
77         return resources.getDrawable(id, theme);
78     }
79 
80     /**
81      * Constructs a new drawable inflater using the specified resources and
82      * class loader.
83      *
84      * @param res the resources used to resolve resource identifiers
85      * @param classLoader the class loader used to load custom drawables
86      * @hide
87      */
DrawableInflater(@onNull Resources res, @NonNull ClassLoader classLoader)88     public DrawableInflater(@NonNull Resources res, @NonNull ClassLoader classLoader) {
89         mRes = res;
90         mClassLoader = classLoader;
91     }
92 
93     /**
94      * Inflates a drawable from inside an XML document using an optional
95      * {@link Theme}.
96      * <p>
97      * This method should be called on a parser positioned at a tag in an XML
98      * document defining a drawable resource. It will attempt to create a
99      * Drawable from the tag at the current position.
100      *
101      * @param name the name of the tag at the current position
102      * @param parser an XML parser positioned at the drawable tag
103      * @param attrs an attribute set that wraps the parser
104      * @param theme the theme against which the drawable should be inflated, or
105      *              {@code null} to not inflate against a theme
106      * @return a drawable
107      *
108      * @throws XmlPullParserException
109      * @throws IOException
110      */
111     @NonNull
inflateFromXml(@onNull String name, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)112     public Drawable inflateFromXml(@NonNull String name, @NonNull XmlPullParser parser,
113             @NonNull AttributeSet attrs, @Nullable Theme theme)
114             throws XmlPullParserException, IOException {
115         // Inner classes must be referenced as Outer$Inner, but XML tag names
116         // can't contain $, so the <drawable> tag allows developers to specify
117         // the class in an attribute. We'll still run it through inflateFromTag
118         // to stay consistent with how LayoutInflater works.
119         if (name.equals("drawable")) {
120             name = attrs.getAttributeValue(null, "class");
121             if (name == null) {
122                 throw new InflateException("<drawable> tag must specify class attribute");
123             }
124         }
125 
126         Drawable drawable = inflateFromTag(name);
127         if (drawable == null) {
128             drawable = inflateFromClass(name);
129         }
130         drawable.inflate(mRes, parser, attrs, theme);
131         return drawable;
132     }
133 
134     @NonNull
135     @SuppressWarnings("deprecation")
inflateFromTag(@onNull String name)136     private Drawable inflateFromTag(@NonNull String name) {
137         switch (name) {
138             case "selector":
139                 return new StateListDrawable();
140             case "animated-selector":
141                 return new AnimatedStateListDrawable();
142             case "level-list":
143                 return new LevelListDrawable();
144             case "layer-list":
145                 return new LayerDrawable();
146             case "transition":
147                 return new TransitionDrawable();
148             case "ripple":
149                 return new RippleDrawable();
150             case "color":
151                 return new ColorDrawable();
152             case "shape":
153                 return new GradientDrawable();
154             case "vector":
155                 return new VectorDrawable();
156             case "animated-vector":
157                 return new AnimatedVectorDrawable();
158             case "scale":
159                 return new ScaleDrawable();
160             case "clip":
161                 return new ClipDrawable();
162             case "rotate":
163                 return new RotateDrawable();
164             case "animated-rotate":
165                 return new AnimatedRotateDrawable();
166             case "animation-list":
167                 return new AnimationDrawable();
168             case "inset":
169                 return new InsetDrawable();
170             case "bitmap":
171                 return new BitmapDrawable();
172             case "nine-patch":
173                 return new NinePatchDrawable();
174             default:
175                 return null;
176         }
177     }
178 
179     @NonNull
inflateFromClass(@onNull String className)180     private Drawable inflateFromClass(@NonNull String className) {
181         try {
182             Constructor<? extends Drawable> constructor;
183             synchronized (CONSTRUCTOR_MAP) {
184                 constructor = CONSTRUCTOR_MAP.get(className);
185                 if (constructor == null) {
186                     final Class<? extends Drawable> clazz =
187                             mClassLoader.loadClass(className).asSubclass(Drawable.class);
188                     constructor = clazz.getConstructor();
189                     CONSTRUCTOR_MAP.put(className, constructor);
190                 }
191             }
192             return constructor.newInstance();
193         } catch (NoSuchMethodException e) {
194             final InflateException ie = new InflateException(
195                     "Error inflating class " + className);
196             ie.initCause(e);
197             throw ie;
198         } catch (ClassCastException e) {
199             // If loaded class is not a Drawable subclass.
200             final InflateException ie = new InflateException(
201                     "Class is not a Drawable " + className);
202             ie.initCause(e);
203             throw ie;
204         } catch (ClassNotFoundException e) {
205             // If loadClass fails, we should propagate the exception.
206             final InflateException ie = new InflateException(
207                     "Class not found " + className);
208             ie.initCause(e);
209             throw ie;
210         } catch (Exception e) {
211             final InflateException ie = new InflateException(
212                     "Error inflating class " + className);
213             ie.initCause(e);
214             throw ie;
215         }
216     }
217 }
218