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