FragmentTransitionCompat21.java revision 434607fedf2615b20b5669f240b46cb09a167caf
1/*
2 * Copyright (C) 2014 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.v4.app;
18
19import android.graphics.Rect;
20import android.support.annotation.RequiresApi;
21import android.transition.Transition;
22import android.transition.TransitionManager;
23import android.transition.TransitionSet;
24import android.view.View;
25import android.view.ViewGroup;
26
27import java.util.ArrayList;
28import java.util.List;
29import java.util.Map;
30
31@RequiresApi(21)
32class FragmentTransitionCompat21 {
33
34    /**
35     * Returns a clone of a transition or null if it is null
36     */
37    public static Object cloneTransition(Object transition) {
38        Transition copy = null;
39        if (transition != null) {
40            copy = ((Transition) transition).clone();
41        }
42        return copy;
43    }
44
45    /**
46     * Wraps a transition in a TransitionSet and returns the set. If transition is null, null is
47     * returned.
48     */
49    public static Object wrapTransitionInSet(Object transition) {
50        if (transition == null) {
51            return null;
52        }
53        TransitionSet transitionSet = new TransitionSet();
54        transitionSet.addTransition((Transition) transition);
55        return transitionSet;
56    }
57
58    /**
59     * Finds all children of the shared elements and sets the wrapping TransitionSet
60     * targets to point to those. It also limits transitions that have no targets to the
61     * specific shared elements. This allows developers to target child views of the
62     * shared elements specifically, but this doesn't happen by default.
63     */
64    public static void setSharedElementTargets(Object transitionObj,
65            View nonExistentView, ArrayList<View> sharedViews) {
66        TransitionSet transition = (TransitionSet) transitionObj;
67        final List<View> views = transition.getTargets();
68        views.clear();
69        final int count = sharedViews.size();
70        for (int i = 0; i < count; i++) {
71            final View view = sharedViews.get(i);
72            bfsAddViewChildren(views, view);
73        }
74        views.add(nonExistentView);
75        sharedViews.add(nonExistentView);
76        addTargets(transition, sharedViews);
77    }
78
79    /**
80     * Uses a breadth-first scheme to add startView and all of its children to views.
81     * It won't add a child if it is already in views.
82     */
83    private static void bfsAddViewChildren(final List<View> views, final View startView) {
84        final int startIndex = views.size();
85        if (containedBeforeIndex(views, startView, startIndex)) {
86            return; // This child is already in the list, so all its children are also.
87        }
88        views.add(startView);
89        for (int index = startIndex; index < views.size(); index++) {
90            final View view = views.get(index);
91            if (view instanceof ViewGroup) {
92                ViewGroup viewGroup = (ViewGroup) view;
93                final int childCount =  viewGroup.getChildCount();
94                for (int childIndex = 0; childIndex < childCount; childIndex++) {
95                    final View child = viewGroup.getChildAt(childIndex);
96                    if (!containedBeforeIndex(views, child, startIndex)) {
97                        views.add(child);
98                    }
99                }
100            }
101        }
102    }
103
104    /**
105     * Does a linear search through views for view, limited to maxIndex.
106     */
107    private static boolean containedBeforeIndex(final List<View> views, final View view,
108            final int maxIndex) {
109        for (int i = 0; i < maxIndex; i++) {
110            if (views.get(i) == view) {
111                return true;
112            }
113        }
114        return false;
115    }
116
117    /**
118     * Sets a transition epicenter to the rectangle of a given View.
119     */
120    public static void setEpicenter(Object transitionObj, View view) {
121        if (view != null) {
122            Transition transition = (Transition) transitionObj;
123            final Rect epicenter = new Rect();
124            getBoundsOnScreen(view, epicenter);
125
126            transition.setEpicenterCallback(new Transition.EpicenterCallback() {
127                @Override
128                public Rect onGetEpicenter(Transition transition) {
129                    return epicenter;
130                }
131            });
132        }
133    }
134
135    /**
136     * Replacement for view.getBoundsOnScreen because that is not public. This returns a rect
137     * containing the bounds relative to the screen that the view is in.
138     */
139    public static void getBoundsOnScreen(View view, Rect epicenter) {
140        int[] loc = new int[2];
141        view.getLocationOnScreen(loc);
142        epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
143    }
144
145    /**
146     * This method adds views as targets to the transition, but only if the transition
147     * doesn't already have a target. It is best for views to contain one View object
148     * that does not exist in the view hierarchy (state.nonExistentView) so that
149     * when they are removed later, a list match will suffice to remove the targets.
150     * Otherwise, if you happened to have targeted the exact views for the transition,
151     * the replaceTargets call will remove them unexpectedly.
152     */
153    public static void addTargets(Object transitionObj, ArrayList<View> views) {
154        Transition transition = (Transition) transitionObj;
155        if (transition == null) {
156            return;
157        }
158        if (transition instanceof TransitionSet) {
159            TransitionSet set = (TransitionSet) transition;
160            int numTransitions = set.getTransitionCount();
161            for (int i = 0; i < numTransitions; i++) {
162                Transition child = set.getTransitionAt(i);
163                addTargets(child, views);
164            }
165        } else if (!hasSimpleTarget(transition)) {
166            List<View> targets = transition.getTargets();
167            if (isNullOrEmpty(targets)) {
168                // We can just add the target views
169                int numViews = views.size();
170                for (int i = 0; i < numViews; i++) {
171                    transition.addTarget(views.get(i));
172                }
173            }
174        }
175    }
176
177    /**
178     * Returns true if there are any targets based on ID, transition or type.
179     */
180    private static boolean hasSimpleTarget(Transition transition) {
181        return !isNullOrEmpty(transition.getTargetIds())
182                || !isNullOrEmpty(transition.getTargetNames())
183                || !isNullOrEmpty(transition.getTargetTypes());
184    }
185
186    /**
187     * Simple utility to detect if a list is null or has no elements.
188     */
189    private static boolean isNullOrEmpty(List list) {
190        return list == null || list.isEmpty();
191    }
192
193    /**
194     * Creates a TransitionSet that plays all passed transitions together. Any null
195     * transitions passed will not be added to the set. If all are null, then an empty
196     * TransitionSet will be returned.
197     */
198    public static Object mergeTransitionsTogether(Object transition1, Object transition2,
199            Object transition3) {
200        TransitionSet transitionSet = new TransitionSet();
201        if (transition1 != null) {
202            transitionSet.addTransition((Transition) transition1);
203        }
204        if (transition2 != null) {
205            transitionSet.addTransition((Transition) transition2);
206        }
207        if (transition3 != null) {
208            transitionSet.addTransition((Transition) transition3);
209        }
210        return transitionSet;
211    }
212
213    /**
214     * After the transition completes, the fragment's view is set to GONE and the exiting
215     * views are set to VISIBLE.
216     */
217    public static void scheduleHideFragmentView(Object exitTransitionObj, final View fragmentView,
218            final ArrayList<View> exitingViews) {
219        Transition exitTransition = (Transition) exitTransitionObj;
220        exitTransition.addListener(new Transition.TransitionListener() {
221            @Override
222            public void onTransitionStart(Transition transition) {
223            }
224
225            @Override
226            public void onTransitionEnd(Transition transition) {
227                transition.removeListener(this);
228                fragmentView.setVisibility(View.GONE);
229                final int numViews = exitingViews.size();
230                for (int i = 0; i < numViews; i++) {
231                    exitingViews.get(i).setVisibility(View.VISIBLE);
232                }
233            }
234
235            @Override
236            public void onTransitionCancel(Transition transition) {
237            }
238
239            @Override
240            public void onTransitionPause(Transition transition) {
241            }
242
243            @Override
244            public void onTransitionResume(Transition transition) {
245            }
246        });
247    }
248
249    /**
250     * Combines enter, exit, and shared element transition so that they play in the proper
251     * sequence. First the exit transition plays along with the shared element transition.
252     * When the exit transition completes, the enter transition starts. The shared element
253     * transition can continue running while the enter transition plays.
254     *
255     * @return A TransitionSet with all of enter, exit, and shared element transitions in
256     * it (modulo null values), ordered such that they play in the proper sequence.
257     */
258    public static Object mergeTransitionsInSequence(Object exitTransitionObj,
259            Object enterTransitionObj, Object sharedElementTransitionObj) {
260        // First do exit, then enter, but allow shared element transition to happen
261        // during both.
262        Transition staggered = null;
263        final Transition exitTransition = (Transition) exitTransitionObj;
264        final Transition enterTransition = (Transition) enterTransitionObj;
265        final Transition sharedElementTransition = (Transition) sharedElementTransitionObj;
266        if (exitTransition != null && enterTransition != null) {
267            staggered = new TransitionSet()
268                    .addTransition(exitTransition)
269                    .addTransition(enterTransition)
270                    .setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
271        } else if (exitTransition != null) {
272            staggered = exitTransition;
273        } else if (enterTransition != null) {
274            staggered = enterTransition;
275        }
276        if (sharedElementTransition != null) {
277            TransitionSet together = new TransitionSet();
278            if (staggered != null) {
279                together.addTransition(staggered);
280            }
281            together.addTransition(sharedElementTransition);
282            return together;
283        } else {
284            return staggered;
285        }
286    }
287
288    /**
289     * Calls {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)}.
290     */
291    public static void beginDelayedTransition(ViewGroup sceneRoot, Object transition) {
292        TransitionManager.beginDelayedTransition(sceneRoot, (Transition) transition);
293    }
294
295    /**
296     * Prepares for setting the shared element names by gathering the names of the incoming
297     * shared elements and clearing them. {@link #setNameOverridesOptimized(View, ArrayList,
298     * ArrayList, ArrayList, Map)} must be called after this to complete setting the shared element
299     * name overrides. This must be called before
300     * {@link #beginDelayedTransition(ViewGroup, Object)}.
301     */
302    public static ArrayList<String> prepareSetNameOverridesOptimized(
303            final ArrayList<View> sharedElementsIn) {
304        final ArrayList<String> names = new ArrayList<>();
305        final int numSharedElements = sharedElementsIn.size();
306        for (int i = 0; i < numSharedElements; i++) {
307            final View view = sharedElementsIn.get(i);
308            names.add(view.getTransitionName());
309            view.setTransitionName(null);
310        }
311        return names;
312    }
313
314    /**
315     * Changes the shared element names for the incoming shared eleemnts to match those of the
316     * outgoing shared elements. This also temporarily clears the shared element names of the
317     * outgoing shared elements. Must be called after
318     * {@link #beginDelayedTransition(ViewGroup, Object)}.
319     */
320    public static void setNameOverridesOptimized(final View sceneRoot,
321            final ArrayList<View> sharedElementsOut, final ArrayList<View> sharedElementsIn,
322            final ArrayList<String> inNames, final Map<String, String> nameOverrides) {
323        final int numSharedElements = sharedElementsIn.size();
324        final ArrayList<String> outNames = new ArrayList<>();
325
326        for (int i = 0; i < numSharedElements; i++) {
327            final View view = sharedElementsOut.get(i);
328            final String name = view.getTransitionName();
329            outNames.add(name);
330            if (name == null) {
331                continue;
332            }
333            view.setTransitionName(null);
334            final String inName = nameOverrides.get(name);
335            for (int j = 0; j < numSharedElements; j++) {
336                if (inName.equals(inNames.get(j))) {
337                    sharedElementsIn.get(j).setTransitionName(name);
338                    break;
339                }
340            }
341        }
342
343        OneShotPreDrawListener.add(sceneRoot, new Runnable() {
344            @Override
345            public void run() {
346                for (int i = 0; i < numSharedElements; i++) {
347                    sharedElementsIn.get(i).setTransitionName(inNames.get(i));
348                    sharedElementsOut.get(i).setTransitionName(outNames.get(i));
349                }
350            }
351        });
352    }
353
354    /**
355     * Gets the Views in the hierarchy affected by entering and exiting Activity Scene transitions.
356     * @param transitioningViews This View will be added to transitioningViews if it is VISIBLE and
357     *                           a normal View or a ViewGroup with
358     *                           {@link android.view.ViewGroup#isTransitionGroup()} true.
359     * @param view The base of the view hierarchy to look in.
360     */
361    public static void captureTransitioningViews(ArrayList<View> transitioningViews, View view) {
362        if (view.getVisibility() == View.VISIBLE) {
363            if (view instanceof ViewGroup) {
364                ViewGroup viewGroup = (ViewGroup) view;
365                if (viewGroup.isTransitionGroup()) {
366                    transitioningViews.add(viewGroup);
367                } else {
368                    int count = viewGroup.getChildCount();
369                    for (int i = 0; i < count; i++) {
370                        View child = viewGroup.getChildAt(i);
371                        captureTransitioningViews(transitioningViews, child);
372                    }
373                }
374            } else {
375                transitioningViews.add(view);
376            }
377        }
378    }
379
380    /**
381     * Finds all views that have transition names in the hierarchy under the given view and
382     * stores them in {@code namedViews} map with the name as the key.
383     */
384    public static void findNamedViews(Map<String, View> namedViews, View view) {
385        if (view.getVisibility() == View.VISIBLE) {
386            String transitionName = view.getTransitionName();
387            if (transitionName != null) {
388                namedViews.put(transitionName, view);
389            }
390            if (view instanceof ViewGroup) {
391                ViewGroup viewGroup = (ViewGroup) view;
392                int count = viewGroup.getChildCount();
393                for (int i = 0; i < count; i++) {
394                    View child = viewGroup.getChildAt(i);
395                    findNamedViews(namedViews, child);
396                }
397            }
398        }
399    }
400
401    public static void setNameOverridesUnoptimized(final View sceneRoot,
402            final ArrayList<View> sharedElementsIn, final Map<String, String> nameOverrides) {
403        OneShotPreDrawListener.add(sceneRoot, new Runnable() {
404            @Override
405            public void run() {
406                final int numSharedElements = sharedElementsIn.size();
407                for (int i = 0; i < numSharedElements; i++) {
408                    View view = sharedElementsIn.get(i);
409                    String name = view.getTransitionName();
410                    if (name != null) {
411                        String inName = findKeyForValue(nameOverrides, name);
412                        view.setTransitionName(inName);
413                    }
414                }
415            }
416        });
417    }
418
419    /**
420     * Utility to find the String key in {@code map} that maps to {@code value}.
421     */
422    private static String findKeyForValue(Map<String, String> map, String value) {
423        for (Map.Entry<String, String> entry : map.entrySet()) {
424            if (value.equals(entry.getValue())) {
425                return entry.getKey();
426            }
427        }
428        return null;
429    }
430
431    /**
432     * After the transition has started, remove all targets that we added to the transitions
433     * so that the transitions are left in a clean state.
434     */
435    public static void scheduleRemoveTargets(final Object overallTransitionObj,
436            final Object enterTransition, final ArrayList<View> enteringViews,
437            final Object exitTransition, final ArrayList<View> exitingViews,
438            final Object sharedElementTransition, final ArrayList<View> sharedElementsIn) {
439        final Transition overallTransition = (Transition) overallTransitionObj;
440        overallTransition.addListener(new Transition.TransitionListener() {
441            @Override
442            public void onTransitionStart(Transition transition) {
443                if (enterTransition != null) {
444                    replaceTargets(enterTransition, enteringViews, null);
445                }
446                if (exitTransition != null) {
447                    replaceTargets(exitTransition, exitingViews, null);
448                }
449                if (sharedElementTransition != null) {
450                    replaceTargets(sharedElementTransition, sharedElementsIn, null);
451                }
452            }
453
454            @Override
455            public void onTransitionEnd(Transition transition) {
456            }
457
458            @Override
459            public void onTransitionCancel(Transition transition) {
460            }
461
462            @Override
463            public void onTransitionPause(Transition transition) {
464            }
465
466            @Override
467            public void onTransitionResume(Transition transition) {
468            }
469        });
470    }
471
472    /**
473     * Swap the targets for the shared element transition from those Views in sharedElementsOut
474     * to those in sharedElementsIn
475     */
476    public static void swapSharedElementTargets(Object sharedElementTransitionObj,
477            ArrayList<View> sharedElementsOut, ArrayList<View> sharedElementsIn) {
478        TransitionSet sharedElementTransition = (TransitionSet) sharedElementTransitionObj;
479        if (sharedElementTransition != null) {
480            sharedElementTransition.getTargets().clear();
481            sharedElementTransition.getTargets().addAll(sharedElementsIn);
482            replaceTargets(sharedElementTransition, sharedElementsOut, sharedElementsIn);
483        }
484    }
485
486
487    /**
488     * This method removes the views from transitions that target ONLY those views and
489     * replaces them with the new targets list.
490     * The views list should match those added in addTargets and should contain
491     * one view that is not in the view hierarchy (state.nonExistentView).
492     */
493    public static void replaceTargets(Object transitionObj, ArrayList<View> oldTargets,
494            ArrayList<View> newTargets) {
495        Transition transition = (Transition) transitionObj;
496        if (transition instanceof TransitionSet) {
497            TransitionSet set = (TransitionSet) transition;
498            int numTransitions = set.getTransitionCount();
499            for (int i = 0; i < numTransitions; i++) {
500                Transition child = set.getTransitionAt(i);
501                replaceTargets(child, oldTargets, newTargets);
502            }
503        } else if (!hasSimpleTarget(transition)) {
504            List<View> targets = transition.getTargets();
505            if (targets != null && targets.size() == oldTargets.size()
506                    && targets.containsAll(oldTargets)) {
507                // We have an exact match. We must have added these earlier in addTargets
508                final int targetCount = newTargets == null ? 0 : newTargets.size();
509                for (int i = 0; i < targetCount; i++) {
510                    transition.addTarget(newTargets.get(i));
511                }
512                for (int i = oldTargets.size() - 1; i >= 0; i--) {
513                    transition.removeTarget(oldTargets.get(i));
514                }
515            }
516        }
517    }
518
519    /**
520     * Adds a View target to a transition. If transitionObj is null, nothing is done.
521     */
522    public static void addTarget(Object transitionObj, View view) {
523        if (transitionObj != null) {
524            Transition transition = (Transition) transitionObj;
525            transition.addTarget(view);
526        }
527    }
528
529    /**
530     * Remove a View target to a transition. If transitionObj is null, nothing is done.
531     */
532    public static void removeTarget(Object transitionObj, View view) {
533        if (transitionObj != null) {
534            Transition transition = (Transition) transitionObj;
535            transition.removeTarget(view);
536        }
537    }
538
539    /**
540     * Sets the epicenter of a transition to a rect object. The object can be modified until
541     * the transition runs.
542     */
543    public static void setEpicenter(Object transitionObj, final Rect epicenter) {
544        if (transitionObj != null) {
545            Transition transition = (Transition) transitionObj;
546            transition.setEpicenterCallback(new Transition.EpicenterCallback() {
547                @Override
548                public Rect onGetEpicenter(Transition transition) {
549                    if (epicenter == null || epicenter.isEmpty()) {
550                        return null;
551                    }
552                    return epicenter;
553                }
554            });
555        }
556    }
557
558    public static void scheduleNameReset(final ViewGroup sceneRoot,
559            final ArrayList<View> sharedElementsIn, final Map<String, String> nameOverrides) {
560        OneShotPreDrawListener.add(sceneRoot, new Runnable() {
561            @Override
562            public void run() {
563                final int numSharedElements = sharedElementsIn.size();
564                for (int i = 0; i < numSharedElements; i++) {
565                    final View view = sharedElementsIn.get(i);
566                    final String name = view.getTransitionName();
567                    final String inName = nameOverrides.get(name);
568                    view.setTransitionName(inName);
569                }
570            }
571        });
572    }
573}
574