1/*
2 * Copyright (C) 2016 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 static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.animation.TimeInterpolator;
22import android.content.Context;
23import android.content.res.TypedArray;
24import android.content.res.XmlResourceParser;
25import android.support.annotation.IdRes;
26import android.support.annotation.NonNull;
27import android.support.annotation.Nullable;
28import android.support.annotation.RestrictTo;
29import android.support.v4.content.res.TypedArrayUtils;
30import android.util.AndroidRuntimeException;
31import android.util.AttributeSet;
32import android.view.View;
33import android.view.ViewGroup;
34
35import java.util.ArrayList;
36
37/**
38 * A TransitionSet is a parent of child transitions (including other
39 * TransitionSets). Using TransitionSets enables more complex
40 * choreography of transitions, where some sets play {@link #ORDERING_TOGETHER} and
41 * others play {@link #ORDERING_SEQUENTIAL}. For example, {@link AutoTransition}
42 * uses a TransitionSet to sequentially play a Fade(Fade.OUT), followed by
43 * a {@link ChangeBounds}, followed by a Fade(Fade.OUT) transition.
44 *
45 * <p>A TransitionSet can be described in a resource file by using the
46 * tag <code>transitionSet</code>, along with the standard
47 * attributes of {@code TransitionSet} and {@link Transition}. Child transitions of the
48 * TransitionSet object can be loaded by adding those child tags inside the
49 * enclosing <code>transitionSet</code> tag. For example, the following xml
50 * describes a TransitionSet that plays a Fade and then a ChangeBounds
51 * transition on the affected view targets:</p>
52 * <pre>
53 *     &lt;transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
54 *             android:ordering="sequential"&gt;
55 *         &lt;fade/&gt;
56 *         &lt;changeBounds/&gt;
57 *     &lt;/transitionSet&gt;
58 * </pre>
59 */
60public class TransitionSet extends Transition {
61
62    private ArrayList<Transition> mTransitions = new ArrayList<>();
63    private boolean mPlayTogether = true;
64    private int mCurrentListeners;
65    private boolean mStarted = false;
66
67    /**
68     * A flag used to indicate that the child transitions of this set
69     * should all start at the same time.
70     */
71    public static final int ORDERING_TOGETHER = 0;
72
73    /**
74     * A flag used to indicate that the child transitions of this set should
75     * play in sequence; when one child transition ends, the next child
76     * transition begins. Note that a transition does not end until all
77     * instances of it (which are playing on all applicable targets of the
78     * transition) end.
79     */
80    public static final int ORDERING_SEQUENTIAL = 1;
81
82    /**
83     * Constructs an empty transition set. Add child transitions to the
84     * set by calling {@link #addTransition(Transition)} )}. By default,
85     * child transitions will play {@link #ORDERING_TOGETHER together}.
86     */
87    public TransitionSet() {
88    }
89
90    public TransitionSet(Context context, AttributeSet attrs) {
91        super(context, attrs);
92        TypedArray a = context.obtainStyledAttributes(attrs, Styleable.TRANSITION_SET);
93        int ordering = TypedArrayUtils.getNamedInt(a, (XmlResourceParser) attrs,
94                "transitionOrdering", Styleable.TransitionSet.TRANSITION_ORDERING,
95                TransitionSet.ORDERING_TOGETHER);
96        setOrdering(ordering);
97        a.recycle();
98    }
99
100    /**
101     * Sets the play order of this set's child transitions.
102     *
103     * @param ordering {@link #ORDERING_TOGETHER} to play this set's child
104     *                 transitions together, {@link #ORDERING_SEQUENTIAL} to play the child
105     *                 transitions in sequence.
106     * @return This transitionSet object.
107     */
108    @NonNull
109    public TransitionSet setOrdering(int ordering) {
110        switch (ordering) {
111            case ORDERING_SEQUENTIAL:
112                mPlayTogether = false;
113                break;
114            case ORDERING_TOGETHER:
115                mPlayTogether = true;
116                break;
117            default:
118                throw new AndroidRuntimeException("Invalid parameter for TransitionSet "
119                        + "ordering: " + ordering);
120        }
121        return this;
122    }
123
124    /**
125     * Returns the ordering of this TransitionSet. By default, the value is
126     * {@link #ORDERING_TOGETHER}.
127     *
128     * @return {@link #ORDERING_TOGETHER} if child transitions will play at the same
129     * time, {@link #ORDERING_SEQUENTIAL} if they will play in sequence.
130     * @see #setOrdering(int)
131     */
132    public int getOrdering() {
133        return mPlayTogether ? ORDERING_TOGETHER : ORDERING_SEQUENTIAL;
134    }
135
136    /**
137     * Adds child transition to this set. The order in which this child transition
138     * is added relative to other child transitions that are added, in addition to
139     * the {@link #getOrdering() ordering} property, determines the
140     * order in which the transitions are started.
141     *
142     * <p>If this transitionSet has a {@link #getDuration() duration} set on it, the
143     * child transition will inherit that duration. Transitions are assumed to have
144     * a maximum of one transitionSet parent.</p>
145     *
146     * @param transition A non-null child transition to be added to this set.
147     * @return This transitionSet object.
148     */
149    @NonNull
150    public TransitionSet addTransition(@NonNull Transition transition) {
151        mTransitions.add(transition);
152        transition.mParent = this;
153        if (mDuration >= 0) {
154            transition.setDuration(mDuration);
155        }
156        return this;
157    }
158
159    /**
160     * Returns the number of child transitions in the TransitionSet.
161     *
162     * @return The number of child transitions in the TransitionSet.
163     * @see #addTransition(Transition)
164     * @see #getTransitionAt(int)
165     */
166    public int getTransitionCount() {
167        return mTransitions.size();
168    }
169
170    /**
171     * Returns the child Transition at the specified position in the TransitionSet.
172     *
173     * @param index The position of the Transition to retrieve.
174     * @see #addTransition(Transition)
175     * @see #getTransitionCount()
176     */
177    public Transition getTransitionAt(int index) {
178        if (index < 0 || index >= mTransitions.size()) {
179            return null;
180        }
181        return mTransitions.get(index);
182    }
183
184    /**
185     * Setting a non-negative duration on a TransitionSet causes all of the child
186     * transitions (current and future) to inherit this duration.
187     *
188     * @param duration The length of the animation, in milliseconds.
189     * @return This transitionSet object.
190     */
191    @NonNull
192    @Override
193    public TransitionSet setDuration(long duration) {
194        super.setDuration(duration);
195        if (mDuration >= 0) {
196            int numTransitions = mTransitions.size();
197            for (int i = 0; i < numTransitions; ++i) {
198                mTransitions.get(i).setDuration(duration);
199            }
200        }
201        return this;
202    }
203
204    @NonNull
205    @Override
206    public TransitionSet setStartDelay(long startDelay) {
207        return (TransitionSet) super.setStartDelay(startDelay);
208    }
209
210    @NonNull
211    @Override
212    public TransitionSet setInterpolator(@Nullable TimeInterpolator interpolator) {
213        return (TransitionSet) super.setInterpolator(interpolator);
214    }
215
216    @NonNull
217    @Override
218    public TransitionSet addTarget(@NonNull View target) {
219        for (int i = 0; i < mTransitions.size(); i++) {
220            mTransitions.get(i).addTarget(target);
221        }
222        return (TransitionSet) super.addTarget(target);
223    }
224
225    @NonNull
226    @Override
227    public TransitionSet addTarget(@IdRes int targetId) {
228        for (int i = 0; i < mTransitions.size(); i++) {
229            mTransitions.get(i).addTarget(targetId);
230        }
231        return (TransitionSet) super.addTarget(targetId);
232    }
233
234    @NonNull
235    @Override
236    public TransitionSet addTarget(@NonNull String targetName) {
237        for (int i = 0; i < mTransitions.size(); i++) {
238            mTransitions.get(i).addTarget(targetName);
239        }
240        return (TransitionSet) super.addTarget(targetName);
241    }
242
243    @NonNull
244    @Override
245    public TransitionSet addTarget(@NonNull Class targetType) {
246        for (int i = 0; i < mTransitions.size(); i++) {
247            mTransitions.get(i).addTarget(targetType);
248        }
249        return (TransitionSet) super.addTarget(targetType);
250    }
251
252    @NonNull
253    @Override
254    public TransitionSet addListener(@NonNull TransitionListener listener) {
255        return (TransitionSet) super.addListener(listener);
256    }
257
258    @NonNull
259    @Override
260    public TransitionSet removeTarget(@IdRes int targetId) {
261        for (int i = 0; i < mTransitions.size(); i++) {
262            mTransitions.get(i).removeTarget(targetId);
263        }
264        return (TransitionSet) super.removeTarget(targetId);
265    }
266
267    @NonNull
268    @Override
269    public TransitionSet removeTarget(@NonNull View target) {
270        for (int i = 0; i < mTransitions.size(); i++) {
271            mTransitions.get(i).removeTarget(target);
272        }
273        return (TransitionSet) super.removeTarget(target);
274    }
275
276    @NonNull
277    @Override
278    public TransitionSet removeTarget(@NonNull Class target) {
279        for (int i = 0; i < mTransitions.size(); i++) {
280            mTransitions.get(i).removeTarget(target);
281        }
282        return (TransitionSet) super.removeTarget(target);
283    }
284
285    @NonNull
286    @Override
287    public TransitionSet removeTarget(@NonNull String target) {
288        for (int i = 0; i < mTransitions.size(); i++) {
289            mTransitions.get(i).removeTarget(target);
290        }
291        return (TransitionSet) super.removeTarget(target);
292    }
293
294    @NonNull
295    @Override
296    public Transition excludeTarget(@NonNull View target, boolean exclude) {
297        for (int i = 0; i < mTransitions.size(); i++) {
298            mTransitions.get(i).excludeTarget(target, exclude);
299        }
300        return super.excludeTarget(target, exclude);
301    }
302
303    @NonNull
304    @Override
305    public Transition excludeTarget(@NonNull String targetName, boolean exclude) {
306        for (int i = 0; i < mTransitions.size(); i++) {
307            mTransitions.get(i).excludeTarget(targetName, exclude);
308        }
309        return super.excludeTarget(targetName, exclude);
310    }
311
312    @NonNull
313    @Override
314    public Transition excludeTarget(int targetId, boolean exclude) {
315        for (int i = 0; i < mTransitions.size(); i++) {
316            mTransitions.get(i).excludeTarget(targetId, exclude);
317        }
318        return super.excludeTarget(targetId, exclude);
319    }
320
321    @NonNull
322    @Override
323    public Transition excludeTarget(@NonNull Class type, boolean exclude) {
324        for (int i = 0; i < mTransitions.size(); i++) {
325            mTransitions.get(i).excludeTarget(type, exclude);
326        }
327        return super.excludeTarget(type, exclude);
328    }
329
330    @NonNull
331    @Override
332    public TransitionSet removeListener(@NonNull TransitionListener listener) {
333        return (TransitionSet) super.removeListener(listener);
334    }
335
336    @Override
337    public void setPathMotion(PathMotion pathMotion) {
338        super.setPathMotion(pathMotion);
339        for (int i = 0; i < mTransitions.size(); i++) {
340            mTransitions.get(i).setPathMotion(pathMotion);
341        }
342    }
343
344    /**
345     * Removes the specified child transition from this set.
346     *
347     * @param transition The transition to be removed.
348     * @return This transitionSet object.
349     */
350    @NonNull
351    public TransitionSet removeTransition(@NonNull Transition transition) {
352        mTransitions.remove(transition);
353        transition.mParent = null;
354        return this;
355    }
356
357    /**
358     * Sets up listeners for each of the child transitions. This is used to
359     * determine when this transition set is finished (all child transitions
360     * must finish first).
361     */
362    private void setupStartEndListeners() {
363        TransitionSetListener listener = new TransitionSetListener(this);
364        for (Transition childTransition : mTransitions) {
365            childTransition.addListener(listener);
366        }
367        mCurrentListeners = mTransitions.size();
368    }
369
370    /**
371     * This listener is used to detect when all child transitions are done, at
372     * which point this transition set is also done.
373     */
374    static class TransitionSetListener extends TransitionListenerAdapter {
375
376        TransitionSet mTransitionSet;
377
378        TransitionSetListener(TransitionSet transitionSet) {
379            mTransitionSet = transitionSet;
380        }
381
382        @Override
383        public void onTransitionStart(@NonNull Transition transition) {
384            if (!mTransitionSet.mStarted) {
385                mTransitionSet.start();
386                mTransitionSet.mStarted = true;
387            }
388        }
389
390        @Override
391        public void onTransitionEnd(@NonNull Transition transition) {
392            --mTransitionSet.mCurrentListeners;
393            if (mTransitionSet.mCurrentListeners == 0) {
394                // All child trans
395                mTransitionSet.mStarted = false;
396                mTransitionSet.end();
397            }
398            transition.removeListener(this);
399        }
400
401    }
402
403    /**
404     * @hide
405     */
406    @RestrictTo(LIBRARY_GROUP)
407    @Override
408    protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues,
409            TransitionValuesMaps endValues, ArrayList<TransitionValues> startValuesList,
410            ArrayList<TransitionValues> endValuesList) {
411        long startDelay = getStartDelay();
412        int numTransitions = mTransitions.size();
413        for (int i = 0; i < numTransitions; i++) {
414            Transition childTransition = mTransitions.get(i);
415            // We only set the start delay on the first transition if we are playing
416            // the transitions sequentially.
417            if (startDelay > 0 && (mPlayTogether || i == 0)) {
418                long childStartDelay = childTransition.getStartDelay();
419                if (childStartDelay > 0) {
420                    childTransition.setStartDelay(startDelay + childStartDelay);
421                } else {
422                    childTransition.setStartDelay(startDelay);
423                }
424            }
425            childTransition.createAnimators(sceneRoot, startValues, endValues, startValuesList,
426                    endValuesList);
427        }
428    }
429
430    /**
431     * @hide
432     */
433    @RestrictTo(LIBRARY_GROUP)
434    @Override
435    protected void runAnimators() {
436        if (mTransitions.isEmpty()) {
437            start();
438            end();
439            return;
440        }
441        setupStartEndListeners();
442        if (!mPlayTogether) {
443            // Setup sequence with listeners
444            // TODO: Need to add listeners in such a way that we can remove them later if canceled
445            for (int i = 1; i < mTransitions.size(); ++i) {
446                Transition previousTransition = mTransitions.get(i - 1);
447                final Transition nextTransition = mTransitions.get(i);
448                previousTransition.addListener(new TransitionListenerAdapter() {
449                    @Override
450                    public void onTransitionEnd(@NonNull Transition transition) {
451                        nextTransition.runAnimators();
452                        transition.removeListener(this);
453                    }
454                });
455            }
456            Transition firstTransition = mTransitions.get(0);
457            if (firstTransition != null) {
458                firstTransition.runAnimators();
459            }
460        } else {
461            for (Transition childTransition : mTransitions) {
462                childTransition.runAnimators();
463            }
464        }
465    }
466
467    @Override
468    public void captureStartValues(@NonNull TransitionValues transitionValues) {
469        if (isValidTarget(transitionValues.view)) {
470            for (Transition childTransition : mTransitions) {
471                if (childTransition.isValidTarget(transitionValues.view)) {
472                    childTransition.captureStartValues(transitionValues);
473                    transitionValues.mTargetedTransitions.add(childTransition);
474                }
475            }
476        }
477    }
478
479    @Override
480    public void captureEndValues(@NonNull TransitionValues transitionValues) {
481        if (isValidTarget(transitionValues.view)) {
482            for (Transition childTransition : mTransitions) {
483                if (childTransition.isValidTarget(transitionValues.view)) {
484                    childTransition.captureEndValues(transitionValues);
485                    transitionValues.mTargetedTransitions.add(childTransition);
486                }
487            }
488        }
489    }
490
491    @Override
492    void capturePropagationValues(TransitionValues transitionValues) {
493        super.capturePropagationValues(transitionValues);
494        int numTransitions = mTransitions.size();
495        for (int i = 0; i < numTransitions; ++i) {
496            mTransitions.get(i).capturePropagationValues(transitionValues);
497        }
498    }
499
500    /** @hide */
501    @RestrictTo(LIBRARY_GROUP)
502    @Override
503    public void pause(View sceneRoot) {
504        super.pause(sceneRoot);
505        int numTransitions = mTransitions.size();
506        for (int i = 0; i < numTransitions; ++i) {
507            mTransitions.get(i).pause(sceneRoot);
508        }
509    }
510
511    /** @hide */
512    @RestrictTo(LIBRARY_GROUP)
513    @Override
514    public void resume(View sceneRoot) {
515        super.resume(sceneRoot);
516        int numTransitions = mTransitions.size();
517        for (int i = 0; i < numTransitions; ++i) {
518            mTransitions.get(i).resume(sceneRoot);
519        }
520    }
521
522    /** @hide */
523    @RestrictTo(LIBRARY_GROUP)
524    @Override
525    protected void cancel() {
526        super.cancel();
527        int numTransitions = mTransitions.size();
528        for (int i = 0; i < numTransitions; ++i) {
529            mTransitions.get(i).cancel();
530        }
531    }
532
533    /** @hide */
534    @RestrictTo(LIBRARY_GROUP)
535    @Override
536    void forceToEnd(ViewGroup sceneRoot) {
537        super.forceToEnd(sceneRoot);
538        int numTransitions = mTransitions.size();
539        for (int i = 0; i < numTransitions; ++i) {
540            mTransitions.get(i).forceToEnd(sceneRoot);
541        }
542    }
543
544    @Override
545    TransitionSet setSceneRoot(ViewGroup sceneRoot) {
546        super.setSceneRoot(sceneRoot);
547        int numTransitions = mTransitions.size();
548        for (int i = 0; i < numTransitions; ++i) {
549            mTransitions.get(i).setSceneRoot(sceneRoot);
550        }
551        return this;
552    }
553
554    @Override
555    void setCanRemoveViews(boolean canRemoveViews) {
556        super.setCanRemoveViews(canRemoveViews);
557        int numTransitions = mTransitions.size();
558        for (int i = 0; i < numTransitions; ++i) {
559            mTransitions.get(i).setCanRemoveViews(canRemoveViews);
560        }
561    }
562
563    @Override
564    public void setEpicenterCallback(EpicenterCallback epicenterCallback) {
565        super.setEpicenterCallback(epicenterCallback);
566        int numTransitions = mTransitions.size();
567        for (int i = 0; i < numTransitions; ++i) {
568            mTransitions.get(i).setEpicenterCallback(epicenterCallback);
569        }
570    }
571
572    @Override
573    String toString(String indent) {
574        String result = super.toString(indent);
575        for (int i = 0; i < mTransitions.size(); ++i) {
576            result += "\n" + mTransitions.get(i).toString(indent + "  ");
577        }
578        return result;
579    }
580
581    @Override
582    public Transition clone() {
583        TransitionSet clone = (TransitionSet) super.clone();
584        clone.mTransitions = new ArrayList<>();
585        int numTransitions = mTransitions.size();
586        for (int i = 0; i < numTransitions; ++i) {
587            clone.addTransition(mTransitions.get(i).clone());
588        }
589        return clone;
590    }
591
592}
593