• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
TransitionInflater(@onNull Context context)49     private TransitionInflater(@NonNull Context context) {
50         mContext = context;
51     }
52 
53     /**
54      * Obtains the TransitionInflater from the given context.
55      */
from(Context context)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      */
inflateTransition(int resource)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      */
inflateTransitionManager(int resource, ViewGroup sceneRoot)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     //
createTransitionFromXml(XmlPullParser parser, AttributeSet attrs, Transition parent)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 
createCustom(AttributeSet attrs, Class expectedType, String tag)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 
getTargetIds(XmlPullParser parser, AttributeSet attrs, Transition transition)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 
createTransitionManagerFromXml(XmlPullParser parser, AttributeSet attrs, ViewGroup sceneRoot)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 
loadTransition(AttributeSet attrs, XmlPullParser parser, ViewGroup sceneRoot, TransitionManager transitionManager)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