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