TransitionInflater.java revision 125578a8637a9ad5e7430d16b9fc0096a8b596d7
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 ("changeTransform".equals(name)) {
162                transition = new ChangeTransform();
163                newTransition = true;
164            } else if ("changeClipBounds".equals(name)) {
165                transition = new ChangeClipBounds();
166                newTransition = true;
167            } else if ("autoTransition".equals(name)) {
168                transition = new AutoTransition();
169                newTransition = true;
170            } else if ("recolor".equals(name)) {
171                transition = new Recolor();
172                newTransition = true;
173            } else if ("transitionSet".equals(name)) {
174                transition = new TransitionSet();
175                TypedArray a = mContext.obtainStyledAttributes(attrs,
176                        com.android.internal.R.styleable.TransitionSet);
177                int ordering = a.getInt(
178                        com.android.internal.R.styleable.TransitionSet_transitionOrdering,
179                        TransitionSet.ORDERING_TOGETHER);
180                ((TransitionSet) transition).setOrdering(ordering);
181                createTransitionFromXml(parser, attrs, ((TransitionSet) transition));
182                a.recycle();
183                newTransition = true;
184            } else if ("targets".equals(name)) {
185                if (parser.getDepth() - 1 > depth && transition != null) {
186                    // We're inside the child tag - add targets to the child
187                    getTargetIds(parser, attrs, transition);
188                } else if (parser.getDepth() - 1 == depth && transitionSet != null) {
189                    // add targets to the set
190                    getTargetIds(parser, attrs, transitionSet);
191                }
192            }
193            if (transition != null || "targets".equals(name)) {
194                if (newTransition) {
195                    loadTransition(transition, attrs);
196                    if (transitionSet != null) {
197                        transitionSet.addTransition(transition);
198                    }
199                }
200            } else {
201                throw new RuntimeException("Unknown scene name: " + parser.getName());
202            }
203        }
204
205        return transition;
206    }
207
208    private Slide createSlideTransition(AttributeSet attrs) {
209        TypedArray a = mContext.obtainStyledAttributes(attrs,
210                com.android.internal.R.styleable.Slide);
211        int edge = a.getInt(com.android.internal.R.styleable.Slide_slideEdge, Gravity.BOTTOM);
212        Slide slide = new Slide(edge);
213        a.recycle();
214        return slide;
215    }
216
217    private void getTargetIds(XmlPullParser parser,
218            AttributeSet attrs, Transition transition) throws XmlPullParserException, IOException {
219
220        // Make sure we are on a start tag.
221        int type;
222        int depth = parser.getDepth();
223
224        while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
225                && type != XmlPullParser.END_DOCUMENT) {
226
227            if (type != XmlPullParser.START_TAG) {
228                continue;
229            }
230
231            String  name = parser.getName();
232            if (name.equals("target")) {
233                TypedArray a = mContext.obtainStyledAttributes(attrs,
234                        com.android.internal.R.styleable.TransitionTarget);
235                int id = a.getResourceId(
236                        com.android.internal.R.styleable.TransitionTarget_targetId, -1);
237                String transitionName;
238                if (id >= 0) {
239                    transition.addTarget(id);
240                } else if ((id = a.getResourceId(
241                        com.android.internal.R.styleable.TransitionTarget_excludeId, -1)) >= 0) {
242                    transition.excludeTarget(id, true);
243                } else if ((transitionName = a.getString(
244                            com.android.internal.R.styleable.TransitionTarget_targetName))
245                        != null) {
246                    transition.addTarget(transitionName);
247                } else if ((transitionName = a.getString(
248                        com.android.internal.R.styleable.TransitionTarget_excludeName))
249                        != null) {
250                    transition.excludeTarget(transitionName, true);
251                } else {
252                    String className = a.getString(
253                            com.android.internal.R.styleable.TransitionTarget_excludeClass);
254                    try {
255                        if (className != null) {
256                            Class clazz = Class.forName(className);
257                            transition.excludeTarget(clazz, true);
258                        } else if ((className = a.getString(
259                                com.android.internal.R.styleable.TransitionTarget_targetClass))
260                                != null) {
261                            Class clazz = Class.forName(className);
262                            transition.addTarget(clazz);
263                        }
264                    } catch (ClassNotFoundException e) {
265                        throw new RuntimeException("Could not create " + className, e);
266                    }
267                }
268            } else {
269                throw new RuntimeException("Unknown scene name: " + parser.getName());
270            }
271        }
272    }
273
274    private int[] parseMatchOrder(String matchOrderString) {
275        StringTokenizer st = new StringTokenizer(matchOrderString, ",");
276        int matches[] = new int[st.countTokens()];
277        int index = 0;
278        while (st.hasMoreTokens()) {
279            String token = st.nextToken().trim();
280            if (MATCH_ID.equalsIgnoreCase(token)) {
281                matches[index] = Transition.MATCH_ID;
282            } else if (MATCH_INSTANCE.equalsIgnoreCase(token)) {
283                matches[index] = Transition.MATCH_INSTANCE;
284            } else if (MATCH_NAME.equalsIgnoreCase(token)) {
285                matches[index] = Transition.MATCH_NAME;
286            } else if (MATCH_VIEW_NAME.equalsIgnoreCase(token)) {
287                matches[index] = Transition.MATCH_NAME;
288            } else if (MATCH_ITEM_ID.equalsIgnoreCase(token)) {
289                matches[index] = Transition.MATCH_ITEM_ID;
290            } else if (token.isEmpty()) {
291                int[] smallerMatches = new int[matches.length - 1];
292                System.arraycopy(matches, 0, smallerMatches, 0, index);
293                matches = smallerMatches;
294                index--;
295            } else {
296                throw new RuntimeException("Unknown match type in matchOrder: '" + token + "'");
297            }
298            index++;
299        }
300        return matches;
301    }
302
303    private Transition loadTransition(Transition transition, AttributeSet attrs)
304            throws Resources.NotFoundException {
305
306        TypedArray a =
307                mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Transition);
308        long duration = a.getInt(com.android.internal.R.styleable.Transition_duration, -1);
309        if (duration >= 0) {
310            transition.setDuration(duration);
311        }
312        long startDelay = a.getInt(com.android.internal.R.styleable.Transition_startDelay, -1);
313        if (startDelay > 0) {
314            transition.setStartDelay(startDelay);
315        }
316        final int resID =
317                a.getResourceId(com.android.internal.R.styleable.Animator_interpolator, 0);
318        if (resID > 0) {
319            transition.setInterpolator(AnimationUtils.loadInterpolator(mContext, resID));
320        }
321        String matchOrder =
322                a.getString(com.android.internal.R.styleable.Transition_matchOrder);
323        if (matchOrder != null) {
324            transition.setMatchOrder(parseMatchOrder(matchOrder));
325        }
326        a.recycle();
327        return transition;
328    }
329
330    //
331    // TransitionManager loading
332    //
333
334    private TransitionManager createTransitionManagerFromXml(XmlPullParser parser,
335            AttributeSet attrs, ViewGroup sceneRoot) throws XmlPullParserException, IOException {
336
337        // Make sure we are on a start tag.
338        int type;
339        int depth = parser.getDepth();
340        TransitionManager transitionManager = null;
341
342        while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
343                && type != XmlPullParser.END_DOCUMENT) {
344
345            if (type != XmlPullParser.START_TAG) {
346                continue;
347            }
348
349            String  name = parser.getName();
350            if (name.equals("transitionManager")) {
351                transitionManager = new TransitionManager();
352            } else if (name.equals("transition") && (transitionManager != null)) {
353                loadTransition(attrs, sceneRoot, transitionManager);
354            } else {
355                throw new RuntimeException("Unknown scene name: " + parser.getName());
356            }
357        }
358        return transitionManager;
359    }
360
361    private void loadTransition(AttributeSet attrs, ViewGroup sceneRoot,
362            TransitionManager transitionManager) throws Resources.NotFoundException {
363
364        TypedArray a = mContext.obtainStyledAttributes(attrs,
365                com.android.internal.R.styleable.TransitionManager);
366        int transitionId = a.getResourceId(
367                com.android.internal.R.styleable.TransitionManager_transition, -1);
368        int fromId = a.getResourceId(
369                com.android.internal.R.styleable.TransitionManager_fromScene, -1);
370        Scene fromScene = (fromId < 0) ? null: Scene.getSceneForLayout(sceneRoot, fromId, mContext);
371        int toId = a.getResourceId(
372                com.android.internal.R.styleable.TransitionManager_toScene, -1);
373        Scene toScene = (toId < 0) ? null : Scene.getSceneForLayout(sceneRoot, toId, mContext);
374
375        if (transitionId >= 0) {
376            Transition transition = inflateTransition(transitionId);
377            if (transition != null) {
378                if (toScene == null) {
379                    throw new RuntimeException("No toScene for transition ID " + transitionId);
380                }
381                if (fromScene == null) {
382                    transitionManager.setTransition(toScene, transition);
383                } else {
384                    transitionManager.setTransition(fromScene, toScene, transition);
385                }
386            }
387        }
388        a.recycle();
389    }
390}
391