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