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 return inflateFromXmlForDensity(name, parser, attrs, 0, theme); 116 } 117 118 /** 119 * Version of {@link #inflateFromXml(String, XmlPullParser, AttributeSet, Theme)} that accepts 120 * an override density. 121 */ 122 @NonNull inflateFromXmlForDensity(@onNull String name, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density, @Nullable Theme theme)123 Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser, 124 @NonNull AttributeSet attrs, int density, @Nullable Theme theme) 125 throws XmlPullParserException, IOException { 126 // Inner classes must be referenced as Outer$Inner, but XML tag names 127 // can't contain $, so the <drawable> tag allows developers to specify 128 // the class in an attribute. We'll still run it through inflateFromTag 129 // to stay consistent with how LayoutInflater works. 130 if (name.equals("drawable")) { 131 name = attrs.getAttributeValue(null, "class"); 132 if (name == null) { 133 throw new InflateException("<drawable> tag must specify class attribute"); 134 } 135 } 136 137 Drawable drawable = inflateFromTag(name); 138 if (drawable == null) { 139 drawable = inflateFromClass(name); 140 } 141 drawable.setSrcDensityOverride(density); 142 drawable.inflate(mRes, parser, attrs, theme); 143 return drawable; 144 } 145 146 @NonNull 147 @SuppressWarnings("deprecation") inflateFromTag(@onNull String name)148 private Drawable inflateFromTag(@NonNull String name) { 149 switch (name) { 150 case "selector": 151 return new StateListDrawable(); 152 case "animated-selector": 153 return new AnimatedStateListDrawable(); 154 case "level-list": 155 return new LevelListDrawable(); 156 case "layer-list": 157 return new LayerDrawable(); 158 case "transition": 159 return new TransitionDrawable(); 160 case "ripple": 161 return new RippleDrawable(); 162 case "adaptive-icon": 163 return new AdaptiveIconDrawable(); 164 case "color": 165 return new ColorDrawable(); 166 case "shape": 167 return new GradientDrawable(); 168 case "vector": 169 return new VectorDrawable(); 170 case "animated-vector": 171 return new AnimatedVectorDrawable(); 172 case "scale": 173 return new ScaleDrawable(); 174 case "clip": 175 return new ClipDrawable(); 176 case "rotate": 177 return new RotateDrawable(); 178 case "animated-rotate": 179 return new AnimatedRotateDrawable(); 180 case "animation-list": 181 return new AnimationDrawable(); 182 case "inset": 183 return new InsetDrawable(); 184 case "bitmap": 185 return new BitmapDrawable(); 186 case "nine-patch": 187 return new NinePatchDrawable(); 188 default: 189 return null; 190 } 191 } 192 193 @NonNull inflateFromClass(@onNull String className)194 private Drawable inflateFromClass(@NonNull String className) { 195 try { 196 Constructor<? extends Drawable> constructor; 197 synchronized (CONSTRUCTOR_MAP) { 198 constructor = CONSTRUCTOR_MAP.get(className); 199 if (constructor == null) { 200 final Class<? extends Drawable> clazz = 201 mClassLoader.loadClass(className).asSubclass(Drawable.class); 202 constructor = clazz.getConstructor(); 203 CONSTRUCTOR_MAP.put(className, constructor); 204 } 205 } 206 return constructor.newInstance(); 207 } catch (NoSuchMethodException e) { 208 final InflateException ie = new InflateException( 209 "Error inflating class " + className); 210 ie.initCause(e); 211 throw ie; 212 } catch (ClassCastException e) { 213 // If loaded class is not a Drawable subclass. 214 final InflateException ie = new InflateException( 215 "Class is not a Drawable " + className); 216 ie.initCause(e); 217 throw ie; 218 } catch (ClassNotFoundException e) { 219 // If loadClass fails, we should propagate the exception. 220 final InflateException ie = new InflateException( 221 "Class not found " + className); 222 ie.initCause(e); 223 throw ie; 224 } catch (Exception e) { 225 final InflateException ie = new InflateException( 226 "Error inflating class " + className); 227 ie.initCause(e); 228 throw ie; 229 } 230 } 231 } 232