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