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