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 String name = parser.getName(); 132 if ("fade".equals(name)) { 133 transition = new Fade(mContext, attrs); 134 } else if ("changeBounds".equals(name)) { 135 transition = new ChangeBounds(mContext, attrs); 136 } else if ("slide".equals(name)) { 137 transition = new Slide(mContext, attrs); 138 } else if ("explode".equals(name)) { 139 transition = new Explode(mContext, attrs); 140 } else if ("changeImageTransform".equals(name)) { 141 transition = new ChangeImageTransform(mContext, attrs); 142 } else if ("changeTransform".equals(name)) { 143 transition = new ChangeTransform(mContext, attrs); 144 } else if ("changeClipBounds".equals(name)) { 145 transition = new ChangeClipBounds(mContext, attrs); 146 } else if ("autoTransition".equals(name)) { 147 transition = new AutoTransition(mContext, attrs); 148 } else if ("changeScroll".equals(name)) { 149 transition = new ChangeScroll(mContext, attrs); 150 } else if ("transitionSet".equals(name)) { 151 transition = new TransitionSet(mContext, attrs); 152 } else if ("transition".equals(name)) { 153 transition = (Transition) createCustom(attrs, Transition.class, "transition"); 154 } else if ("targets".equals(name)) { 155 getTargetIds(parser, attrs, parent); 156 } else if ("arcMotion".equals(name)) { 157 if (parent == null) { 158 throw new RuntimeException("Invalid use of arcMotion element"); 159 } 160 parent.setPathMotion(new ArcMotion(mContext, attrs)); 161 } else if ("pathMotion".equals(name)) { 162 if (parent == null) { 163 throw new RuntimeException("Invalid use of pathMotion element"); 164 } 165 parent.setPathMotion((PathMotion) createCustom(attrs, PathMotion.class, 166 "pathMotion")); 167 } else if ("patternPathMotion".equals(name)) { 168 if (parent == null) { 169 throw new RuntimeException("Invalid use of patternPathMotion element"); 170 } 171 parent.setPathMotion(new PatternPathMotion(mContext, attrs)); 172 } else { 173 throw new RuntimeException("Unknown scene name: " + parser.getName()); 174 } 175 if (transition != null) { 176 if (!parser.isEmptyElementTag()) { 177 createTransitionFromXml(parser, attrs, transition); 178 } 179 if (transitionSet != null) { 180 transitionSet.addTransition(transition); 181 transition = null; 182 } else if (parent != null) { 183 throw new InflateException("Could not add transition to another transition."); 184 } 185 } 186 } 187 188 return transition; 189 } 190 191 private Object createCustom(AttributeSet attrs, Class expectedType, String tag) { 192 String className = attrs.getAttributeValue(null, "class"); 193 194 if (className == null) { 195 throw new InflateException(tag + " tag must have a 'class' attribute"); 196 } 197 198 try { 199 synchronized (CONSTRUCTORS) { 200 Constructor constructor = CONSTRUCTORS.get(className); 201 if (constructor == null) { 202 @SuppressWarnings("unchecked") 203 Class<?> c = mContext.getClassLoader().loadClass(className) 204 .asSubclass(expectedType); 205 if (c != null) { 206 constructor = c.getConstructor(CONSTRUCTOR_SIGNATURE); 207 constructor.setAccessible(true); 208 CONSTRUCTORS.put(className, constructor); 209 } 210 } 211 //noinspection ConstantConditions 212 return constructor.newInstance(mContext, attrs); 213 } 214 } catch (Exception e) { 215 throw new InflateException("Could not instantiate " + expectedType + " class " 216 + className, e); 217 } 218 } 219 220 private void getTargetIds(XmlPullParser parser, 221 AttributeSet attrs, Transition transition) throws XmlPullParserException, IOException { 222 223 // Make sure we are on a start tag. 224 int type; 225 int depth = parser.getDepth(); 226 227 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 228 && type != XmlPullParser.END_DOCUMENT) { 229 230 if (type != XmlPullParser.START_TAG) { 231 continue; 232 } 233 234 String name = parser.getName(); 235 if (name.equals("target")) { 236 TypedArray a = mContext.obtainStyledAttributes(attrs, Styleable.TRANSITION_TARGET); 237 int id = TypedArrayUtils.getNamedResourceId(a, parser, "targetId", 238 Styleable.TransitionTarget.TARGET_ID, 0); 239 String transitionName; 240 if (id != 0) { 241 transition.addTarget(id); 242 } else if ((id = TypedArrayUtils.getNamedResourceId(a, parser, "excludeId", 243 Styleable.TransitionTarget.EXCLUDE_ID, 0)) != 0) { 244 transition.excludeTarget(id, true); 245 } else if ((transitionName = TypedArrayUtils.getNamedString(a, parser, "targetName", 246 Styleable.TransitionTarget.TARGET_NAME)) != null) { 247 transition.addTarget(transitionName); 248 } else if ((transitionName = TypedArrayUtils.getNamedString(a, parser, 249 "excludeName", Styleable.TransitionTarget.EXCLUDE_NAME)) != null) { 250 transition.excludeTarget(transitionName, true); 251 } else { 252 String className = TypedArrayUtils.getNamedString(a, parser, 253 "excludeClass", Styleable.TransitionTarget.EXCLUDE_CLASS); 254 try { 255 if (className != null) { 256 Class clazz = Class.forName(className); 257 transition.excludeTarget(clazz, true); 258 } else if ((className = TypedArrayUtils.getNamedString(a, parser, 259 "targetClass", Styleable.TransitionTarget.TARGET_CLASS)) != null) { 260 Class clazz = Class.forName(className); 261 transition.addTarget(clazz); 262 } 263 } catch (ClassNotFoundException e) { 264 a.recycle(); 265 throw new RuntimeException("Could not create " + className, e); 266 } 267 } 268 a.recycle(); 269 } else { 270 throw new RuntimeException("Unknown scene name: " + parser.getName()); 271 } 272 } 273 } 274 275 // 276 // TransitionManager loading 277 // 278 279 private TransitionManager createTransitionManagerFromXml(XmlPullParser parser, 280 AttributeSet attrs, ViewGroup sceneRoot) throws XmlPullParserException, IOException { 281 282 // Make sure we are on a start tag. 283 int type; 284 int depth = parser.getDepth(); 285 TransitionManager transitionManager = null; 286 287 while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) 288 && type != XmlPullParser.END_DOCUMENT) { 289 290 if (type != XmlPullParser.START_TAG) { 291 continue; 292 } 293 294 String name = parser.getName(); 295 if (name.equals("transitionManager")) { 296 transitionManager = new TransitionManager(); 297 } else if (name.equals("transition") && (transitionManager != null)) { 298 loadTransition(attrs, parser, sceneRoot, transitionManager); 299 } else { 300 throw new RuntimeException("Unknown scene name: " + parser.getName()); 301 } 302 } 303 return transitionManager; 304 } 305 306 private void loadTransition(AttributeSet attrs, XmlPullParser parser, ViewGroup sceneRoot, 307 TransitionManager transitionManager) throws Resources.NotFoundException { 308 309 TypedArray a = mContext.obtainStyledAttributes(attrs, Styleable.TRANSITION_MANAGER); 310 int transitionId = TypedArrayUtils.getNamedResourceId(a, parser, "transition", 311 Styleable.TransitionManager.TRANSITION, -1); 312 int fromId = TypedArrayUtils.getNamedResourceId(a, parser, "fromScene", 313 Styleable.TransitionManager.FROM_SCENE, -1); 314 Scene fromScene = (fromId < 0) ? null : Scene.getSceneForLayout(sceneRoot, fromId, 315 mContext); 316 int toId = TypedArrayUtils.getNamedResourceId(a, parser, "toScene", 317 Styleable.TransitionManager.TO_SCENE, -1); 318 Scene toScene = (toId < 0) ? null : Scene.getSceneForLayout(sceneRoot, toId, mContext); 319 320 if (transitionId >= 0) { 321 Transition transition = inflateTransition(transitionId); 322 if (transition != null) { 323 if (toScene == null) { 324 throw new RuntimeException("No toScene for transition ID " + transitionId); 325 } 326 if (fromScene == null) { 327 transitionManager.setTransition(toScene, transition); 328 } else { 329 transitionManager.setTransition(fromScene, toScene, transition); 330 } 331 } 332 } 333 a.recycle(); 334 } 335 336} 337