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