1 /* 2 * Copyright (C) 2017 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 androidx.transition; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.content.res.XmlResourceParser; 23 import android.util.AttributeSet; 24 import android.util.Xml; 25 import android.view.InflateException; 26 import android.view.ViewGroup; 27 28 import androidx.annotation.NonNull; 29 import androidx.collection.ArrayMap; 30 import androidx.core.content.res.TypedArrayUtils; 31 32 import org.xmlpull.v1.XmlPullParser; 33 import org.xmlpull.v1.XmlPullParserException; 34 35 import java.io.IOException; 36 import java.lang.reflect.Constructor; 37 38 /** 39 * This class inflates scenes and transitions from resource files. 40 */ 41 public class TransitionInflater { 42 43 private static final Class<?>[] CONSTRUCTOR_SIGNATURE = 44 new Class[]{Context.class, AttributeSet.class}; 45 private static final ArrayMap<String, Constructor> CONSTRUCTORS = new ArrayMap<>(); 46 47 private final Context mContext; 48 49 private TransitionInflater(@NonNull Context context) { 50 mContext = context; 51 } 52 53 /** 54 * Obtains the TransitionInflater from the given context. 55 */ 56 public static TransitionInflater from(Context context) { 57 return new TransitionInflater(context); 58 } 59 60 /** 61 * Loads a {@link Transition} object from a resource 62 * 63 * @param resource The resource id of the transition to load 64 * @return The loaded Transition object 65 * @throws android.content.res.Resources.NotFoundException when the 66 * transition cannot be loaded 67 */ 68 public Transition inflateTransition(int resource) { 69 XmlResourceParser parser = mContext.getResources().getXml(resource); 70 try { 71 return createTransitionFromXml(parser, Xml.asAttributeSet(parser), null); 72 } catch (XmlPullParserException e) { 73 throw new InflateException(e.getMessage(), e); 74 } catch (IOException e) { 75 throw new InflateException( 76 parser.getPositionDescription() + ": " + e.getMessage(), e); 77 } finally { 78 parser.close(); 79 } 80 } 81 82 /** 83 * Loads a {@link TransitionManager} object from a resource 84 * 85 * @param resource The resource id of the transition manager to load 86 * @return The loaded TransitionManager object 87 * @throws android.content.res.Resources.NotFoundException when the 88 * transition manager cannot be loaded 89 */ 90 public TransitionManager inflateTransitionManager(int resource, ViewGroup sceneRoot) { 91 XmlResourceParser parser = mContext.getResources().getXml(resource); 92 try { 93 return createTransitionManagerFromXml(parser, Xml.asAttributeSet(parser), sceneRoot); 94 } catch (XmlPullParserException e) { 95 InflateException ex = new InflateException(e.getMessage()); 96 ex.initCause(e); 97 throw ex; 98 } catch (IOException e) { 99 InflateException ex = new InflateException( 100 parser.getPositionDescription() 101 + ": " + e.getMessage()); 102 ex.initCause(e); 103 throw ex; 104 } finally { 105 parser.close(); 106 } 107 } 108 109 // 110 // Transition loading 111 // 112 private Transition createTransitionFromXml(XmlPullParser parser, 113 AttributeSet attrs, Transition parent) 114 throws XmlPullParserException, IOException { 115 116 Transition transition = null; 117 118 // Make sure we are on a start tag. 119 int type; 120 int depth = parser.getDepth(); 121 122 TransitionSet transitionSet = (parent instanceof TransitionSet) 123 ? (TransitionSet) parent : null; 124 125 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 126 && type != XmlPullParser.END_DOCUMENT) { 127 128 if (type != XmlPullParser.START_TAG) { 129 continue; 130 } 131 132 String name = parser.getName(); 133 if ("fade".equals(name)) { 134 transition = new Fade(mContext, attrs); 135 } else if ("changeBounds".equals(name)) { 136 transition = new ChangeBounds(mContext, attrs); 137 } else if ("slide".equals(name)) { 138 transition = new Slide(mContext, attrs); 139 } else if ("explode".equals(name)) { 140 transition = new Explode(mContext, attrs); 141 } else if ("changeImageTransform".equals(name)) { 142 transition = new ChangeImageTransform(mContext, attrs); 143 } else if ("changeTransform".equals(name)) { 144 transition = new ChangeTransform(mContext, attrs); 145 } else if ("changeClipBounds".equals(name)) { 146 transition = new ChangeClipBounds(mContext, attrs); 147 } else if ("autoTransition".equals(name)) { 148 transition = new AutoTransition(mContext, attrs); 149 } else if ("changeScroll".equals(name)) { 150 transition = new ChangeScroll(mContext, attrs); 151 } else if ("transitionSet".equals(name)) { 152 transition = new TransitionSet(mContext, attrs); 153 } else if ("transition".equals(name)) { 154 transition = (Transition) createCustom(attrs, Transition.class, "transition"); 155 } else if ("targets".equals(name)) { 156 getTargetIds(parser, attrs, parent); 157 } else if ("arcMotion".equals(name)) { 158 if (parent == null) { 159 throw new RuntimeException("Invalid use of arcMotion element"); 160 } 161 parent.setPathMotion(new ArcMotion(mContext, attrs)); 162 } else if ("pathMotion".equals(name)) { 163 if (parent == null) { 164 throw new RuntimeException("Invalid use of pathMotion element"); 165 } 166 parent.setPathMotion((PathMotion) createCustom(attrs, PathMotion.class, 167 "pathMotion")); 168 } else if ("patternPathMotion".equals(name)) { 169 if (parent == null) { 170 throw new RuntimeException("Invalid use of patternPathMotion element"); 171 } 172 parent.setPathMotion(new PatternPathMotion(mContext, attrs)); 173 } else { 174 throw new RuntimeException("Unknown scene name: " + parser.getName()); 175 } 176 if (transition != null) { 177 if (!parser.isEmptyElementTag()) { 178 createTransitionFromXml(parser, attrs, transition); 179 } 180 if (transitionSet != null) { 181 transitionSet.addTransition(transition); 182 transition = null; 183 } else if (parent != null) { 184 throw new InflateException("Could not add transition to another transition."); 185 } 186 } 187 } 188 189 return transition; 190 } 191 192 private Object createCustom(AttributeSet attrs, Class expectedType, String tag) { 193 String className = attrs.getAttributeValue(null, "class"); 194 195 if (className == null) { 196 throw new InflateException(tag + " tag must have a 'class' attribute"); 197 } 198 199 try { 200 synchronized (CONSTRUCTORS) { 201 Constructor constructor = CONSTRUCTORS.get(className); 202 if (constructor == null) { 203 @SuppressWarnings("unchecked") 204 Class<?> c = mContext.getClassLoader().loadClass(className) 205 .asSubclass(expectedType); 206 if (c != null) { 207 constructor = c.getConstructor(CONSTRUCTOR_SIGNATURE); 208 constructor.setAccessible(true); 209 CONSTRUCTORS.put(className, constructor); 210 } 211 } 212 //noinspection ConstantConditions 213 return constructor.newInstance(mContext, attrs); 214 } 215 } catch (Exception e) { 216 throw new InflateException("Could not instantiate " + expectedType + " class " 217 + className, e); 218 } 219 } 220 221 private void getTargetIds(XmlPullParser parser, 222 AttributeSet attrs, Transition transition) throws XmlPullParserException, IOException { 223 224 // Make sure we are on a start tag. 225 int type; 226 int depth = parser.getDepth(); 227 228 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 229 && type != XmlPullParser.END_DOCUMENT) { 230 231 if (type != XmlPullParser.START_TAG) { 232 continue; 233 } 234 235 String name = parser.getName(); 236 if (name.equals("target")) { 237 TypedArray a = mContext.obtainStyledAttributes(attrs, Styleable.TRANSITION_TARGET); 238 int id = TypedArrayUtils.getNamedResourceId(a, parser, "targetId", 239 Styleable.TransitionTarget.TARGET_ID, 0); 240 String transitionName; 241 if (id != 0) { 242 transition.addTarget(id); 243 } else if ((id = TypedArrayUtils.getNamedResourceId(a, parser, "excludeId", 244 Styleable.TransitionTarget.EXCLUDE_ID, 0)) != 0) { 245 transition.excludeTarget(id, true); 246 } else if ((transitionName = TypedArrayUtils.getNamedString(a, parser, "targetName", 247 Styleable.TransitionTarget.TARGET_NAME)) != null) { 248 transition.addTarget(transitionName); 249 } else if ((transitionName = TypedArrayUtils.getNamedString(a, parser, 250 "excludeName", Styleable.TransitionTarget.EXCLUDE_NAME)) != null) { 251 transition.excludeTarget(transitionName, true); 252 } else { 253 String className = TypedArrayUtils.getNamedString(a, parser, 254 "excludeClass", Styleable.TransitionTarget.EXCLUDE_CLASS); 255 try { 256 if (className != null) { 257 Class clazz = Class.forName(className); 258 transition.excludeTarget(clazz, true); 259 } else if ((className = TypedArrayUtils.getNamedString(a, parser, 260 "targetClass", Styleable.TransitionTarget.TARGET_CLASS)) != null) { 261 Class clazz = Class.forName(className); 262 transition.addTarget(clazz); 263 } 264 } catch (ClassNotFoundException e) { 265 a.recycle(); 266 throw new RuntimeException("Could not create " + className, e); 267 } 268 } 269 a.recycle(); 270 } else { 271 throw new RuntimeException("Unknown scene name: " + parser.getName()); 272 } 273 } 274 } 275 276 // 277 // TransitionManager loading 278 // 279 280 private TransitionManager createTransitionManagerFromXml(XmlPullParser parser, 281 AttributeSet attrs, ViewGroup sceneRoot) throws XmlPullParserException, IOException { 282 283 // Make sure we are on a start tag. 284 int type; 285 int depth = parser.getDepth(); 286 TransitionManager transitionManager = null; 287 288 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 289 && type != XmlPullParser.END_DOCUMENT) { 290 291 if (type != XmlPullParser.START_TAG) { 292 continue; 293 } 294 295 String name = parser.getName(); 296 if (name.equals("transitionManager")) { 297 transitionManager = new TransitionManager(); 298 } else if (name.equals("transition") && (transitionManager != null)) { 299 loadTransition(attrs, parser, sceneRoot, transitionManager); 300 } else { 301 throw new RuntimeException("Unknown scene name: " + parser.getName()); 302 } 303 } 304 return transitionManager; 305 } 306 307 private void loadTransition(AttributeSet attrs, XmlPullParser parser, ViewGroup sceneRoot, 308 TransitionManager transitionManager) throws Resources.NotFoundException { 309 310 TypedArray a = mContext.obtainStyledAttributes(attrs, Styleable.TRANSITION_MANAGER); 311 int transitionId = TypedArrayUtils.getNamedResourceId(a, parser, "transition", 312 Styleable.TransitionManager.TRANSITION, -1); 313 int fromId = TypedArrayUtils.getNamedResourceId(a, parser, "fromScene", 314 Styleable.TransitionManager.FROM_SCENE, -1); 315 Scene fromScene = (fromId < 0) ? null : Scene.getSceneForLayout(sceneRoot, fromId, 316 mContext); 317 int toId = TypedArrayUtils.getNamedResourceId(a, parser, "toScene", 318 Styleable.TransitionManager.TO_SCENE, -1); 319 Scene toScene = (toId < 0) ? null : Scene.getSceneForLayout(sceneRoot, toId, mContext); 320 321 if (transitionId >= 0) { 322 Transition transition = inflateTransition(transitionId); 323 if (transition != null) { 324 if (toScene == null) { 325 throw new RuntimeException("No toScene for transition ID " + transitionId); 326 } 327 if (fromScene == null) { 328 transitionManager.setTransition(toScene, transition); 329 } else { 330 transitionManager.setTransition(fromScene, toScene, transition); 331 } 332 } 333 } 334 a.recycle(); 335 } 336 337 } 338