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.support.v4.app;
17
18import android.graphics.Rect;
19import android.os.Build;
20import android.support.annotation.RequiresApi;
21import android.support.v4.util.ArrayMap;
22import android.support.v4.view.ViewCompat;
23import android.util.SparseArray;
24import android.view.View;
25import android.view.ViewGroup;
26
27import java.util.ArrayList;
28import java.util.Collection;
29import java.util.List;
30import java.util.Map;
31
32/**
33 * Contains the Fragment Transition functionality for both ordered and reordered
34 * Fragment Transactions. With reordered fragment transactions, all Views have been
35 * added to the View hierarchy prior to calling startTransitions. With ordered
36 * fragment transactions, Views will be removed and added after calling startTransitions.
37 */
38class FragmentTransition {
39    /**
40     * The inverse of all BackStackRecord operation commands. This assumes that
41     * REPLACE operations have already been replaced by add/remove operations.
42     */
43    private static final int[] INVERSE_OPS = {
44            BackStackRecord.OP_NULL,              // inverse of OP_NULL (error)
45            BackStackRecord.OP_REMOVE,            // inverse of OP_ADD
46            BackStackRecord.OP_NULL,              // inverse of OP_REPLACE (error)
47            BackStackRecord.OP_ADD,               // inverse of OP_REMOVE
48            BackStackRecord.OP_SHOW,              // inverse of OP_HIDE
49            BackStackRecord.OP_HIDE,              // inverse of OP_SHOW
50            BackStackRecord.OP_ATTACH,            // inverse of OP_DETACH
51            BackStackRecord.OP_DETACH,            // inverse of OP_ATTACH
52            BackStackRecord.OP_UNSET_PRIMARY_NAV, // inverse of OP_SET_PRIMARY_NAV
53            BackStackRecord.OP_SET_PRIMARY_NAV,   // inverse of OP_UNSET_PRIMARY_NAV
54    };
55
56    /**
57     * The main entry point for Fragment Transitions, this starts the transitions
58     * set on the leaving Fragment's {@link Fragment#getExitTransition()}, the
59     * entering Fragment's {@link Fragment#getEnterTransition()} and
60     * {@link Fragment#getSharedElementEnterTransition()}. When popping,
61     * the leaving Fragment's {@link Fragment#getReturnTransition()} and
62     * {@link Fragment#getSharedElementReturnTransition()} and the entering
63     * {@link Fragment#getReenterTransition()} will be run.
64     * <p>
65     * With reordered Fragment Transitions, all Views have been added to the
66     * View hierarchy prior to calling this method. The incoming Fragment's Views
67     * will be INVISIBLE. With ordered Fragment Transitions, this method
68     * is called before any change has been made to the hierarchy. That means
69     * that the added Fragments have not created their Views yet and the hierarchy
70     * is unknown.
71     *
72     * @param fragmentManager The executing FragmentManagerImpl
73     * @param records The list of transactions being executed.
74     * @param isRecordPop For each transaction, whether it is a pop transaction or not.
75     * @param startIndex The first index into records and isRecordPop to execute as
76     *                   part of this transition.
77     * @param endIndex One past the last index into records and isRecordPop to execute
78     *                 as part of this transition.
79     * @param isReordered true if this is a reordered transaction, meaning that the
80     *                    Views of incoming fragments have been added. false if the
81     *                    transaction has yet to be run and Views haven't been created.
82     */
83    static void startTransitions(FragmentManagerImpl fragmentManager,
84            ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
85            int startIndex, int endIndex, boolean isReordered) {
86        if (fragmentManager.mCurState < Fragment.CREATED) {
87            return;
88        }
89
90        if (Build.VERSION.SDK_INT >= 21) {
91            SparseArray<FragmentContainerTransition> transitioningFragments =
92                    new SparseArray<>();
93            for (int i = startIndex; i < endIndex; i++) {
94                final BackStackRecord record = records.get(i);
95                final boolean isPop = isRecordPop.get(i);
96                if (isPop) {
97                    calculatePopFragments(record, transitioningFragments, isReordered);
98                } else {
99                    calculateFragments(record, transitioningFragments, isReordered);
100                }
101            }
102
103            if (transitioningFragments.size() != 0) {
104                final View nonExistentView = new View(fragmentManager.mHost.getContext());
105                final int numContainers = transitioningFragments.size();
106                for (int i = 0; i < numContainers; i++) {
107                    int containerId = transitioningFragments.keyAt(i);
108                    ArrayMap<String, String> nameOverrides = calculateNameOverrides(containerId,
109                            records, isRecordPop, startIndex, endIndex);
110
111                    FragmentContainerTransition containerTransition =
112                            transitioningFragments.valueAt(i);
113
114                    if (isReordered) {
115                        configureTransitionsReordered(fragmentManager, containerId,
116                                containerTransition, nonExistentView, nameOverrides);
117                    } else {
118                        configureTransitionsOrdered(fragmentManager, containerId,
119                                containerTransition, nonExistentView, nameOverrides);
120                    }
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    @RequiresApi(21)
193    private static void configureTransitionsReordered(FragmentManagerImpl fragmentManager,
194            int containerId, FragmentContainerTransition fragments,
195            View nonExistentView, ArrayMap<String, String> nameOverrides) {
196        ViewGroup sceneRoot = null;
197        if (fragmentManager.mContainer.onHasView()) {
198            sceneRoot = (ViewGroup) fragmentManager.mContainer.onFindViewById(containerId);
199        }
200        if (sceneRoot == null) {
201            return;
202        }
203        final Fragment inFragment = fragments.lastIn;
204        final Fragment outFragment = fragments.firstOut;
205        final boolean inIsPop = fragments.lastInIsPop;
206        final boolean outIsPop = fragments.firstOutIsPop;
207
208        ArrayList<View> sharedElementsIn = new ArrayList<>();
209        ArrayList<View> sharedElementsOut = new ArrayList<>();
210        Object enterTransition = getEnterTransition(inFragment, inIsPop);
211        Object exitTransition = getExitTransition(outFragment, outIsPop);
212
213        Object sharedElementTransition = configureSharedElementsReordered(sceneRoot,
214                nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,
215                enterTransition, exitTransition);
216
217        if (enterTransition == null && sharedElementTransition == null
218                && exitTransition == null) {
219            return; // no transitions!
220        }
221
222        ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition,
223                outFragment, sharedElementsOut, nonExistentView);
224
225        ArrayList<View> enteringViews = configureEnteringExitingViews(enterTransition,
226                inFragment, sharedElementsIn, nonExistentView);
227
228        setViewVisibility(enteringViews, View.INVISIBLE);
229
230        Object transition = mergeTransitions(enterTransition, exitTransition,
231                sharedElementTransition, inFragment, inIsPop);
232
233        if (transition != null) {
234            replaceHide(exitTransition, outFragment, exitingViews);
235            ArrayList<String> inNames =
236                    FragmentTransitionCompat21.prepareSetNameOverridesReordered(sharedElementsIn);
237            FragmentTransitionCompat21.scheduleRemoveTargets(transition,
238                    enterTransition, enteringViews, exitTransition, exitingViews,
239                    sharedElementTransition, sharedElementsIn);
240            FragmentTransitionCompat21.beginDelayedTransition(sceneRoot, transition);
241            FragmentTransitionCompat21.setNameOverridesReordered(sceneRoot, sharedElementsOut,
242                    sharedElementsIn, inNames, nameOverrides);
243            setViewVisibility(enteringViews, View.VISIBLE);
244            FragmentTransitionCompat21.swapSharedElementTargets(sharedElementTransition,
245                    sharedElementsOut, sharedElementsIn);
246        }
247    }
248
249    /**
250     * Replace hide operations with visibility changes on the exiting views. Instead of making
251     * the entire fragment's view GONE, make each exiting view INVISIBLE. At the end of the
252     * transition, make the fragment's view GONE.
253     */
254    @RequiresApi(21)
255    private static void replaceHide(Object exitTransition, Fragment exitingFragment,
256            final ArrayList<View> exitingViews) {
257        if (exitingFragment != null && exitTransition != null && exitingFragment.mAdded
258                && exitingFragment.mHidden && exitingFragment.mHiddenChanged) {
259            exitingFragment.setHideReplaced(true);
260            FragmentTransitionCompat21.scheduleHideFragmentView(exitTransition,
261                    exitingFragment.getView(), exitingViews);
262            final ViewGroup container = exitingFragment.mContainer;
263            OneShotPreDrawListener.add(container, new Runnable() {
264                @Override
265                public void run() {
266                    setViewVisibility(exitingViews, View.INVISIBLE);
267                }
268            });
269        }
270    }
271
272    /**
273     * Configures a transition for a single fragment container for which the transaction was
274     * ordrered. That means that the transaction has not been executed yet, so incoming
275     * Views are not yet known.
276     *
277     * @param fragmentManager The executing FragmentManagerImpl
278     * @param containerId The container ID that is executing the transition.
279     * @param fragments A structure holding the transitioning fragments in this container.
280     * @param nonExistentView A View that does not exist in the hierarchy. This is used to
281     *                        prevent transitions from acting on other Views when there is no
282     *                        other target.
283     * @param nameOverrides A map of the shared element names from the starting fragment to
284     *                      the final fragment's Views as given in
285     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
286     */
287    @RequiresApi(21)
288    private static void configureTransitionsOrdered(FragmentManagerImpl fragmentManager,
289            int containerId, FragmentContainerTransition fragments,
290            View nonExistentView, ArrayMap<String, String> nameOverrides) {
291        ViewGroup sceneRoot = null;
292        if (fragmentManager.mContainer.onHasView()) {
293            sceneRoot = (ViewGroup) fragmentManager.mContainer.onFindViewById(containerId);
294        }
295        if (sceneRoot == null) {
296            return;
297        }
298        final Fragment inFragment = fragments.lastIn;
299        final Fragment outFragment = fragments.firstOut;
300        final boolean inIsPop = fragments.lastInIsPop;
301        final boolean outIsPop = fragments.firstOutIsPop;
302
303        Object enterTransition = getEnterTransition(inFragment, inIsPop);
304        Object exitTransition = getExitTransition(outFragment, outIsPop);
305
306        ArrayList<View> sharedElementsOut = new ArrayList<>();
307        ArrayList<View> sharedElementsIn = new ArrayList<>();
308
309        Object sharedElementTransition = configureSharedElementsOrdered(sceneRoot,
310                nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,
311                enterTransition, exitTransition);
312
313        if (enterTransition == null && sharedElementTransition == null
314                && exitTransition == null) {
315            return; // no transitions!
316        }
317
318        ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition,
319                outFragment, sharedElementsOut, nonExistentView);
320
321        if (exitingViews == null || exitingViews.isEmpty()) {
322            exitTransition = null;
323        }
324
325        // Ensure the entering transition doesn't target anything until the views are made
326        // visible
327        FragmentTransitionCompat21.addTarget(enterTransition, nonExistentView);
328
329        Object transition = mergeTransitions(enterTransition, exitTransition,
330                sharedElementTransition, inFragment, fragments.lastInIsPop);
331
332        if (transition != null) {
333            final ArrayList<View> enteringViews = new ArrayList<>();
334            FragmentTransitionCompat21.scheduleRemoveTargets(transition,
335                    enterTransition, enteringViews, exitTransition, exitingViews,
336                    sharedElementTransition, sharedElementsIn);
337            scheduleTargetChange(sceneRoot, inFragment, nonExistentView, sharedElementsIn,
338                    enterTransition, enteringViews, exitTransition, exitingViews);
339            FragmentTransitionCompat21.setNameOverridesOrdered(sceneRoot, sharedElementsIn,
340                    nameOverrides);
341
342            FragmentTransitionCompat21.beginDelayedTransition(sceneRoot, transition);
343            FragmentTransitionCompat21.scheduleNameReset(sceneRoot, sharedElementsIn,
344                    nameOverrides);
345        }
346    }
347
348    /**
349     * This method is used for fragment transitions for ordrered transactions to change the
350     * enter and exit transition targets after the call to
351     * {@link FragmentTransitionCompat21#beginDelayedTransition(ViewGroup, Object)}. The exit
352     * transition must ensure that it does not target any Views and the enter transition must start
353     * targeting 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    @RequiresApi(21)
366    private static void scheduleTargetChange(final ViewGroup sceneRoot,
367            final Fragment inFragment, final View nonExistentView,
368            final ArrayList<View> sharedElementsIn,
369            final Object enterTransition, final ArrayList<View> enteringViews,
370            final Object exitTransition, final ArrayList<View> exitingViews) {
371        OneShotPreDrawListener.add(sceneRoot, new Runnable() {
372            @Override
373            public void run() {
374                if (enterTransition != null) {
375                    FragmentTransitionCompat21.removeTarget(enterTransition,
376                            nonExistentView);
377                    ArrayList<View> views = configureEnteringExitingViews(
378                            enterTransition, inFragment, sharedElementsIn, nonExistentView);
379                    enteringViews.addAll(views);
380                }
381
382                if (exitingViews != null) {
383                    if (exitTransition != null) {
384                        ArrayList<View> tempExiting = new ArrayList<>();
385                        tempExiting.add(nonExistentView);
386                        FragmentTransitionCompat21.replaceTargets(exitTransition, exitingViews,
387                                tempExiting);
388                    }
389                    exitingViews.clear();
390                    exitingViews.add(nonExistentView);
391                }
392            }
393        });
394    }
395
396    /**
397     * Returns a TransitionSet containing the shared element transition. The wrapping TransitionSet
398     * targets all shared elements to ensure that no other Views are targeted. The shared element
399     * transition can then target any or all shared elements without worrying about accidentally
400     * targeting entering or exiting Views.
401     *
402     * @param inFragment The incoming fragment
403     * @param outFragment the outgoing fragment
404     * @param isPop True if this is a pop transaction or false if it is a normal (add) transaction.
405     * @return A TransitionSet wrapping the shared element transition or null if no such transition
406     * exists.
407     */
408    @RequiresApi(21)
409    private static Object getSharedElementTransition(Fragment inFragment,
410            Fragment outFragment, boolean isPop) {
411        if (inFragment == null || outFragment == null) {
412            return null;
413        }
414        Object transition = FragmentTransitionCompat21.cloneTransition(isPop
415                ? outFragment.getSharedElementReturnTransition()
416                : inFragment.getSharedElementEnterTransition());
417        return FragmentTransitionCompat21.wrapTransitionInSet(transition);
418    }
419
420    /**
421     * Returns a clone of the enter transition or null if no such transition exists.
422     */
423    @RequiresApi(21)
424    private static Object getEnterTransition(Fragment inFragment, boolean isPop) {
425        if (inFragment == null) {
426            return null;
427        }
428        return FragmentTransitionCompat21.cloneTransition(isPop
429                ? inFragment.getReenterTransition()
430                : inFragment.getEnterTransition());
431    }
432
433    /**
434     * Returns a clone of the exit transition or null if no such transition exists.
435     */
436    @RequiresApi(21)
437    private static Object getExitTransition(Fragment outFragment, boolean isPop) {
438        if (outFragment == null) {
439            return null;
440        }
441        return FragmentTransitionCompat21.cloneTransition(isPop
442                ? outFragment.getReturnTransition()
443                : outFragment.getExitTransition());
444    }
445
446    /**
447     * Configures the shared elements of a reordered fragment transaction's transition.
448     * This retrieves the shared elements of the outgoing and incoming fragments, maps the
449     * views, and sets up the epicenter on the transitions.
450     * <p>
451     * The epicenter of exit and shared element transitions is the first shared element
452     * in the outgoing fragment. The epicenter of the entering transition is the first shared
453     * element in the incoming fragment.
454     *
455     * @param sceneRoot The fragment container View
456     * @param nonExistentView A View that does not exist in the hierarchy. This is used to
457     *                        prevent transitions from acting on other Views when there is no
458     *                        other target.
459     * @param nameOverrides A map of the shared element names from the starting fragment to
460     *                      the final fragment's Views as given in
461     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
462     * @param fragments A structure holding the transitioning fragments in this container.
463     * @param sharedElementsOut A list modified to contain the shared elements in the outgoing
464     *                          fragment
465     * @param sharedElementsIn A list modified to contain the shared elements in the incoming
466     *                         fragment
467     * @param enterTransition The transition used for entering Views, modified by applying the
468     *                        epicenter
469     * @param exitTransition The transition used for exiting Views, modified by applying the
470     *                       epicenter
471     * @return The shared element transition or null if no shared elements exist
472     */
473    @RequiresApi(21)
474    private static Object configureSharedElementsReordered(final ViewGroup sceneRoot,
475            final View nonExistentView, final ArrayMap<String, String> nameOverrides,
476            final FragmentContainerTransition fragments,
477            final ArrayList<View> sharedElementsOut,
478            final ArrayList<View> sharedElementsIn,
479            final Object enterTransition, final Object exitTransition) {
480        final Fragment inFragment = fragments.lastIn;
481        final Fragment outFragment = fragments.firstOut;
482        if (inFragment != null) {
483            inFragment.getView().setVisibility(View.VISIBLE);
484        }
485        if (inFragment == null || outFragment == null) {
486            return null; // no shared element without a fragment
487        }
488
489        final boolean inIsPop = fragments.lastInIsPop;
490        Object sharedElementTransition = nameOverrides.isEmpty() ? null
491                : getSharedElementTransition(inFragment, outFragment, inIsPop);
492
493        final ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides,
494                sharedElementTransition, fragments);
495
496        final ArrayMap<String, View> inSharedElements = captureInSharedElements(nameOverrides,
497                sharedElementTransition, fragments);
498
499        if (nameOverrides.isEmpty()) {
500            sharedElementTransition = null;
501            if (outSharedElements != null) {
502                outSharedElements.clear();
503            }
504            if (inSharedElements != null) {
505                inSharedElements.clear();
506            }
507        } else {
508            addSharedElementsWithMatchingNames(sharedElementsOut, outSharedElements,
509                    nameOverrides.keySet());
510            addSharedElementsWithMatchingNames(sharedElementsIn, inSharedElements,
511                    nameOverrides.values());
512        }
513
514        if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
515            // don't call onSharedElementStart/End since there is no transition
516            return null;
517        }
518
519        callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);
520
521        final Rect epicenter;
522        final View epicenterView;
523        if (sharedElementTransition != null) {
524            sharedElementsIn.add(nonExistentView);
525            FragmentTransitionCompat21.setSharedElementTargets(sharedElementTransition,
526                    nonExistentView, sharedElementsOut);
527            final boolean outIsPop = fragments.firstOutIsPop;
528            final BackStackRecord outTransaction = fragments.firstOutTransaction;
529            setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop,
530                    outTransaction);
531            epicenter = new Rect();
532            epicenterView = getInEpicenterView(inSharedElements, fragments,
533                    enterTransition, inIsPop);
534            if (epicenterView != null) {
535                FragmentTransitionCompat21.setEpicenter(enterTransition, epicenter);
536            }
537        } else {
538            epicenter = null;
539            epicenterView = null;
540        }
541
542        OneShotPreDrawListener.add(sceneRoot, new Runnable() {
543            @Override
544            public void run() {
545                callSharedElementStartEnd(inFragment, outFragment, inIsPop,
546                        inSharedElements, false);
547                if (epicenterView != null) {
548                    FragmentTransitionCompat21.getBoundsOnScreen(epicenterView, epicenter);
549                }
550            }
551        });
552        return sharedElementTransition;
553    }
554
555    /**
556     * Add Views from sharedElements into views that have the transitionName in the
557     * nameOverridesSet.
558     *
559     * @param views               Views list to add shared elements to
560     * @param sharedElements      List of shared elements
561     * @param nameOverridesSet    The transition names for all views to be copied from
562     *                            sharedElements to views.
563     */
564    private static void addSharedElementsWithMatchingNames(ArrayList<View> views,
565            ArrayMap<String, View> sharedElements, Collection<String> nameOverridesSet) {
566        for (int i = sharedElements.size() - 1; i >= 0; i--) {
567            View view = sharedElements.valueAt(i);
568            if (nameOverridesSet.contains(ViewCompat.getTransitionName(view))) {
569                views.add(view);
570            }
571        }
572    }
573
574    /**
575     * Configures the shared elements of an ordered fragment transaction's transition.
576     * This retrieves the shared elements of the incoming fragments, and schedules capturing
577     * the incoming fragment's shared elements. It also maps the views, and sets up the epicenter
578     * on the transitions.
579     * <p>
580     * The epicenter of exit and shared element transitions is the first shared element
581     * in the outgoing fragment. The epicenter of the entering transition is the first shared
582     * element in the incoming fragment.
583     *
584     * @param sceneRoot The fragment container View
585     * @param nonExistentView A View that does not exist in the hierarchy. This is used to
586     *                        prevent transitions from acting on other Views when there is no
587     *                        other target.
588     * @param nameOverrides A map of the shared element names from the starting fragment to
589     *                      the final fragment's Views as given in
590     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
591     * @param fragments A structure holding the transitioning fragments in this container.
592     * @param sharedElementsOut A list modified to contain the shared elements in the outgoing
593     *                          fragment
594     * @param sharedElementsIn A list modified to contain the shared elements in the incoming
595     *                         fragment
596     * @param enterTransition The transition used for entering Views, modified by applying the
597     *                        epicenter
598     * @param exitTransition The transition used for exiting Views, modified by applying the
599     *                       epicenter
600     * @return The shared element transition or null if no shared elements exist
601     */
602    @RequiresApi(21)
603    private static Object configureSharedElementsOrdered(final ViewGroup sceneRoot,
604            final View nonExistentView, final ArrayMap<String, String> nameOverrides,
605            final FragmentContainerTransition fragments,
606            final ArrayList<View> sharedElementsOut,
607            final ArrayList<View> sharedElementsIn,
608            final Object enterTransition, final Object exitTransition) {
609        final Fragment inFragment = fragments.lastIn;
610        final Fragment outFragment = fragments.firstOut;
611
612        if (inFragment == null || outFragment == null) {
613            return null; // no transition
614        }
615
616        final boolean inIsPop = fragments.lastInIsPop;
617        Object sharedElementTransition = nameOverrides.isEmpty() ? null
618                : getSharedElementTransition(inFragment, outFragment, inIsPop);
619
620        ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides,
621                sharedElementTransition, fragments);
622
623        if (nameOverrides.isEmpty()) {
624            sharedElementTransition = null;
625        } else {
626            sharedElementsOut.addAll(outSharedElements.values());
627        }
628
629        if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
630            // don't call onSharedElementStart/End since there is no transition
631            return null;
632        }
633
634        callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);
635
636        final Rect inEpicenter;
637        if (sharedElementTransition != null) {
638            inEpicenter = new Rect();
639            FragmentTransitionCompat21.setSharedElementTargets(sharedElementTransition,
640                    nonExistentView, sharedElementsOut);
641            final boolean outIsPop = fragments.firstOutIsPop;
642            final BackStackRecord outTransaction = fragments.firstOutTransaction;
643            setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop,
644                    outTransaction);
645            if (enterTransition != null) {
646                FragmentTransitionCompat21.setEpicenter(enterTransition, inEpicenter);
647            }
648        } else {
649            inEpicenter = null;
650        }
651
652
653        final Object finalSharedElementTransition = sharedElementTransition;
654        OneShotPreDrawListener.add(sceneRoot, new Runnable() {
655            @Override
656            public void run() {
657                ArrayMap<String, View> inSharedElements = captureInSharedElements(
658                        nameOverrides, finalSharedElementTransition, fragments);
659
660                if (inSharedElements != null) {
661                    sharedElementsIn.addAll(inSharedElements.values());
662                    sharedElementsIn.add(nonExistentView);
663                }
664
665                callSharedElementStartEnd(inFragment, outFragment, inIsPop,
666                        inSharedElements, false);
667                if (finalSharedElementTransition != null) {
668                    FragmentTransitionCompat21.swapSharedElementTargets(
669                            finalSharedElementTransition, sharedElementsOut,
670                            sharedElementsIn);
671
672                    final View inEpicenterView = getInEpicenterView(inSharedElements,
673                            fragments, enterTransition, inIsPop);
674                    if (inEpicenterView != null) {
675                        FragmentTransitionCompat21.getBoundsOnScreen(inEpicenterView,
676                                inEpicenter);
677                    }
678                }
679            }
680        });
681
682        return sharedElementTransition;
683    }
684
685    /**
686     * Finds the shared elements in the outgoing fragment. It also calls
687     * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control
688     * of the shared element mapping. {@code nameOverrides} is updated to match the
689     * actual transition name of the mapped shared elements.
690     *
691     * @param nameOverrides A map of the shared element names from the starting fragment to
692     *                      the final fragment's Views as given in
693     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
694     * @param sharedElementTransition The shared element transition
695     * @param fragments A structure holding the transitioning fragments in this container.
696     * @return The mapping of shared element names to the Views in the hierarchy or null
697     * if there is no shared element transition.
698     */
699    @RequiresApi(21)
700    private static ArrayMap<String, View> captureOutSharedElements(
701            ArrayMap<String, String> nameOverrides, Object sharedElementTransition,
702            FragmentContainerTransition fragments) {
703        if (nameOverrides.isEmpty() || sharedElementTransition == null) {
704            nameOverrides.clear();
705            return null;
706        }
707        final Fragment outFragment = fragments.firstOut;
708        final ArrayMap<String, View> outSharedElements = new ArrayMap<>();
709        FragmentTransitionCompat21.findNamedViews(outSharedElements, outFragment.getView());
710
711        final SharedElementCallback sharedElementCallback;
712        final ArrayList<String> names;
713        final BackStackRecord outTransaction = fragments.firstOutTransaction;
714        if (fragments.firstOutIsPop) {
715            sharedElementCallback = outFragment.getEnterTransitionCallback();
716            names = outTransaction.mSharedElementTargetNames;
717        } else {
718            sharedElementCallback = outFragment.getExitTransitionCallback();
719            names = outTransaction.mSharedElementSourceNames;
720        }
721
722        outSharedElements.retainAll(names);
723        if (sharedElementCallback != null) {
724            sharedElementCallback.onMapSharedElements(names, outSharedElements);
725            for (int i = names.size() - 1; i >= 0; i--) {
726                String name = names.get(i);
727                View view = outSharedElements.get(name);
728                if (view == null) {
729                    nameOverrides.remove(name);
730                } else if (!name.equals(ViewCompat.getTransitionName(view))) {
731                    String targetValue = nameOverrides.remove(name);
732                    nameOverrides.put(ViewCompat.getTransitionName(view), targetValue);
733                }
734            }
735        } else {
736            nameOverrides.retainAll(outSharedElements.keySet());
737        }
738        return outSharedElements;
739    }
740
741    /**
742     * Finds the shared elements in the incoming fragment. It also calls
743     * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control
744     * of the shared element mapping. {@code nameOverrides} is updated to match the
745     * actual transition name of the mapped shared elements.
746     *
747     * @param nameOverrides A map of the shared element names from the starting fragment to
748     *                      the final fragment's Views as given in
749     *                      {@link FragmentTransaction#addSharedElement(View, String)}.
750     * @param sharedElementTransition The shared element transition
751     * @param fragments A structure holding the transitioning fragments in this container.
752     * @return The mapping of shared element names to the Views in the hierarchy or null
753     * if there is no shared element transition.
754     */
755    @RequiresApi(21)
756    private static ArrayMap<String, View> captureInSharedElements(
757            ArrayMap<String, String> nameOverrides, Object sharedElementTransition,
758            FragmentContainerTransition fragments) {
759        Fragment inFragment = fragments.lastIn;
760        final View fragmentView = inFragment.getView();
761        if (nameOverrides.isEmpty() || sharedElementTransition == null || fragmentView == null) {
762            nameOverrides.clear();
763            return null;
764        }
765        final ArrayMap<String, View> inSharedElements = new ArrayMap<>();
766        FragmentTransitionCompat21.findNamedViews(inSharedElements, fragmentView);
767
768        final SharedElementCallback sharedElementCallback;
769        final ArrayList<String> names;
770        final BackStackRecord inTransaction = fragments.lastInTransaction;
771        if (fragments.lastInIsPop) {
772            sharedElementCallback = inFragment.getExitTransitionCallback();
773            names = inTransaction.mSharedElementSourceNames;
774        } else {
775            sharedElementCallback = inFragment.getEnterTransitionCallback();
776            names = inTransaction.mSharedElementTargetNames;
777        }
778
779        if (names != null) {
780            inSharedElements.retainAll(names);
781        }
782        if (sharedElementCallback != null) {
783            sharedElementCallback.onMapSharedElements(names, inSharedElements);
784            for (int i = names.size() - 1; i >= 0; i--) {
785                String name = names.get(i);
786                View view = inSharedElements.get(name);
787                if (view == null) {
788                    String key = findKeyForValue(nameOverrides, name);
789                    if (key != null) {
790                        nameOverrides.remove(key);
791                    }
792                } else if (!name.equals(ViewCompat.getTransitionName(view))) {
793                    String key = findKeyForValue(nameOverrides, name);
794                    if (key != null) {
795                        nameOverrides.put(key, ViewCompat.getTransitionName(view));
796                    }
797                }
798            }
799        } else {
800            retainValues(nameOverrides, inSharedElements);
801        }
802        return inSharedElements;
803    }
804
805    /**
806     * Utility to find the String key in {@code map} that maps to {@code value}.
807     */
808    private static String findKeyForValue(ArrayMap<String, String> map, String value) {
809        final int numElements = map.size();
810        for (int i = 0; i < numElements; i++) {
811            if (value.equals(map.valueAt(i))) {
812                return map.keyAt(i);
813            }
814        }
815        return null;
816    }
817
818    /**
819     * Returns the View in the incoming Fragment that should be used as the epicenter.
820     *
821     * @param inSharedElements The mapping of shared element names to Views in the
822     *                         incoming fragment.
823     * @param fragments A structure holding the transitioning fragments in this container.
824     * @param enterTransition The transition used for the incoming Fragment's views
825     * @param inIsPop Is the incoming fragment being added as a pop transaction?
826     */
827    private static View getInEpicenterView(ArrayMap<String, View> inSharedElements,
828            FragmentContainerTransition fragments,
829            Object enterTransition, boolean inIsPop) {
830        BackStackRecord inTransaction = fragments.lastInTransaction;
831        if (enterTransition != null && inSharedElements != null
832                && inTransaction.mSharedElementSourceNames != null
833                && !inTransaction.mSharedElementSourceNames.isEmpty()) {
834            final String targetName = inIsPop
835                    ? inTransaction.mSharedElementSourceNames.get(0)
836                    : inTransaction.mSharedElementTargetNames.get(0);
837            return inSharedElements.get(targetName);
838        }
839        return null;
840    }
841
842    /**
843     * Sets the epicenter for the exit transition.
844     *
845     * @param sharedElementTransition The shared element transition
846     * @param exitTransition The transition for the outgoing fragment's views
847     * @param outSharedElements Shared elements in the outgoing fragment
848     * @param outIsPop Is the outgoing fragment being removed as a pop transaction?
849     * @param outTransaction The transaction that caused the fragment to be removed.
850     */
851    @RequiresApi(21)
852    private static void setOutEpicenter(Object sharedElementTransition,
853            Object exitTransition, ArrayMap<String, View> outSharedElements, boolean outIsPop,
854            BackStackRecord outTransaction) {
855        if (outTransaction.mSharedElementSourceNames != null
856                && !outTransaction.mSharedElementSourceNames.isEmpty()) {
857            final String sourceName = outIsPop
858                    ? outTransaction.mSharedElementTargetNames.get(0)
859                    : outTransaction.mSharedElementSourceNames.get(0);
860            final View outEpicenterView = outSharedElements.get(sourceName);
861            FragmentTransitionCompat21.setEpicenter(sharedElementTransition, outEpicenterView);
862
863            if (exitTransition != null) {
864                FragmentTransitionCompat21.setEpicenter(exitTransition, outEpicenterView);
865            }
866        }
867    }
868
869    /**
870     * A utility to retain only the mappings in {@code nameOverrides} that have a value
871     * that has a key in {@code namedViews}. This is a useful equivalent to
872     * {@link ArrayMap#retainAll(Collection)} for values.
873     */
874    private static void retainValues(ArrayMap<String, String> nameOverrides,
875            ArrayMap<String, View> namedViews) {
876        for (int i = nameOverrides.size() - 1; i >= 0; i--) {
877            final String targetName = nameOverrides.valueAt(i);
878            if (!namedViews.containsKey(targetName)) {
879                nameOverrides.removeAt(i);
880            }
881        }
882    }
883
884    /**
885     * Calls the {@link SharedElementCallback#onSharedElementStart(List, List, List)} or
886     * {@link SharedElementCallback#onSharedElementEnd(List, List, List)} on the appropriate
887     * incoming or outgoing fragment.
888     *
889     * @param inFragment The incoming fragment
890     * @param outFragment The outgoing fragment
891     * @param isPop Is the incoming fragment part of a pop transaction?
892     * @param sharedElements The shared element Views
893     * @param isStart Call the start or end call on the SharedElementCallback
894     */
895    private static void callSharedElementStartEnd(Fragment inFragment, Fragment outFragment,
896            boolean isPop, ArrayMap<String, View> sharedElements, boolean isStart) {
897        SharedElementCallback sharedElementCallback = isPop
898                ? outFragment.getEnterTransitionCallback()
899                : inFragment.getEnterTransitionCallback();
900        if (sharedElementCallback != null) {
901            ArrayList<View> views = new ArrayList<>();
902            ArrayList<String> names = new ArrayList<>();
903            final int count = sharedElements == null ? 0 : sharedElements.size();
904            for (int i = 0; i < count; i++) {
905                names.add(sharedElements.keyAt(i));
906                views.add(sharedElements.valueAt(i));
907            }
908            if (isStart) {
909                sharedElementCallback.onSharedElementStart(names, views, null);
910            } else {
911                sharedElementCallback.onSharedElementEnd(names, views, null);
912            }
913        }
914    }
915
916    @RequiresApi(21)
917    private static ArrayList<View> configureEnteringExitingViews(Object transition,
918            Fragment fragment, ArrayList<View> sharedElements, View nonExistentView) {
919        ArrayList<View> viewList = null;
920        if (transition != null) {
921            viewList = new ArrayList<>();
922            View root = fragment.getView();
923            if (root != null) {
924                FragmentTransitionCompat21.captureTransitioningViews(viewList, root);
925            }
926            if (sharedElements != null) {
927                viewList.removeAll(sharedElements);
928            }
929            if (!viewList.isEmpty()) {
930                viewList.add(nonExistentView);
931                FragmentTransitionCompat21.addTargets(transition, viewList);
932            }
933        }
934        return viewList;
935    }
936
937    /**
938     * Sets the visibility of all Views in {@code views} to {@code visibility}.
939     */
940    private static void setViewVisibility(ArrayList<View> views, int visibility) {
941        if (views == null) {
942            return;
943        }
944        for (int i = views.size() - 1; i >= 0; i--) {
945            final View view = views.get(i);
946            view.setVisibility(visibility);
947        }
948    }
949
950    /**
951     * Merges exit, shared element, and enter transitions so that they act together or
952     * sequentially as defined in the fragments.
953     */
954    @RequiresApi(21)
955    private static Object mergeTransitions(Object enterTransition,
956            Object exitTransition, Object sharedElementTransition, Fragment inFragment,
957            boolean isPop) {
958        boolean overlap = true;
959        if (enterTransition != null && exitTransition != null && inFragment != null) {
960            overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() :
961                    inFragment.getAllowEnterTransitionOverlap();
962        }
963
964        // Wrap the transitions. Explicit targets like in enter and exit will cause the
965        // views to be targeted regardless of excluded views. If that happens, then the
966        // excluded fragments views (hidden fragments) will still be in the transition.
967
968        Object transition;
969        if (overlap) {
970            // Regular transition -- do it all together
971            transition = FragmentTransitionCompat21.mergeTransitionsTogether(exitTransition,
972                    enterTransition, sharedElementTransition);
973        } else {
974            // First do exit, then enter, but allow shared element transition to happen
975            // during both.
976            transition = FragmentTransitionCompat21.mergeTransitionsInSequence(exitTransition,
977                    enterTransition, sharedElementTransition);
978        }
979        return transition;
980    }
981
982    /**
983     * Finds the first removed fragment and last added fragments when going forward.
984     * If none of the fragments have transitions, then both lists will be empty.
985     *
986     * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
987     *                               and last fragments to be added. This will be modified by
988     *                               this method.
989     */
990    public static void calculateFragments(BackStackRecord transaction,
991            SparseArray<FragmentContainerTransition> transitioningFragments,
992            boolean isReordered) {
993        final int numOps = transaction.mOps.size();
994        for (int opNum = 0; opNum < numOps; opNum++) {
995            final BackStackRecord.Op op = transaction.mOps.get(opNum);
996            addToFirstInLastOut(transaction, op, transitioningFragments, false, isReordered);
997        }
998    }
999
1000    /**
1001     * Finds the first removed fragment and last added fragments when popping the back stack.
1002     * If none of the fragments have transitions, then both lists will be empty.
1003     *
1004     * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
1005     *                               and last fragments to be added. This will be modified by
1006     *                               this method.
1007     */
1008    public static void calculatePopFragments(BackStackRecord transaction,
1009            SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered) {
1010        if (!transaction.mManager.mContainer.onHasView()) {
1011            return; // nothing to see, so no transitions
1012        }
1013        final int numOps = transaction.mOps.size();
1014        for (int opNum = numOps - 1; opNum >= 0; opNum--) {
1015            final BackStackRecord.Op op = transaction.mOps.get(opNum);
1016            addToFirstInLastOut(transaction, op, transitioningFragments, true, isReordered);
1017        }
1018    }
1019
1020    /**
1021     * Examines the {@code command} and may set the first out or last in fragment for the fragment's
1022     * container.
1023     *
1024     * @param transaction The executing transaction
1025     * @param op The operation being run.
1026     * @param transitioningFragments A structure holding the first in and last out fragments
1027     *                               for each fragment container.
1028     * @param isPop Is the operation a pop?
1029     * @param isReorderedTransaction True if the operations have been partially executed and the
1030     *                               added fragments have Views in the hierarchy or false if the
1031     *                               operations haven't been executed yet.
1032     */
1033    @SuppressWarnings("ReferenceEquality")
1034    private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op,
1035            SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop,
1036            boolean isReorderedTransaction) {
1037        final Fragment fragment = op.fragment;
1038        if (fragment == null) {
1039            return; // no fragment, no transition
1040        }
1041        final int containerId = fragment.mContainerId;
1042        if (containerId == 0) {
1043            return; // no container, no transition
1044        }
1045        final int command = isPop ? INVERSE_OPS[op.cmd] : op.cmd;
1046        boolean setLastIn = false;
1047        boolean wasRemoved = false;
1048        boolean setFirstOut = false;
1049        boolean wasAdded = false;
1050        switch (command) {
1051            case BackStackRecord.OP_SHOW:
1052                if (isReorderedTransaction) {
1053                    setLastIn = fragment.mHiddenChanged && !fragment.mHidden && fragment.mAdded;
1054                } else {
1055                    setLastIn = fragment.mHidden;
1056                }
1057                wasAdded = true;
1058                break;
1059            case BackStackRecord.OP_ADD:
1060            case BackStackRecord.OP_ATTACH:
1061                if (isReorderedTransaction) {
1062                    setLastIn = fragment.mIsNewlyAdded;
1063                } else {
1064                    setLastIn = !fragment.mAdded && !fragment.mHidden;
1065                }
1066                wasAdded = true;
1067                break;
1068            case BackStackRecord.OP_HIDE:
1069                if (isReorderedTransaction) {
1070                    setFirstOut = fragment.mHiddenChanged && fragment.mAdded && fragment.mHidden;
1071                } else {
1072                    setFirstOut = fragment.mAdded && !fragment.mHidden;
1073                }
1074                wasRemoved = true;
1075                break;
1076            case BackStackRecord.OP_REMOVE:
1077            case BackStackRecord.OP_DETACH:
1078                if (isReorderedTransaction) {
1079                    setFirstOut = !fragment.mAdded && fragment.mView != null
1080                            && fragment.mView.getVisibility() == View.VISIBLE
1081                            && fragment.mPostponedAlpha >= 0;
1082                } else {
1083                    setFirstOut = fragment.mAdded && !fragment.mHidden;
1084                }
1085                wasRemoved = true;
1086                break;
1087        }
1088        FragmentContainerTransition containerTransition = transitioningFragments.get(containerId);
1089        if (setLastIn) {
1090            containerTransition =
1091                    ensureContainer(containerTransition, transitioningFragments, containerId);
1092            containerTransition.lastIn = fragment;
1093            containerTransition.lastInIsPop = isPop;
1094            containerTransition.lastInTransaction = transaction;
1095        }
1096        if (!isReorderedTransaction && wasAdded) {
1097            if (containerTransition != null && containerTransition.firstOut == fragment) {
1098                containerTransition.firstOut = null;
1099            }
1100
1101            /*
1102             * Ensure that fragments that are entering are at least at the CREATED state
1103             * so that they may load Transitions using TransitionInflater.
1104             */
1105            FragmentManagerImpl manager = transaction.mManager;
1106            if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED
1107                    && !transaction.mReorderingAllowed) {
1108                manager.makeActive(fragment);
1109                manager.moveToState(fragment, Fragment.CREATED, 0, 0, false);
1110            }
1111        }
1112        if (setFirstOut && (containerTransition == null || containerTransition.firstOut == null)) {
1113            containerTransition =
1114                    ensureContainer(containerTransition, transitioningFragments, containerId);
1115            containerTransition.firstOut = fragment;
1116            containerTransition.firstOutIsPop = isPop;
1117            containerTransition.firstOutTransaction = transaction;
1118        }
1119
1120        if (!isReorderedTransaction && wasRemoved
1121                && (containerTransition != null && containerTransition.lastIn == fragment)) {
1122            containerTransition.lastIn = null;
1123        }
1124    }
1125
1126    /**
1127     * Ensures that a FragmentContainerTransition has been added to the SparseArray. If so,
1128     * it returns the existing one. If not, one is created and added to the SparseArray and
1129     * returned.
1130     */
1131    private static FragmentContainerTransition ensureContainer(
1132            FragmentContainerTransition containerTransition,
1133            SparseArray<FragmentContainerTransition> transitioningFragments, int containerId) {
1134        if (containerTransition == null) {
1135            containerTransition = new FragmentContainerTransition();
1136            transitioningFragments.put(containerId, containerTransition);
1137        }
1138        return containerTransition;
1139    }
1140
1141    /**
1142     * Tracks the last fragment added and first fragment removed for fragment transitions.
1143     * This also tracks which fragments are changed by push or pop transactions.
1144     */
1145    static class FragmentContainerTransition {
1146        /**
1147         * The last fragment added/attached/shown in its container
1148         */
1149        public Fragment lastIn;
1150
1151        /**
1152         * true when lastIn was added during a pop transaction or false if added with a push
1153         */
1154        public boolean lastInIsPop;
1155
1156        /**
1157         * The transaction that included the last in fragment
1158         */
1159        public BackStackRecord lastInTransaction;
1160
1161        /**
1162         * The first fragment with a View that was removed/detached/hidden in its container.
1163         */
1164        public Fragment firstOut;
1165
1166        /**
1167         * true when firstOut was removed during a pop transaction or false otherwise
1168         */
1169        public boolean firstOutIsPop;
1170
1171        /**
1172         * The transaction that included the first out fragment
1173         */
1174        public BackStackRecord firstOutTransaction;
1175    }
1176}
1177