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