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 */
16package android.app;
17
18import android.graphics.Rect;
19import android.os.Build;
20import android.transition.Transition;
21import android.transition.TransitionListenerAdapter;
22import android.transition.TransitionManager;
23import android.transition.TransitionSet;
24import android.util.ArrayMap;
25import android.util.SparseArray;
26import android.view.View;
27import android.view.ViewGroup;
28
29import com.android.internal.view.OneShotPreDrawListener;
30
31import java.util.ArrayList;
32import java.util.Collection;
33import java.util.List;
34import java.util.Map;
35
36/**
37 * Contains the Fragment Transition functionality for both ordered and reordered
38 * Fragment Transactions. With reordered fragment transactions, all Views have been
39 * added to the View hierarchy prior to calling startTransitions. With ordered
40 * fragment transactions, Views will be removed and added after calling startTransitions.
41 */
42class FragmentTransition {
43    /**
44     * The inverse of all BackStackRecord operation commands. This assumes that
45     * REPLACE operations have already been replaced by add/remove operations.
46     */
47    private static final int[] INVERSE_OPS = {
48            BackStackRecord.OP_NULL,              // inverse of OP_NULL (error)
49            BackStackRecord.OP_REMOVE,            // inverse of OP_ADD
50            BackStackRecord.OP_NULL,              // inverse of OP_REPLACE (error)
51            BackStackRecord.OP_ADD,               // inverse of OP_REMOVE
52            BackStackRecord.OP_SHOW,              // inverse of OP_HIDE
53            BackStackRecord.OP_HIDE,              // inverse of OP_SHOW
54            BackStackRecord.OP_ATTACH,            // inverse of OP_DETACH
55            BackStackRecord.OP_DETACH,            // inverse of OP_ATTACH
56            BackStackRecord.OP_UNSET_PRIMARY_NAV, // inverse of OP_SET_PRIMARY_NAV
57            BackStackRecord.OP_SET_PRIMARY_NAV,   // inverse of OP_UNSET_PRIMARY_NAV
58    };
59
60    /**
61     * The main entry point for Fragment Transitions, this starts the transitions
62     * set on the leaving Fragment's {@link Fragment#getExitTransition()}, the
63     * entering Fragment's {@link Fragment#getEnterTransition()} and
64     * {@link Fragment#getSharedElementEnterTransition()}. When popping,
65     * the leaving Fragment's {@link Fragment#getReturnTransition()} and
66     * {@link Fragment#getSharedElementReturnTransition()} and the entering
67     * {@link Fragment#getReenterTransition()} will be run.
68     * <p>
69     * With reordered Fragment Transitions, all Views have been added to the
70     * View hierarchy prior to calling this method. The incoming Fragment's Views
71     * will be INVISIBLE. With ordered Fragment Transitions, this method
72     * is called before any change has been made to the hierarchy. That means
73     * that the added Fragments have not created their Views yet and the hierarchy
74     * is unknown.
75     *
76     * @param fragmentManager The executing FragmentManagerImpl
77     * @param records The list of transactions being executed.
78     * @param isRecordPop For each transaction, whether it is a pop transaction or not.
79     * @param startIndex The first index into records and isRecordPop to execute as
80     *                   part of this transition.
81     * @param endIndex One past the last index into records and isRecordPop to execute
82     *                 as part of this transition.
83     * @param isReordered true if this is a reordered transaction, meaning that the
84     *                    Views of incoming fragments have been added. false if the
85     *                    transaction has yet to be run and Views haven't been created.
86     */
87    static void startTransitions(FragmentManagerImpl fragmentManager,
88            ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
89            int startIndex, int endIndex, boolean isReordered) {
90        if (fragmentManager.mCurState < Fragment.CREATED) {
91            return;
92        }
93        SparseArray<FragmentContainerTransition> transitioningFragments =
94                new SparseArray<>();
95        for (int i = startIndex; i < endIndex; i++) {
96            final BackStackRecord record = records.get(i);
97            final boolean isPop = isRecordPop.get(i);
98            if (isPop) {
99                calculatePopFragments(record, transitioningFragments, isReordered);
100            } else {
101                calculateFragments(record, transitioningFragments, isReordered);
102            }
103        }
104
105        if (transitioningFragments.size() != 0) {
106            final View nonExistentView = new View(fragmentManager.mHost.getContext());
107            final int numContainers = transitioningFragments.size();
108            for (int i = 0; i < numContainers; i++) {
109                int containerId = transitioningFragments.keyAt(i);
110                ArrayMap<String, String> nameOverrides = calculateNameOverrides(containerId,
111                        records, isRecordPop, startIndex, endIndex);
112
113                FragmentContainerTransition containerTransition = transitioningFragments.valueAt(i);
114
115                if (isReordered) {
116                    configureTransitionsReordered(fragmentManager, containerId,
117                            containerTransition, nonExistentView, nameOverrides);
118                } else {
119                    configureTransitionsOrdered(fragmentManager, containerId,
120                            containerTransition, nonExistentView, nameOverrides);
121                }
122            }
123        }
124    }
125
126    /**
127     * Iterates through the transactions that affect a given fragment container
128     * and tracks the shared element names across transactions. This is most useful
129     * in pop transactions where the names of shared elements are known.
130     *
131     * @param containerId The container ID that is executing the transition.
132     * @param records The list of transactions being executed.
133     * @param isRecordPop For each transaction, whether it is a pop transaction or not.
134     * @param startIndex The first index into records and isRecordPop to execute as
135     *                   part of this transition.
136     * @param endIndex One past the last index into records and isRecordPop to execute
137     *                 as part of this transition.
138     * @return A map from the initial shared element name to the final shared element name
139     * before any onMapSharedElements is run.
140     */
141    private static ArrayMap<String, String> calculateNameOverrides(int containerId,
142            ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
143            int startIndex, int endIndex) {
144        ArrayMap<String, String> nameOverrides = new ArrayMap<>();
145        for (int recordNum = endIndex - 1; recordNum >= startIndex; recordNum--) {
146            final BackStackRecord record = records.get(recordNum);
147            if (!record.interactsWith(containerId)) {
148                continue;
149            }
150            final boolean isPop = isRecordPop.get(recordNum);
151            if (record.mSharedElementSourceNames != null) {
152                final int numSharedElements = record.mSharedElementSourceNames.size();
153                final ArrayList<String> sources;
154                final ArrayList<String> targets;
155                if (isPop) {
156                    targets = record.mSharedElementSourceNames;
157                    sources = record.mSharedElementTargetNames;
158                } else {
159                    sources = record.mSharedElementSourceNames;
160                    targets = record.mSharedElementTargetNames;
161                }
162                for (int i = 0; i < numSharedElements; i++) {
163                    String sourceName = sources.get(i);
164                    String targetName = targets.get(i);
165                    String previousTarget = nameOverrides.remove(targetName);
166                    if (previousTarget != null) {
167                        nameOverrides.put(sourceName, previousTarget);
168                    } else {
169                        nameOverrides.put(sourceName, targetName);
170                    }
171                }
172            }
173        }
174        return nameOverrides;
175    }
176
177    /**
178     * Configures a transition for a single fragment container for which the transaction was
179     * reordered. That means that all Fragment Views have been added and incoming fragment
180     * Views are marked invisible.
181     *
182     * @param fragmentManager The executing FragmentManagerImpl
183     * @param containerId The container ID that is executing the transition.
184     * @param fragments A structure holding the transitioning fragments in this container.
185     * @param nonExistentView A View that does not exist in the hierarchy. This is used to
186     *                        prevent transitions from acting on other Views when there is no
187     *                        other target.
188     * @param nameOverrides A map of the shared element names from the starting fragment to
189     *                      the final fragment's Views as given in
190     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
191     */
192    private static void configureTransitionsReordered(FragmentManagerImpl fragmentManager,
193            int containerId, FragmentContainerTransition fragments,
194            View nonExistentView, ArrayMap<String, String> nameOverrides) {
195        ViewGroup sceneRoot = null;
196        if (fragmentManager.mContainer.onHasView()) {
197            sceneRoot = fragmentManager.mContainer.onFindViewById(containerId);
198        }
199        if (sceneRoot == null) {
200            return;
201        }
202        final Fragment inFragment = fragments.lastIn;
203        final Fragment outFragment = fragments.firstOut;
204        final boolean inIsPop = fragments.lastInIsPop;
205        final boolean outIsPop = fragments.firstOutIsPop;
206
207        ArrayList<View> sharedElementsIn = new ArrayList<>();
208        ArrayList<View> sharedElementsOut = new ArrayList<>();
209        Transition enterTransition = getEnterTransition(inFragment, inIsPop);
210        Transition exitTransition = getExitTransition(outFragment, outIsPop);
211
212        TransitionSet sharedElementTransition = configureSharedElementsReordered(sceneRoot,
213                nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,
214                enterTransition, exitTransition);
215
216        if (enterTransition == null && sharedElementTransition == null &&
217                exitTransition == null) {
218            return; // no transitions!
219        }
220
221        ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition,
222                outFragment, sharedElementsOut, nonExistentView);
223
224        ArrayList<View> enteringViews = configureEnteringExitingViews(enterTransition,
225                inFragment, sharedElementsIn, nonExistentView);
226
227        setViewVisibility(enteringViews, View.INVISIBLE);
228
229        Transition transition = mergeTransitions(enterTransition, exitTransition,
230                sharedElementTransition, inFragment, inIsPop);
231
232        if (transition != null) {
233            replaceHide(exitTransition, outFragment, exitingViews);
234            transition.setNameOverrides(nameOverrides);
235            scheduleRemoveTargets(transition,
236                    enterTransition, enteringViews, exitTransition, exitingViews,
237                    sharedElementTransition, sharedElementsIn);
238            TransitionManager.beginDelayedTransition(sceneRoot, transition);
239            setViewVisibility(enteringViews, View.VISIBLE);
240            // Swap the shared element targets
241            if (sharedElementTransition != null) {
242                sharedElementTransition.getTargets().clear();
243                sharedElementTransition.getTargets().addAll(sharedElementsIn);
244                replaceTargets(sharedElementTransition, sharedElementsOut, sharedElementsIn);
245            }
246        }
247    }
248
249    /**
250     * Configures a transition for a single fragment container for which the transaction was
251     * ordered. That means that the transaction has not been executed yet, so incoming
252     * Views are not yet known.
253     *
254     * @param fragmentManager The executing FragmentManagerImpl
255     * @param containerId The container ID that is executing the transition.
256     * @param fragments A structure holding the transitioning fragments in this container.
257     * @param nonExistentView A View that does not exist in the hierarchy. This is used to
258     *                        prevent transitions from acting on other Views when there is no
259     *                        other target.
260     * @param nameOverrides A map of the shared element names from the starting fragment to
261     *                      the final fragment's Views as given in
262     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
263     */
264    private static void configureTransitionsOrdered(FragmentManagerImpl fragmentManager,
265            int containerId, FragmentContainerTransition fragments,
266            View nonExistentView, ArrayMap<String, String> nameOverrides) {
267        ViewGroup sceneRoot = null;
268        if (fragmentManager.mContainer.onHasView()) {
269            sceneRoot = fragmentManager.mContainer.onFindViewById(containerId);
270        }
271        if (sceneRoot == null) {
272            return;
273        }
274        final Fragment inFragment = fragments.lastIn;
275        final Fragment outFragment = fragments.firstOut;
276        final boolean inIsPop = fragments.lastInIsPop;
277        final boolean outIsPop = fragments.firstOutIsPop;
278
279        Transition enterTransition = getEnterTransition(inFragment, inIsPop);
280        Transition exitTransition = getExitTransition(outFragment, outIsPop);
281
282        ArrayList<View> sharedElementsOut = new ArrayList<>();
283        ArrayList<View> sharedElementsIn = new ArrayList<>();
284
285        TransitionSet sharedElementTransition = configureSharedElementsOrdered(sceneRoot,
286                nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,
287                enterTransition, exitTransition);
288
289        if (enterTransition == null && sharedElementTransition == null &&
290                exitTransition == null) {
291            return; // no transitions!
292        }
293
294        ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition,
295                outFragment, sharedElementsOut, nonExistentView);
296
297        if (exitingViews == null || exitingViews.isEmpty()) {
298            exitTransition = null;
299        }
300
301        if (enterTransition != null) {
302            // Ensure the entering transition doesn't target anything until the views are made
303            // visible
304            enterTransition.addTarget(nonExistentView);
305        }
306
307        Transition transition = mergeTransitions(enterTransition, exitTransition,
308                sharedElementTransition, inFragment, fragments.lastInIsPop);
309
310        if (transition != null) {
311            transition.setNameOverrides(nameOverrides);
312            final ArrayList<View> enteringViews = new ArrayList<>();
313            scheduleRemoveTargets(transition,
314                    enterTransition, enteringViews, exitTransition, exitingViews,
315                    sharedElementTransition, sharedElementsIn);
316            scheduleTargetChange(sceneRoot, inFragment, nonExistentView, sharedElementsIn,
317                    enterTransition, enteringViews, exitTransition, exitingViews);
318
319            TransitionManager.beginDelayedTransition(sceneRoot, transition);
320        }
321    }
322
323    /**
324     * Replace hide operations with visibility changes on the exiting views. Instead of making
325     * the entire fragment's view GONE, make each exiting view INVISIBLE. At the end of the
326     * transition, make the fragment's view GONE.
327     */
328    private static void replaceHide(Transition exitTransition, Fragment exitingFragment,
329            final ArrayList<View> exitingViews) {
330        if (exitingFragment != null && exitTransition != null && exitingFragment.mAdded
331                && exitingFragment.mHidden && exitingFragment.mHiddenChanged) {
332            exitingFragment.setHideReplaced(true);
333            final View fragmentView = exitingFragment.getView();
334            OneShotPreDrawListener.add(exitingFragment.mContainer, () -> {
335                setViewVisibility(exitingViews, View.INVISIBLE);
336            });
337            exitTransition.addListener(new TransitionListenerAdapter() {
338                @Override
339                public void onTransitionEnd(Transition transition) {
340                    transition.removeListener(this);
341                    fragmentView.setVisibility(View.GONE);
342                    setViewVisibility(exitingViews, View.VISIBLE);
343                }
344            });
345        }
346    }
347
348    /**
349     * This method is used for fragment transitions for ordered transactions to change the
350     * enter and exit transition targets after the call to
351     * {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)}. The exit transition
352     * must ensure that it does not target any Views and the enter transition must start targeting
353     * the Views of the incoming Fragment.
354     *
355     * @param sceneRoot The fragment container View
356     * @param inFragment The last fragment that is entering
357     * @param nonExistentView A view that does not exist in the hierarchy that is used as a
358     *                        transition target to ensure no View is targeted.
359     * @param sharedElementsIn The shared element Views of the incoming fragment
360     * @param enterTransition The enter transition of the incoming fragment
361     * @param enteringViews The entering Views of the incoming fragment
362     * @param exitTransition The exit transition of the outgoing fragment
363     * @param exitingViews The exiting views of the outgoing fragment
364     */
365    private static void scheduleTargetChange(final ViewGroup sceneRoot,
366            final Fragment inFragment, final View nonExistentView,
367            final ArrayList<View> sharedElementsIn,
368            final Transition enterTransition, final ArrayList<View> enteringViews,
369            final Transition exitTransition, final ArrayList<View> exitingViews) {
370
371        OneShotPreDrawListener.add(sceneRoot, () -> {
372            if (enterTransition != null) {
373                enterTransition.removeTarget(nonExistentView);
374                ArrayList<View> views = configureEnteringExitingViews(
375                        enterTransition, inFragment, sharedElementsIn, nonExistentView);
376                enteringViews.addAll(views);
377            }
378
379            if (exitingViews != null) {
380                if (exitTransition != null) {
381                    ArrayList<View> tempExiting = new ArrayList<>();
382                    tempExiting.add(nonExistentView);
383                    replaceTargets(exitTransition, exitingViews, tempExiting);
384                }
385                exitingViews.clear();
386                exitingViews.add(nonExistentView);
387            }
388        });
389    }
390
391    /**
392     * Returns a TransitionSet containing the shared element transition. The wrapping TransitionSet
393     * targets all shared elements to ensure that no other Views are targeted. The shared element
394     * transition can then target any or all shared elements without worrying about accidentally
395     * targeting entering or exiting Views.
396     *
397     * @param inFragment The incoming fragment
398     * @param outFragment the outgoing fragment
399     * @param isPop True if this is a pop transaction or false if it is a normal (add) transaction.
400     * @return A TransitionSet wrapping the shared element transition or null if no such transition
401     * exists.
402     */
403    private static TransitionSet getSharedElementTransition(Fragment inFragment,
404            Fragment outFragment, boolean isPop) {
405        if (inFragment == null || outFragment == null) {
406            return null;
407        }
408        Transition transition = cloneTransition(isPop
409                ? outFragment.getSharedElementReturnTransition()
410                : inFragment.getSharedElementEnterTransition());
411        if (transition == null) {
412            return null;
413        }
414        TransitionSet transitionSet = new TransitionSet();
415        transitionSet.addTransition(transition);
416        return transitionSet;
417    }
418
419    /**
420     * Returns a clone of the enter transition or null if no such transition exists.
421     */
422    private static Transition getEnterTransition(Fragment inFragment, boolean isPop) {
423        if (inFragment == null) {
424            return null;
425        }
426        return cloneTransition(isPop ? inFragment.getReenterTransition() :
427                inFragment.getEnterTransition());
428    }
429
430    /**
431     * Returns a clone of the exit transition or null if no such transition exists.
432     */
433    private static Transition getExitTransition(Fragment outFragment, boolean isPop) {
434        if (outFragment == null) {
435            return null;
436        }
437        return cloneTransition(isPop ? outFragment.getReturnTransition() :
438                outFragment.getExitTransition());
439    }
440
441    /**
442     * Returns a clone of a transition or null if it is null
443     */
444    private static Transition cloneTransition(Transition transition) {
445        if (transition != null) {
446            transition = transition.clone();
447        }
448        return transition;
449    }
450
451    /**
452     * Configures the shared elements of an reordered fragment transaction's transition.
453     * This retrieves the shared elements of the outgoing and incoming fragments, maps the
454     * views, and sets up the epicenter on the transitions.
455     * <p>
456     * The epicenter of exit and shared element transitions is the first shared element
457     * in the outgoing fragment. The epicenter of the entering transition is the first shared
458     * element in the incoming fragment.
459     *
460     * @param sceneRoot The fragment container View
461     * @param nonExistentView A View that does not exist in the hierarchy. This is used to
462     *                        prevent transitions from acting on other Views when there is no
463     *                        other target.
464     * @param nameOverrides A map of the shared element names from the starting fragment to
465     *                      the final fragment's Views as given in
466     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
467     * @param fragments A structure holding the transitioning fragments in this container.
468     * @param sharedElementsOut A list modified to contain the shared elements in the outgoing
469     *                          fragment
470     * @param sharedElementsIn A list modified to contain the shared elements in the incoming
471     *                         fragment
472     * @param enterTransition The transition used for entering Views, modified by applying the
473     *                        epicenter
474     * @param exitTransition The transition used for exiting Views, modified by applying the
475     *                       epicenter
476     * @return The shared element transition or null if no shared elements exist
477     */
478    private static TransitionSet configureSharedElementsReordered(final ViewGroup sceneRoot,
479            final View nonExistentView, ArrayMap<String, String> nameOverrides,
480            final FragmentContainerTransition fragments,
481            final ArrayList<View> sharedElementsOut,
482            final ArrayList<View> sharedElementsIn,
483            final Transition enterTransition, final Transition exitTransition) {
484        final Fragment inFragment = fragments.lastIn;
485        final Fragment outFragment = fragments.firstOut;
486        if (inFragment != null) {
487            inFragment.getView().setVisibility(View.VISIBLE);
488        }
489        if (inFragment == null || outFragment == null) {
490            return null; // no shared element without a fragment
491        }
492
493        final boolean inIsPop = fragments.lastInIsPop;
494        TransitionSet sharedElementTransition = nameOverrides.isEmpty() ? null
495                : getSharedElementTransition(inFragment, outFragment, inIsPop);
496
497        ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides,
498                sharedElementTransition, fragments);
499
500        ArrayMap<String, View> inSharedElements = captureInSharedElements(nameOverrides,
501                sharedElementTransition, fragments);
502
503        if (nameOverrides.isEmpty()) {
504            sharedElementTransition = null;
505            if (outSharedElements != null) {
506                outSharedElements.clear();
507            }
508            if (inSharedElements != null) {
509                inSharedElements.clear();
510            }
511        } else {
512            addSharedElementsWithMatchingNames(sharedElementsOut, outSharedElements,
513                    nameOverrides.keySet());
514            addSharedElementsWithMatchingNames(sharedElementsIn, inSharedElements,
515                    nameOverrides.values());
516        }
517
518        if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
519            // don't call onSharedElementStart/End since there is no transition
520            return null;
521        }
522
523        callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);
524
525        final Rect epicenter;
526        final View epicenterView;
527        if (sharedElementTransition != null) {
528            sharedElementsIn.add(nonExistentView);
529            setSharedElementTargets(sharedElementTransition, nonExistentView, sharedElementsOut);
530            final boolean outIsPop = fragments.firstOutIsPop;
531            final BackStackRecord outTransaction = fragments.firstOutTransaction;
532            setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop,
533                    outTransaction);
534            epicenter = new Rect();
535            epicenterView = getInEpicenterView(inSharedElements, fragments,
536                    enterTransition, inIsPop);
537            if (epicenterView != null) {
538                enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
539                    @Override
540                    public Rect onGetEpicenter(Transition transition) {
541                        return epicenter;
542                    }
543                });
544            }
545        } else {
546            epicenter = null;
547            epicenterView = null;
548        }
549
550        OneShotPreDrawListener.add(sceneRoot, () -> {
551            callSharedElementStartEnd(inFragment, outFragment, inIsPop,
552                    inSharedElements, false);
553            if (epicenterView != null) {
554                epicenterView.getBoundsOnScreen(epicenter);
555            }
556        });
557        return sharedElementTransition;
558    }
559
560    /**
561     * Add Views from sharedElements into views that have the transitionName in the
562     * nameOverridesSet.
563     *
564     * @param views               Views list to add shared elements to
565     * @param sharedElements      List of shared elements
566     * @param nameOverridesSet    The transition names for all views to be copied from
567     *                            sharedElements to views.
568     */
569    private static void addSharedElementsWithMatchingNames(ArrayList<View> views,
570            ArrayMap<String, View> sharedElements, Collection<String> nameOverridesSet) {
571        for (int i = sharedElements.size() - 1; i >= 0; i--) {
572            View view = sharedElements.valueAt(i);
573            if (view != null && nameOverridesSet.contains(view.getTransitionName())) {
574                views.add(view);
575            }
576        }
577    }
578
579    /**
580     * Configures the shared elements of an ordered fragment transaction's transition.
581     * This retrieves the shared elements of the incoming fragments, and schedules capturing
582     * the incoming fragment's shared elements. It also maps the views, and sets up the epicenter
583     * on the transitions.
584     * <p>
585     * The epicenter of exit and shared element transitions is the first shared element
586     * in the outgoing fragment. The epicenter of the entering transition is the first shared
587     * element in the incoming fragment.
588     *
589     * @param sceneRoot The fragment container View
590     * @param nonExistentView A View that does not exist in the hierarchy. This is used to
591     *                        prevent transitions from acting on other Views when there is no
592     *                        other target.
593     * @param nameOverrides A map of the shared element names from the starting fragment to
594     *                      the final fragment's Views as given in
595     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
596     * @param fragments A structure holding the transitioning fragments in this container.
597     * @param sharedElementsOut A list modified to contain the shared elements in the outgoing
598     *                          fragment
599     * @param sharedElementsIn A list modified to contain the shared elements in the incoming
600     *                         fragment
601     * @param enterTransition The transition used for entering Views, modified by applying the
602     *                        epicenter
603     * @param exitTransition The transition used for exiting Views, modified by applying the
604     *                       epicenter
605     * @return The shared element transition or null if no shared elements exist
606     */
607    private static TransitionSet configureSharedElementsOrdered(final ViewGroup sceneRoot,
608            final View nonExistentView, ArrayMap<String, String> nameOverrides,
609            final FragmentContainerTransition fragments,
610            final ArrayList<View> sharedElementsOut,
611            final ArrayList<View> sharedElementsIn,
612            final Transition enterTransition, final Transition exitTransition) {
613        final Fragment inFragment = fragments.lastIn;
614        final Fragment outFragment = fragments.firstOut;
615
616        if (inFragment == null || outFragment == null) {
617            return null; // no transition
618        }
619
620        final boolean inIsPop = fragments.lastInIsPop;
621        TransitionSet sharedElementTransition = nameOverrides.isEmpty() ? null
622                : getSharedElementTransition(inFragment, outFragment, inIsPop);
623
624        ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides,
625                sharedElementTransition, fragments);
626
627        if (nameOverrides.isEmpty()) {
628            sharedElementTransition = null;
629        } else {
630            sharedElementsOut.addAll(outSharedElements.values());
631        }
632
633        if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
634            // don't call onSharedElementStart/End since there is no transition
635            return null;
636        }
637
638        callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);
639
640        final Rect inEpicenter;
641        if (sharedElementTransition != null) {
642            inEpicenter = new Rect();
643            setSharedElementTargets(sharedElementTransition, nonExistentView, sharedElementsOut);
644            final boolean outIsPop = fragments.firstOutIsPop;
645            final BackStackRecord outTransaction = fragments.firstOutTransaction;
646            setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop,
647                    outTransaction);
648            if (enterTransition != null) {
649                enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
650                    @Override
651                    public Rect onGetEpicenter(Transition transition) {
652                        if (inEpicenter.isEmpty()) {
653                            return null;
654                        }
655                        return inEpicenter;
656                    }
657                });
658            }
659        } else {
660            inEpicenter = null;
661        }
662
663        TransitionSet finalSharedElementTransition = sharedElementTransition;
664
665        OneShotPreDrawListener.add(sceneRoot, () -> {
666            ArrayMap<String, View> inSharedElements = captureInSharedElements(
667                    nameOverrides, finalSharedElementTransition, fragments);
668
669            if (inSharedElements != null) {
670                sharedElementsIn.addAll(inSharedElements.values());
671                sharedElementsIn.add(nonExistentView);
672            }
673
674            callSharedElementStartEnd(inFragment, outFragment, inIsPop,
675                    inSharedElements, false);
676            if (finalSharedElementTransition != null) {
677                finalSharedElementTransition.getTargets().clear();
678                finalSharedElementTransition.getTargets().addAll(sharedElementsIn);
679                replaceTargets(finalSharedElementTransition, sharedElementsOut,
680                        sharedElementsIn);
681
682                final View inEpicenterView = getInEpicenterView(inSharedElements,
683                        fragments, enterTransition, inIsPop);
684                if (inEpicenterView != null) {
685                    inEpicenterView.getBoundsOnScreen(inEpicenter);
686                }
687            }
688        });
689        return sharedElementTransition;
690    }
691
692    /**
693     * Finds the shared elements in the outgoing fragment. It also calls
694     * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control
695     * of the shared element mapping. {@code nameOverrides} is updated to match the
696     * actual transition name of the mapped shared elements.
697     *
698     * @param nameOverrides A map of the shared element names from the starting fragment to
699     *                      the final fragment's Views as given in
700     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
701     * @param sharedElementTransition The shared element transition
702     * @param fragments A structure holding the transitioning fragments in this container.
703     * @return The mapping of shared element names to the Views in the hierarchy or null
704     * if there is no shared element transition.
705     */
706    private static ArrayMap<String, View> captureOutSharedElements(
707            ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition,
708            FragmentContainerTransition fragments) {
709        if (nameOverrides.isEmpty() || sharedElementTransition == null) {
710            nameOverrides.clear();
711            return null;
712        }
713        final Fragment outFragment = fragments.firstOut;
714        final ArrayMap<String, View> outSharedElements = new ArrayMap<>();
715        outFragment.getView().findNamedViews(outSharedElements);
716
717        final SharedElementCallback sharedElementCallback;
718        final ArrayList<String> names;
719        final BackStackRecord outTransaction = fragments.firstOutTransaction;
720        if (fragments.firstOutIsPop) {
721            sharedElementCallback = outFragment.getEnterTransitionCallback();
722            names = outTransaction.mSharedElementTargetNames;
723        } else {
724            sharedElementCallback = outFragment.getExitTransitionCallback();
725            names = outTransaction.mSharedElementSourceNames;
726        }
727
728        outSharedElements.retainAll(names);
729        if (sharedElementCallback != null) {
730            sharedElementCallback.onMapSharedElements(names, outSharedElements);
731            for (int i = names.size() - 1; i >= 0; i--) {
732                String name = names.get(i);
733                View view = outSharedElements.get(name);
734                if (view == null) {
735                    nameOverrides.remove(name);
736                } else if (!name.equals(view.getTransitionName())) {
737                    String targetValue = nameOverrides.remove(name);
738                    nameOverrides.put(view.getTransitionName(), targetValue);
739                }
740            }
741        } else {
742            nameOverrides.retainAll(outSharedElements.keySet());
743        }
744        return outSharedElements;
745    }
746
747    /**
748     * Finds the shared elements in the incoming fragment. It also calls
749     * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control
750     * of the shared element mapping. {@code nameOverrides} is updated to match the
751     * actual transition name of the mapped shared elements.
752     *
753     * @param nameOverrides A map of the shared element names from the starting fragment to
754     *                      the final fragment's Views as given in
755     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
756     * @param sharedElementTransition The shared element transition
757     * @param fragments A structure holding the transitioning fragments in this container.
758     * @return The mapping of shared element names to the Views in the hierarchy or null
759     * if there is no shared element transition.
760     */
761    private static ArrayMap<String, View> captureInSharedElements(
762            ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition,
763            FragmentContainerTransition fragments) {
764        Fragment inFragment = fragments.lastIn;
765        final View fragmentView = inFragment.getView();
766        if (nameOverrides.isEmpty() || sharedElementTransition == null || fragmentView == null) {
767            nameOverrides.clear();
768            return null;
769        }
770        final ArrayMap<String, View> inSharedElements = new ArrayMap<>();
771        fragmentView.findNamedViews(inSharedElements);
772
773        final SharedElementCallback sharedElementCallback;
774        final ArrayList<String> names;
775        final BackStackRecord inTransaction = fragments.lastInTransaction;
776        if (fragments.lastInIsPop) {
777            sharedElementCallback = inFragment.getExitTransitionCallback();
778            names = inTransaction.mSharedElementSourceNames;
779        } else {
780            sharedElementCallback = inFragment.getEnterTransitionCallback();
781            names = inTransaction.mSharedElementTargetNames;
782        }
783
784        if (names != null) {
785            inSharedElements.retainAll(names);
786        }
787        if (names != null && sharedElementCallback != null) {
788            sharedElementCallback.onMapSharedElements(names, inSharedElements);
789            for (int i = names.size() - 1; i >= 0; i--) {
790                String name = names.get(i);
791                View view = inSharedElements.get(name);
792                if (view == null) {
793                    String key = findKeyForValue(nameOverrides, name);
794                    if (key != null) {
795                        nameOverrides.remove(key);
796                    }
797                } else if (!name.equals(view.getTransitionName())) {
798                    String key = findKeyForValue(nameOverrides, name);
799                    if (key != null) {
800                        nameOverrides.put(key, view.getTransitionName());
801                    }
802                }
803            }
804        } else {
805            retainValues(nameOverrides, inSharedElements);
806        }
807        return inSharedElements;
808    }
809
810    /**
811     * Utility to find the String key in {@code map} that maps to {@code value}.
812     */
813    private static String findKeyForValue(ArrayMap<String, String> map, String value) {
814        final int numElements = map.size();
815        for (int i = 0; i < numElements; i++) {
816            if (value.equals(map.valueAt(i))) {
817                return map.keyAt(i);
818            }
819        }
820        return null;
821    }
822
823    /**
824     * Returns the View in the incoming Fragment that should be used as the epicenter.
825     *
826     * @param inSharedElements The mapping of shared element names to Views in the
827     *                         incoming fragment.
828     * @param fragments A structure holding the transitioning fragments in this container.
829     * @param enterTransition The transition used for the incoming Fragment's views
830     * @param inIsPop Is the incoming fragment being added as a pop transaction?
831     */
832    private static View getInEpicenterView(ArrayMap<String, View> inSharedElements,
833            FragmentContainerTransition fragments,
834            Transition enterTransition, boolean inIsPop) {
835        BackStackRecord inTransaction = fragments.lastInTransaction;
836        if (enterTransition != null && inSharedElements != null
837                && inTransaction.mSharedElementSourceNames != null
838                && !inTransaction.mSharedElementSourceNames.isEmpty()) {
839            final String targetName = inIsPop
840                    ? inTransaction.mSharedElementSourceNames.get(0)
841                    : inTransaction.mSharedElementTargetNames.get(0);
842            return inSharedElements.get(targetName);
843        }
844        return null;
845    }
846
847    /**
848     * Sets the epicenter for the exit transition.
849     *
850     * @param sharedElementTransition The shared element transition
851     * @param exitTransition The transition for the outgoing fragment's views
852     * @param outSharedElements Shared elements in the outgoing fragment
853     * @param outIsPop Is the outgoing fragment being removed as a pop transaction?
854     * @param outTransaction The transaction that caused the fragment to be removed.
855     */
856    private static void setOutEpicenter(TransitionSet sharedElementTransition,
857            Transition exitTransition, ArrayMap<String, View> outSharedElements, boolean outIsPop,
858            BackStackRecord outTransaction) {
859        if (outTransaction.mSharedElementSourceNames != null &&
860                !outTransaction.mSharedElementSourceNames.isEmpty()) {
861            final String sourceName = outIsPop
862                    ? outTransaction.mSharedElementTargetNames.get(0)
863                    : outTransaction.mSharedElementSourceNames.get(0);
864            final View outEpicenterView = outSharedElements.get(sourceName);
865            setEpicenter(sharedElementTransition, outEpicenterView);
866
867            if (exitTransition != null) {
868                setEpicenter(exitTransition, outEpicenterView);
869            }
870        }
871    }
872
873    /**
874     * Sets a transition epicenter to the rectangle of a given View.
875     */
876    private static void setEpicenter(Transition transition, View view) {
877        if (view != null) {
878            final Rect epicenter = new Rect();
879            view.getBoundsOnScreen(epicenter);
880
881            transition.setEpicenterCallback(new Transition.EpicenterCallback() {
882                @Override
883                public Rect onGetEpicenter(Transition transition) {
884                    return epicenter;
885                }
886            });
887        }
888    }
889
890    /**
891     * A utility to retain only the mappings in {@code nameOverrides} that have a value
892     * that has a key in {@code namedViews}. This is a useful equivalent to
893     * {@link ArrayMap#retainAll(Collection)} for values.
894     */
895    private static void retainValues(ArrayMap<String, String> nameOverrides,
896            ArrayMap<String, View> namedViews) {
897        for (int i = nameOverrides.size() - 1; i >= 0; i--) {
898            final String targetName = nameOverrides.valueAt(i);
899            if (!namedViews.containsKey(targetName)) {
900                nameOverrides.removeAt(i);
901            }
902        }
903    }
904
905    /**
906     * Calls the {@link SharedElementCallback#onSharedElementStart(List, List, List)} or
907     * {@link SharedElementCallback#onSharedElementEnd(List, List, List)} on the appropriate
908     * incoming or outgoing fragment.
909     *
910     * @param inFragment The incoming fragment
911     * @param outFragment The outgoing fragment
912     * @param isPop Is the incoming fragment part of a pop transaction?
913     * @param sharedElements The shared element Views
914     * @param isStart Call the start or end call on the SharedElementCallback
915     */
916    private static void callSharedElementStartEnd(Fragment inFragment, Fragment outFragment,
917            boolean isPop, ArrayMap<String, View> sharedElements, boolean isStart) {
918        SharedElementCallback sharedElementCallback = isPop
919                ? outFragment.getEnterTransitionCallback()
920                : inFragment.getEnterTransitionCallback();
921        if (sharedElementCallback != null) {
922            ArrayList<View> views = new ArrayList<>();
923            ArrayList<String> names = new ArrayList<>();
924            final int count = sharedElements == null ? 0 : sharedElements.size();
925            for (int i = 0; i < count; i++) {
926                names.add(sharedElements.keyAt(i));
927                views.add(sharedElements.valueAt(i));
928            }
929            if (isStart) {
930                sharedElementCallback.onSharedElementStart(names, views, null);
931            } else {
932                sharedElementCallback.onSharedElementEnd(names, views, null);
933            }
934        }
935    }
936
937    /**
938     * Finds all children of the shared elements and sets the wrapping TransitionSet
939     * targets to point to those. It also limits transitions that have no targets to the
940     * specific shared elements. This allows developers to target child views of the
941     * shared elements specifically, but this doesn't happen by default.
942     */
943    private static void setSharedElementTargets(TransitionSet transition,
944            View nonExistentView, ArrayList<View> sharedViews) {
945        final List<View> views = transition.getTargets();
946        views.clear();
947        final int count = sharedViews.size();
948        for (int i = 0; i < count; i++) {
949            final View view = sharedViews.get(i);
950            bfsAddViewChildren(views, view);
951        }
952        views.add(nonExistentView);
953        sharedViews.add(nonExistentView);
954        addTargets(transition, sharedViews);
955    }
956
957    /**
958     * Uses a breadth-first scheme to add startView and all of its children to views.
959     * It won't add a child if it is already in views.
960     */
961    private static void bfsAddViewChildren(final List<View> views, final View startView) {
962        final int startIndex = views.size();
963        if (containedBeforeIndex(views, startView, startIndex)) {
964            return; // This child is already in the list, so all its children are also.
965        }
966        views.add(startView);
967        for (int index = startIndex; index < views.size(); index++) {
968            final View view = views.get(index);
969            if (view instanceof ViewGroup) {
970                ViewGroup viewGroup = (ViewGroup) view;
971                final int childCount =  viewGroup.getChildCount();
972                for (int childIndex = 0; childIndex < childCount; childIndex++) {
973                    final View child = viewGroup.getChildAt(childIndex);
974                    if (!containedBeforeIndex(views, child, startIndex)) {
975                        views.add(child);
976                    }
977                }
978            }
979        }
980    }
981
982    /**
983     * Does a linear search through views for view, limited to maxIndex.
984     */
985    private static boolean containedBeforeIndex(final List<View> views, final View view,
986            final int maxIndex) {
987        for (int i = 0; i < maxIndex; i++) {
988            if (views.get(i) == view) {
989                return true;
990            }
991        }
992        return false;
993    }
994
995    /**
996     * After the transition has started, remove all targets that we added to the transitions
997     * so that the transitions are left in a clean state.
998     */
999    private static void scheduleRemoveTargets(final Transition overalTransition,
1000            final Transition enterTransition, final ArrayList<View> enteringViews,
1001            final Transition exitTransition, final ArrayList<View> exitingViews,
1002            final TransitionSet sharedElementTransition, final ArrayList<View> sharedElementsIn) {
1003        overalTransition.addListener(new TransitionListenerAdapter() {
1004            @Override
1005            public void onTransitionStart(Transition transition) {
1006                if (enterTransition != null) {
1007                    replaceTargets(enterTransition, enteringViews, null);
1008                }
1009                if (exitTransition != null) {
1010                    replaceTargets(exitTransition, exitingViews, null);
1011                }
1012                if (sharedElementTransition != null) {
1013                    replaceTargets(sharedElementTransition, sharedElementsIn, null);
1014                }
1015            }
1016        });
1017    }
1018
1019    /**
1020     * This method removes the views from transitions that target ONLY those views and
1021     * replaces them with the new targets list.
1022     * The views list should match those added in addTargets and should contain
1023     * one view that is not in the view hierarchy (state.nonExistentView).
1024     */
1025    public static void replaceTargets(Transition transition, ArrayList<View> oldTargets,
1026            ArrayList<View> newTargets) {
1027        if (transition instanceof TransitionSet) {
1028            TransitionSet set = (TransitionSet) transition;
1029            int numTransitions = set.getTransitionCount();
1030            for (int i = 0; i < numTransitions; i++) {
1031                Transition child = set.getTransitionAt(i);
1032                replaceTargets(child, oldTargets, newTargets);
1033            }
1034        } else if (!hasSimpleTarget(transition)) {
1035            List<View> targets = transition.getTargets();
1036            if (targets != null && targets.size() == oldTargets.size() &&
1037                    targets.containsAll(oldTargets)) {
1038                // We have an exact match. We must have added these earlier in addTargets
1039                final int targetCount = newTargets == null ? 0 : newTargets.size();
1040                for (int i = 0; i < targetCount; i++) {
1041                    transition.addTarget(newTargets.get(i));
1042                }
1043                for (int i = oldTargets.size() - 1; i >= 0; i--) {
1044                    transition.removeTarget(oldTargets.get(i));
1045                }
1046            }
1047        }
1048    }
1049
1050    /**
1051     * This method adds views as targets to the transition, but only if the transition
1052     * doesn't already have a target. It is best for views to contain one View object
1053     * that does not exist in the view hierarchy (state.nonExistentView) so that
1054     * when they are removed later, a list match will suffice to remove the targets.
1055     * Otherwise, if you happened to have targeted the exact views for the transition,
1056     * the replaceTargets call will remove them unexpectedly.
1057     */
1058    public static void addTargets(Transition transition, ArrayList<View> views) {
1059        if (transition == null) {
1060            return;
1061        }
1062        if (transition instanceof TransitionSet) {
1063            TransitionSet set = (TransitionSet) transition;
1064            int numTransitions = set.getTransitionCount();
1065            for (int i = 0; i < numTransitions; i++) {
1066                Transition child = set.getTransitionAt(i);
1067                addTargets(child, views);
1068            }
1069        } else if (!hasSimpleTarget(transition)) {
1070            List<View> targets = transition.getTargets();
1071            if (isNullOrEmpty(targets)) {
1072                // We can just add the target views
1073                int numViews = views.size();
1074                for (int i = 0; i < numViews; i++) {
1075                    transition.addTarget(views.get(i));
1076                }
1077            }
1078        }
1079    }
1080
1081    /**
1082     * Returns true if there are any targets based on ID, transition or type.
1083     */
1084    private static boolean hasSimpleTarget(Transition transition) {
1085        return !isNullOrEmpty(transition.getTargetIds()) ||
1086                !isNullOrEmpty(transition.getTargetNames()) ||
1087                !isNullOrEmpty(transition.getTargetTypes());
1088    }
1089
1090    /**
1091     * Simple utility to detect if a list is null or has no elements.
1092     */
1093    private static boolean isNullOrEmpty(List list) {
1094        return list == null || list.isEmpty();
1095    }
1096
1097    private static ArrayList<View> configureEnteringExitingViews(Transition transition,
1098            Fragment fragment, ArrayList<View> sharedElements, View nonExistentView) {
1099        ArrayList<View> viewList = null;
1100        if (transition != null) {
1101            viewList = new ArrayList<>();
1102            View root = fragment.getView();
1103            if (root != null) {
1104                root.captureTransitioningViews(viewList);
1105            }
1106            if (sharedElements != null) {
1107                viewList.removeAll(sharedElements);
1108            }
1109            if (!viewList.isEmpty()) {
1110                viewList.add(nonExistentView);
1111                addTargets(transition, viewList);
1112            }
1113        }
1114        return viewList;
1115    }
1116
1117    /**
1118     * Sets the visibility of all Views in {@code views} to {@code visibility}.
1119     */
1120    private static void setViewVisibility(ArrayList<View> views, @View.Visibility int visibility) {
1121        if (views == null) {
1122            return;
1123        }
1124        for (int i = views.size() - 1; i >= 0; i--) {
1125            final View view = views.get(i);
1126            view.setVisibility(visibility);
1127        }
1128    }
1129
1130    /**
1131     * Merges exit, shared element, and enter transitions so that they act together or
1132     * sequentially as defined in the fragments.
1133     */
1134    private static Transition mergeTransitions(Transition enterTransition,
1135            Transition exitTransition, Transition sharedElementTransition, Fragment inFragment,
1136            boolean isPop) {
1137        boolean overlap = true;
1138        if (enterTransition != null && exitTransition != null && inFragment != null) {
1139            overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() :
1140                    inFragment.getAllowEnterTransitionOverlap();
1141        }
1142
1143        // Wrap the transitions. Explicit targets like in enter and exit will cause the
1144        // views to be targeted regardless of excluded views. If that happens, then the
1145        // excluded fragments views (hidden fragments) will still be in the transition.
1146
1147        Transition transition;
1148        if (overlap) {
1149            // Regular transition -- do it all together
1150            TransitionSet transitionSet = new TransitionSet();
1151            if (enterTransition != null) {
1152                transitionSet.addTransition(enterTransition);
1153            }
1154            if (exitTransition != null) {
1155                transitionSet.addTransition(exitTransition);
1156            }
1157            if (sharedElementTransition != null) {
1158                transitionSet.addTransition(sharedElementTransition);
1159            }
1160            transition = transitionSet;
1161        } else {
1162            // First do exit, then enter, but allow shared element transition to happen
1163            // during both.
1164            Transition staggered = null;
1165            if (exitTransition != null && enterTransition != null) {
1166                staggered = new TransitionSet()
1167                        .addTransition(exitTransition)
1168                        .addTransition(enterTransition)
1169                        .setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
1170            } else if (exitTransition != null) {
1171                staggered = exitTransition;
1172            } else if (enterTransition != null) {
1173                staggered = enterTransition;
1174            }
1175            if (sharedElementTransition != null) {
1176                TransitionSet together = new TransitionSet();
1177                if (staggered != null) {
1178                    together.addTransition(staggered);
1179                }
1180                together.addTransition(sharedElementTransition);
1181                transition = together;
1182            } else {
1183                transition = staggered;
1184            }
1185        }
1186        return transition;
1187    }
1188
1189    /**
1190     * Finds the first removed fragment and last added fragments when going forward.
1191     * If none of the fragments have transitions, then both lists will be empty.
1192     *
1193     * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
1194     *                               and last fragments to be added. This will be modified by
1195     *                               this method.
1196     */
1197    public static void calculateFragments(BackStackRecord transaction,
1198            SparseArray<FragmentContainerTransition> transitioningFragments,
1199            boolean isReordered) {
1200        final int numOps = transaction.mOps.size();
1201        for (int opNum = 0; opNum < numOps; opNum++) {
1202            final BackStackRecord.Op op = transaction.mOps.get(opNum);
1203            addToFirstInLastOut(transaction, op, transitioningFragments, false, isReordered);
1204        }
1205    }
1206
1207    /**
1208     * Finds the first removed fragment and last added fragments when popping the back stack.
1209     * If none of the fragments have transitions, then both lists will be empty.
1210     *
1211     * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
1212     *                               and last fragments to be added. This will be modified by
1213     *                               this method.
1214     */
1215    public static void calculatePopFragments(BackStackRecord transaction,
1216            SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered) {
1217        if (!transaction.mManager.mContainer.onHasView()) {
1218            return; // nothing to see, so no transitions
1219        }
1220        final int numOps = transaction.mOps.size();
1221        for (int opNum = numOps - 1; opNum >= 0; opNum--) {
1222            final BackStackRecord.Op op = transaction.mOps.get(opNum);
1223            addToFirstInLastOut(transaction, op, transitioningFragments, true, isReordered);
1224        }
1225    }
1226
1227    /**
1228     * Examines the {@code command} and may set the first out or last in fragment for the fragment's
1229     * container.
1230     *
1231     * @param transaction The executing transaction
1232     * @param op The operation being run.
1233     * @param transitioningFragments A structure holding the first in and last out fragments
1234     *                               for each fragment container.
1235     * @param isPop Is the operation a pop?
1236     * @param isReorderedTransaction True if the operations have been partially executed and the
1237     *                               added fragments have Views in the hierarchy or false if the
1238     *                               operations haven't been executed yet.
1239     */
1240    @SuppressWarnings("ReferenceEquality")
1241    private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op,
1242            SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop,
1243            boolean isReorderedTransaction) {
1244        final Fragment fragment = op.fragment;
1245        if (fragment == null) {
1246            return; // no fragment, no transition
1247        }
1248        final int containerId = fragment.mContainerId;
1249        if (containerId == 0) {
1250            return; // no container, no transition
1251        }
1252        final int command = isPop ? INVERSE_OPS[op.cmd] : op.cmd;
1253        boolean setLastIn = false;
1254        boolean wasRemoved = false;
1255        boolean setFirstOut = false;
1256        boolean wasAdded = false;
1257        switch (command) {
1258            case BackStackRecord.OP_SHOW:
1259                if (isReorderedTransaction) {
1260                    setLastIn = fragment.mHiddenChanged && !fragment.mHidden &&
1261                            fragment.mAdded;
1262                } else {
1263                    setLastIn = fragment.mHidden;
1264                }
1265                wasAdded = true;
1266                break;
1267            case BackStackRecord.OP_ADD:
1268            case BackStackRecord.OP_ATTACH:
1269                if (isReorderedTransaction) {
1270                    setLastIn = fragment.mIsNewlyAdded;
1271                } else {
1272                    setLastIn = !fragment.mAdded && !fragment.mHidden;
1273                }
1274                wasAdded = true;
1275                break;
1276            case BackStackRecord.OP_HIDE:
1277                if (isReorderedTransaction) {
1278                    setFirstOut = fragment.mHiddenChanged && fragment.mAdded &&
1279                            fragment.mHidden;
1280                } else {
1281                    setFirstOut = fragment.mAdded && !fragment.mHidden;
1282                }
1283                wasRemoved = true;
1284                break;
1285            case BackStackRecord.OP_REMOVE:
1286            case BackStackRecord.OP_DETACH:
1287                if (isReorderedTransaction) {
1288                    setFirstOut = !fragment.mAdded && fragment.mView != null
1289                            && fragment.mView.getVisibility() == View.VISIBLE
1290                            && fragment.mView.getTransitionAlpha() > 0;
1291                } else {
1292                    setFirstOut = fragment.mAdded && !fragment.mHidden;
1293                }
1294                wasRemoved = true;
1295                break;
1296        }
1297        FragmentContainerTransition containerTransition = transitioningFragments.get(containerId);
1298        if (setLastIn) {
1299            containerTransition =
1300                    ensureContainer(containerTransition, transitioningFragments, containerId);
1301            containerTransition.lastIn = fragment;
1302            containerTransition.lastInIsPop = isPop;
1303            containerTransition.lastInTransaction = transaction;
1304        }
1305        if (!isReorderedTransaction && wasAdded) {
1306            if (containerTransition != null && containerTransition.firstOut == fragment) {
1307                containerTransition.firstOut = null;
1308            }
1309
1310            /*
1311             * Ensure that fragments that are entering are at least at the CREATED state
1312             * so that they may load Transitions using TransitionInflater.
1313             */
1314            FragmentManagerImpl manager = transaction.mManager;
1315            if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED &&
1316                    manager.mHost.getContext().getApplicationInfo().targetSdkVersion >=
1317                            Build.VERSION_CODES.N && !transaction.mReorderingAllowed) {
1318                manager.makeActive(fragment);
1319                manager.moveToState(fragment, Fragment.CREATED, 0, 0, false);
1320            }
1321        }
1322        if (setFirstOut && (containerTransition == null || containerTransition.firstOut == null)) {
1323            containerTransition =
1324                    ensureContainer(containerTransition, transitioningFragments, containerId);
1325            containerTransition.firstOut = fragment;
1326            containerTransition.firstOutIsPop = isPop;
1327            containerTransition.firstOutTransaction = transaction;
1328        }
1329
1330        if (!isReorderedTransaction && wasRemoved &&
1331                (containerTransition != null && containerTransition.lastIn == fragment)) {
1332            containerTransition.lastIn = null;
1333        }
1334    }
1335
1336    /**
1337     * Ensures that a FragmentContainerTransition has been added to the SparseArray. If so,
1338     * it returns the existing one. If not, one is created and added to the SparseArray and
1339     * returned.
1340     */
1341    private static FragmentContainerTransition ensureContainer(
1342            FragmentContainerTransition containerTransition,
1343            SparseArray<FragmentContainerTransition> transitioningFragments, int containerId) {
1344        if (containerTransition == null) {
1345            containerTransition = new FragmentContainerTransition();
1346            transitioningFragments.put(containerId, containerTransition);
1347        }
1348        return containerTransition;
1349    }
1350
1351    /**
1352     * Tracks the last fragment added and first fragment removed for fragment transitions.
1353     * This also tracks which fragments are changed by push or pop transactions.
1354     */
1355    public static class FragmentContainerTransition {
1356        /**
1357         * The last fragment added/attached/shown in its container
1358         */
1359        public Fragment lastIn;
1360
1361        /**
1362         * true when lastIn was added during a pop transaction or false if added with a push
1363         */
1364        public boolean lastInIsPop;
1365
1366        /**
1367         * The transaction that included the last in fragment
1368         */
1369        public BackStackRecord lastInTransaction;
1370
1371        /**
1372         * The first fragment with a View that was removed/detached/hidden in its container.
1373         */
1374        public Fragment firstOut;
1375
1376        /**
1377         * true when firstOut was removed during a pop transaction or false otherwise
1378         */
1379        public boolean firstOutIsPop;
1380
1381        /**
1382         * The transaction that included the first out fragment
1383         */
1384        public BackStackRecord firstOutTransaction;
1385    }
1386}
1387