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