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 */ 64 @Nullable loadDrawable(@onNull Context context, @DrawableRes int id)65 public static Drawable loadDrawable(@NonNull Context context, @DrawableRes int id) { 66 return loadDrawable(context.getResources(), context.getTheme(), id); 67 } 68 69 /** 70 * Loads the drawable resource with the specified identifier. 71 * 72 * @param resources the resources from which the drawable should be loaded 73 * @param theme the theme against which the drawable should be inflated 74 * @param id the identifier of the drawable resource 75 * @return a drawable, or {@code null} if the drawable failed to load 76 */ 77 @Nullable loadDrawable( @onNull Resources resources, @Nullable Theme theme, @DrawableRes int id)78 public static Drawable loadDrawable( 79 @NonNull Resources resources, @Nullable Theme theme, @DrawableRes int id) { 80 return resources.getDrawable(id, theme); 81 } 82 83 /** 84 * Constructs a new drawable inflater using the specified resources and 85 * class loader. 86 * 87 * @param res the resources used to resolve resource identifiers 88 * @param classLoader the class loader used to load custom drawables 89 * @hide 90 */ DrawableInflater(@onNull Resources res, @NonNull ClassLoader classLoader)91 public DrawableInflater(@NonNull Resources res, @NonNull ClassLoader classLoader) { 92 mRes = res; 93 mClassLoader = classLoader; 94 } 95 96 /** 97 * Inflates a drawable from inside an XML document using an optional 98 * {@link Theme}. 99 * <p> 100 * This method should be called on a parser positioned at a tag in an XML 101 * document defining a drawable resource. It will attempt to create a 102 * Drawable from the tag at the current position. 103 * 104 * @param name the name of the tag at the current position 105 * @param parser an XML parser positioned at the drawable tag 106 * @param attrs an attribute set that wraps the parser 107 * @param theme the theme against which the drawable should be inflated, or 108 * {@code null} to not inflate against a theme 109 * @return a drawable 110 * 111 * @throws XmlPullParserException 112 * @throws IOException 113 */ 114 @NonNull inflateFromXml(@onNull String name, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)115 public Drawable inflateFromXml(@NonNull String name, @NonNull XmlPullParser parser, 116 @NonNull AttributeSet attrs, @Nullable Theme theme) 117 throws XmlPullParserException, IOException { 118 return inflateFromXmlForDensity(name, parser, attrs, 0, theme); 119 } 120 121 /** 122 * Version of {@link #inflateFromXml(String, XmlPullParser, AttributeSet, Theme)} that accepts 123 * an override density. 124 */ 125 @NonNull inflateFromXmlForDensity(@onNull String name, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, int density, @Nullable Theme theme)126 Drawable inflateFromXmlForDensity(@NonNull String name, @NonNull XmlPullParser parser, 127 @NonNull AttributeSet attrs, int density, @Nullable Theme theme) 128 throws XmlPullParserException, IOException { 129 // Inner classes must be referenced as Outer$Inner, but XML tag names 130 // can't contain $, so the <drawable> tag allows developers to specify 131 // the class in an attribute. We'll still run it through inflateFromTag 132 // to stay consistent with how LayoutInflater works. 133 if (name.equals("drawable")) { 134 name = attrs.getAttributeValue(null, "class"); 135 if (name == null) { 136 throw new InflateException("<drawable> tag must specify class attribute"); 137 } 138 } 139 140 Drawable drawable = inflateFromTag(name); 141 if (drawable == null) { 142 drawable = inflateFromClass(name); 143 } 144 drawable.setSrcDensityOverride(density); 145 drawable.inflate(mRes, parser, attrs, theme); 146 return drawable; 147 } 148 149 @NonNull 150 @SuppressWarnings("deprecation") inflateFromTag(@onNull String name)151 private Drawable inflateFromTag(@NonNull String name) { 152 switch (name) { 153 case "selector": 154 return new StateListDrawable(); 155 case "animated-selector": 156 return new AnimatedStateListDrawable(); 157 case "level-list": 158 return new LevelListDrawable(); 159 case "layer-list": 160 return new LayerDrawable(); 161 case "transition": 162 return new TransitionDrawable(); 163 case "ripple": 164 return new RippleDrawable(); 165 case "adaptive-icon": 166 return new AdaptiveIconDrawable(); 167 case "color": 168 return new ColorDrawable(); 169 case "shape": 170 return new GradientDrawable(); 171 case "vector": 172 return new VectorDrawable(); 173 case "animated-vector": 174 return new AnimatedVectorDrawable(); 175 case "scale": 176 return new ScaleDrawable(); 177 case "clip": 178 return new ClipDrawable(); 179 case "rotate": 180 return new RotateDrawable(); 181 case "animated-rotate": 182 return new AnimatedRotateDrawable(); 183 case "animation-list": 184 return new AnimationDrawable(); 185 case "inset": 186 return new InsetDrawable(); 187 case "bitmap": 188 return new BitmapDrawable(); 189 case "nine-patch": 190 return new NinePatchDrawable(); 191 case "animated-image": 192 return new AnimatedImageDrawable(); 193 default: 194 return null; 195 } 196 } 197 198 @NonNull inflateFromClass(@onNull String className)199 private Drawable inflateFromClass(@NonNull String className) { 200 try { 201 Constructor<? extends Drawable> constructor; 202 synchronized (CONSTRUCTOR_MAP) { 203 constructor = CONSTRUCTOR_MAP.get(className); 204 if (constructor == null) { 205 final Class<? extends Drawable> clazz = 206 mClassLoader.loadClass(className).asSubclass(Drawable.class); 207 constructor = clazz.getConstructor(); 208 CONSTRUCTOR_MAP.put(className, constructor); 209 } 210 } 211 return constructor.newInstance(); 212 } catch (NoSuchMethodException e) { 213 final InflateException ie = new InflateException( 214 "Error inflating class " + className); 215 ie.initCause(e); 216 throw ie; 217 } catch (ClassCastException e) { 218 // If loaded class is not a Drawable subclass. 219 final InflateException ie = new InflateException( 220 "Class is not a Drawable " + className); 221 ie.initCause(e); 222 throw ie; 223 } catch (ClassNotFoundException e) { 224 // If loadClass fails, we should propagate the exception. 225 final InflateException ie = new InflateException( 226 "Class not found " + className); 227 ie.initCause(e); 228 throw ie; 229 } catch (Exception e) { 230 final InflateException ie = new InflateException( 231 "Error inflating class " + className); 232 ie.initCause(e); 233 throw ie; 234 } 235 } 236 } 237