TransitionInflater.java revision 31e8005e06acf363a0cd92b891d43f79c72dac30
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 android.content.Context;
20import android.content.res.Resources;
21import android.content.res.TypedArray;
22import android.content.res.XmlResourceParser;
23import android.util.AttributeSet;
24import android.util.Xml;
25import android.view.Gravity;
26import android.view.InflateException;
27import android.view.ViewGroup;
28import android.view.animation.AnimationUtils;
29import org.xmlpull.v1.XmlPullParser;
30import org.xmlpull.v1.XmlPullParserException;
31
32import java.io.IOException;
33import java.util.StringTokenizer;
34
35/**
36 * This class inflates scenes and transitions from resource files.
37 *
38 * Information on XML resource descriptions for transitions can be found for
39 * {@link android.R.styleable#Transition}, {@link android.R.styleable#TransitionSet},
40 * {@link android.R.styleable#TransitionTarget}, {@link android.R.styleable#Fade},
41 * and {@link android.R.styleable#TransitionManager}.
42 */
43public class TransitionInflater {
44    private static final String MATCH_INSTANCE = "instance";
45    private static final String MATCH_NAME = "name";
46    /** To be removed before L release */
47    private static final String MATCH_VIEW_NAME = "viewName";
48    private static final String MATCH_ID = "id";
49    private static final String MATCH_ITEM_ID = "itemId";
50
51    private Context mContext;
52
53    private TransitionInflater(Context context) {
54        mContext = context;
55    }
56
57    /**
58     * Obtains the TransitionInflater from the given context.
59     */
60    public static TransitionInflater from(Context context) {
61        return new TransitionInflater(context);
62    }
63
64    /**
65     * Loads a {@link Transition} object from a resource
66     *
67     * @param resource The resource id of the transition to load
68     * @return The loaded Transition object
69     * @throws android.content.res.Resources.NotFoundException when the
70     * transition cannot be loaded
71     */
72    public Transition inflateTransition(int resource) {
73        XmlResourceParser parser =  mContext.getResources().getXml(resource);
74        try {
75            return createTransitionFromXml(parser, Xml.asAttributeSet(parser), null);
76        } catch (XmlPullParserException e) {
77            InflateException ex = new InflateException(e.getMessage());
78            ex.initCause(e);
79            throw ex;
80        } catch (IOException e) {
81            InflateException ex = new InflateException(
82                    parser.getPositionDescription()
83                            + ": " + e.getMessage());
84            ex.initCause(e);
85            throw ex;
86        } finally {
87            parser.close();
88        }
89    }
90
91    /**
92     * Loads a {@link TransitionManager} object from a resource
93     *
94     * @param resource The resource id of the transition manager to load
95     * @return The loaded TransitionManager object
96     * @throws android.content.res.Resources.NotFoundException when the
97     * transition manager cannot be loaded
98     */
99    public TransitionManager inflateTransitionManager(int resource, ViewGroup sceneRoot) {
100        XmlResourceParser parser =  mContext.getResources().getXml(resource);
101        try {
102            return createTransitionManagerFromXml(parser, Xml.asAttributeSet(parser), sceneRoot);
103        } catch (XmlPullParserException e) {
104            InflateException ex = new InflateException(e.getMessage());
105            ex.initCause(e);
106            throw ex;
107        } catch (IOException e) {
108            InflateException ex = new InflateException(
109                    parser.getPositionDescription()
110                            + ": " + e.getMessage());
111            ex.initCause(e);
112            throw ex;
113        } finally {
114            parser.close();
115        }
116    }
117
118    //
119    // Transition loading
120    //
121
122    private Transition createTransitionFromXml(XmlPullParser parser,
123            AttributeSet attrs, TransitionSet transitionSet)
124            throws XmlPullParserException, IOException {
125
126        Transition transition = null;
127
128        // Make sure we are on a start tag.
129        int type;
130        int depth = parser.getDepth();
131
132        while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
133                && type != XmlPullParser.END_DOCUMENT) {
134
135            boolean newTransition = false;
136
137            if (type != XmlPullParser.START_TAG) {
138                continue;
139            }
140
141            String  name = parser.getName();
142            if ("fade".equals(name)) {
143                TypedArray a = mContext.obtainStyledAttributes(attrs,
144                        com.android.internal.R.styleable.Fade);
145                int fadingMode = a.getInt(com.android.internal.R.styleable.Fade_fadingMode,
146                        Fade.IN | Fade.OUT);
147                transition = new Fade(fadingMode);
148                newTransition = true;
149            } else if ("changeBounds".equals(name)) {
150                transition = new ChangeBounds();
151                newTransition = true;
152            } else if ("slide".equals(name)) {
153                transition = createSlideTransition(attrs);
154                newTransition = true;
155            } else if ("explode".equals(name)) {
156                transition = new Explode();
157                newTransition = true;
158            } else if ("moveImage".equals(name)) {
159                transition = new MoveImage();
160                newTransition = true;
161            } else if ("changeImageTransform".equals(name)) {
162                transition = new ChangeImageTransform();
163                newTransition = true;
164            } else if ("changeTransform".equals(name)) {
165                transition = new ChangeTransform();
166                newTransition = true;
167            } else if ("changeClipBounds".equals(name)) {
168                transition = new ChangeClipBounds();
169                newTransition = true;
170            } else if ("autoTransition".equals(name)) {
171                transition = new AutoTransition();
172                newTransition = true;
173            } else if ("recolor".equals(name)) {
174                transition = new Recolor();
175                newTransition = true;
176            } else if ("transitionSet".equals(name)) {
177                transition = new TransitionSet();
178                TypedArray a = mContext.obtainStyledAttributes(attrs,
179                        com.android.internal.R.styleable.TransitionSet);
180                int ordering = a.getInt(
181                        com.android.internal.R.styleable.TransitionSet_transitionOrdering,
182                        TransitionSet.ORDERING_TOGETHER);
183                ((TransitionSet) transition).setOrdering(ordering);
184                createTransitionFromXml(parser, attrs, ((TransitionSet) transition));
185                a.recycle();
186                newTransition = true;
187            } else if ("transition".equals(name)) {
188                transition = createCustomTransition(attrs);
189            } else if ("targets".equals(name)) {
190                if (parser.getDepth() - 1 > depth && transition != null) {
191                    // We're inside the child tag - add targets to the child
192                    getTargetIds(parser, attrs, transition);
193                } else if (parser.getDepth() - 1 == depth && transitionSet != null) {
194                    // add targets to the set
195                    getTargetIds(parser, attrs, transitionSet);
196                }
197            }
198            if (transition != null || "targets".equals(name)) {
199                if (newTransition) {
200                    loadTransition(transition, attrs);
201                    if (transitionSet != null) {
202                        transitionSet.addTransition(transition);
203                    }
204                }
205            } else {
206                throw new RuntimeException("Unknown scene name: " + parser.getName());
207            }
208        }
209
210        return transition;
211    }
212
213    private Transition createCustomTransition(AttributeSet attrs) {
214        String className = attrs.getAttributeValue(null, "class");
215
216        if (className == null) {
217            throw new RuntimeException("transition tag must have a 'class' attribute");
218        }
219
220        try {
221            Class c = Class.forName(className);
222            if (!Transition.class.isAssignableFrom(c)) {
223                throw new RuntimeException("transition class must be a Transition: " + className);
224            }
225            return (Transition) c.newInstance();
226        } catch (InstantiationException e) {
227            throw new RuntimeException("Could not instantiate transition class", e);
228        } catch (IllegalAccessException e) {
229            throw new RuntimeException("Could not access default constructor for transition class "
230                    + className, e);
231        } catch (ClassNotFoundException e) {
232            throw new RuntimeException("Could not find transition class " + className, e);
233        }
234    }
235
236    private Slide createSlideTransition(AttributeSet attrs) {
237        TypedArray a = mContext.obtainStyledAttributes(attrs,
238                com.android.internal.R.styleable.Slide);
239        int edge = a.getInt(com.android.internal.R.styleable.Slide_slideEdge, Gravity.BOTTOM);
240        Slide slide = new Slide(edge);
241        a.recycle();
242        return slide;
243    }
244
245    private void getTargetIds(XmlPullParser parser,
246            AttributeSet attrs, Transition transition) throws XmlPullParserException, IOException {
247
248        // Make sure we are on a start tag.
249        int type;
250        int depth = parser.getDepth();
251
252        while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
253                && type != XmlPullParser.END_DOCUMENT) {
254
255            if (type != XmlPullParser.START_TAG) {
256                continue;
257            }
258
259            String  name = parser.getName();
260            if (name.equals("target")) {
261                TypedArray a = mContext.obtainStyledAttributes(attrs,
262                        com.android.internal.R.styleable.TransitionTarget);
263                int id = a.getResourceId(
264                        com.android.internal.R.styleable.TransitionTarget_targetId, -1);
265                String transitionName;
266                if (id >= 0) {
267                    transition.addTarget(id);
268                } else if ((id = a.getResourceId(
269                        com.android.internal.R.styleable.TransitionTarget_excludeId, -1)) >= 0) {
270                    transition.excludeTarget(id, true);
271                } else if ((transitionName = a.getString(
272                            com.android.internal.R.styleable.TransitionTarget_targetName))
273                        != null) {
274                    transition.addTarget(transitionName);
275                } else if ((transitionName = a.getString(
276                        com.android.internal.R.styleable.TransitionTarget_excludeName))
277                        != null) {
278                    transition.excludeTarget(transitionName, true);
279                } else {
280                    String className = a.getString(
281                            com.android.internal.R.styleable.TransitionTarget_excludeClass);
282                    try {
283                        if (className != null) {
284                            Class clazz = Class.forName(className);
285                            transition.excludeTarget(clazz, true);
286                        } else if ((className = a.getString(
287                                com.android.internal.R.styleable.TransitionTarget_targetClass))
288                                != null) {
289                            Class clazz = Class.forName(className);
290                            transition.addTarget(clazz);
291                        }
292                    } catch (ClassNotFoundException e) {
293                        throw new RuntimeException("Could not create " + className, e);
294                    }
295                }
296            } else {
297                throw new RuntimeException("Unknown scene name: " + parser.getName());
298            }
299        }
300    }
301
302    private int[] parseMatchOrder(String matchOrderString) {
303        StringTokenizer st = new StringTokenizer(matchOrderString, ",");
304        int matches[] = new int[st.countTokens()];
305        int index = 0;
306        while (st.hasMoreTokens()) {
307            String token = st.nextToken().trim();
308            if (MATCH_ID.equalsIgnoreCase(token)) {
309                matches[index] = Transition.MATCH_ID;
310            } else if (MATCH_INSTANCE.equalsIgnoreCase(token)) {
311                matches[index] = Transition.MATCH_INSTANCE;
312            } else if (MATCH_NAME.equalsIgnoreCase(token)) {
313                matches[index] = Transition.MATCH_NAME;
314            } else if (MATCH_VIEW_NAME.equalsIgnoreCase(token)) {
315                matches[index] = Transition.MATCH_NAME;
316            } else if (MATCH_ITEM_ID.equalsIgnoreCase(token)) {
317                matches[index] = Transition.MATCH_ITEM_ID;
318            } else if (token.isEmpty()) {
319                int[] smallerMatches = new int[matches.length - 1];
320                System.arraycopy(matches, 0, smallerMatches, 0, index);
321                matches = smallerMatches;
322                index--;
323            } else {
324                throw new RuntimeException("Unknown match type in matchOrder: '" + token + "'");
325            }
326            index++;
327        }
328        return matches;
329    }
330
331    private Transition loadTransition(Transition transition, AttributeSet attrs)
332            throws Resources.NotFoundException {
333
334        TypedArray a =
335                mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Transition);
336        long duration = a.getInt(com.android.internal.R.styleable.Transition_duration, -1);
337        if (duration >= 0) {
338            transition.setDuration(duration);
339        }
340        long startDelay = a.getInt(com.android.internal.R.styleable.Transition_startDelay, -1);
341        if (startDelay > 0) {
342            transition.setStartDelay(startDelay);
343        }
344        final int resID =
345                a.getResourceId(com.android.internal.R.styleable.Animator_interpolator, 0);
346        if (resID > 0) {
347            transition.setInterpolator(AnimationUtils.loadInterpolator(mContext, resID));
348        }
349        String matchOrder =
350                a.getString(com.android.internal.R.styleable.Transition_matchOrder);
351        if (matchOrder != null) {
352            transition.setMatchOrder(parseMatchOrder(matchOrder));
353        }
354        a.recycle();
355        if (transition instanceof Visibility) {
356            a = mContext.obtainStyledAttributes(attrs,
357                    com.android.internal.R.styleable.VisibilityTransition);
358            int mode = a.getInt(
359                    com.android.internal.R.styleable.VisibilityTransition_visibilityMode, 0);
360            a.recycle();
361            if (mode != 0) {
362                ((Visibility)transition).setMode(mode);
363            }
364        }
365        return transition;
366    }
367
368    //
369    // TransitionManager loading
370    //
371
372    private TransitionManager createTransitionManagerFromXml(XmlPullParser parser,
373            AttributeSet attrs, ViewGroup sceneRoot) throws XmlPullParserException, IOException {
374
375        // Make sure we are on a start tag.
376        int type;
377        int depth = parser.getDepth();
378        TransitionManager transitionManager = null;
379
380        while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
381                && type != XmlPullParser.END_DOCUMENT) {
382
383            if (type != XmlPullParser.START_TAG) {
384                continue;
385            }
386
387            String  name = parser.getName();
388            if (name.equals("transitionManager")) {
389                transitionManager = new TransitionManager();
390            } else if (name.equals("transition") && (transitionManager != null)) {
391                loadTransition(attrs, sceneRoot, transitionManager);
392            } else {
393                throw new RuntimeException("Unknown scene name: " + parser.getName());
394            }
395        }
396        return transitionManager;
397    }
398
399    private void loadTransition(AttributeSet attrs, ViewGroup sceneRoot,
400            TransitionManager transitionManager) throws Resources.NotFoundException {
401
402        TypedArray a = mContext.obtainStyledAttributes(attrs,
403                com.android.internal.R.styleable.TransitionManager);
404        int transitionId = a.getResourceId(
405                com.android.internal.R.styleable.TransitionManager_transition, -1);
406        int fromId = a.getResourceId(
407                com.android.internal.R.styleable.TransitionManager_fromScene, -1);
408        Scene fromScene = (fromId < 0) ? null: Scene.getSceneForLayout(sceneRoot, fromId, mContext);
409        int toId = a.getResourceId(
410                com.android.internal.R.styleable.TransitionManager_toScene, -1);
411        Scene toScene = (toId < 0) ? null : Scene.getSceneForLayout(sceneRoot, toId, mContext);
412
413        if (transitionId >= 0) {
414            Transition transition = inflateTransition(transitionId);
415            if (transition != null) {
416                if (toScene == null) {
417                    throw new RuntimeException("No toScene for transition ID " + transitionId);
418                }
419                if (fromScene == null) {
420                    transitionManager.setTransition(toScene, transition);
421                } else {
422                    transitionManager.setTransition(fromScene, toScene, transition);
423                }
424            }
425        }
426        a.recycle();
427    }
428}
429