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