1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.app;
17 
18 import android.graphics.Rect;
19 import android.os.Build;
20 import android.transition.Transition;
21 import android.transition.TransitionListenerAdapter;
22 import android.transition.TransitionManager;
23 import android.transition.TransitionSet;
24 import android.util.ArrayMap;
25 import android.util.SparseArray;
26 import android.view.View;
27 import android.view.ViewGroup;
28 
29 import com.android.internal.view.OneShotPreDrawListener;
30 
31 import java.util.ArrayList;
32 import java.util.Collection;
33 import java.util.List;
34 import 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  */
42 class 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      */
startTransitions(FragmentManagerImpl fragmentManager, ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, int startIndex, int endIndex, boolean isReordered)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      */
calculateNameOverrides(int containerId, ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, int startIndex, int endIndex)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      */
configureTransitionsReordered(FragmentManagerImpl fragmentManager, int containerId, FragmentContainerTransition fragments, View nonExistentView, ArrayMap<String, String> nameOverrides)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      */
configureTransitionsOrdered(FragmentManagerImpl fragmentManager, int containerId, FragmentContainerTransition fragments, View nonExistentView, ArrayMap<String, String> nameOverrides)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      */
replaceHide(Transition exitTransition, Fragment exitingFragment, final ArrayList<View> exitingViews)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      */
scheduleTargetChange(final ViewGroup sceneRoot, final Fragment inFragment, final View nonExistentView, final ArrayList<View> sharedElementsIn, final Transition enterTransition, final ArrayList<View> enteringViews, final Transition exitTransition, final ArrayList<View> exitingViews)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      */
getSharedElementTransition(Fragment inFragment, Fragment outFragment, boolean isPop)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      */
getEnterTransition(Fragment inFragment, boolean isPop)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      */
getExitTransition(Fragment outFragment, boolean isPop)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      */
cloneTransition(Transition transition)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      */
configureSharedElementsReordered(final ViewGroup sceneRoot, final View nonExistentView, ArrayMap<String, String> nameOverrides, final FragmentContainerTransition fragments, final ArrayList<View> sharedElementsOut, final ArrayList<View> sharedElementsIn, final Transition enterTransition, final Transition exitTransition)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      */
addSharedElementsWithMatchingNames(ArrayList<View> views, ArrayMap<String, View> sharedElements, Collection<String> nameOverridesSet)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      */
configureSharedElementsOrdered(final ViewGroup sceneRoot, final View nonExistentView, ArrayMap<String, String> nameOverrides, final FragmentContainerTransition fragments, final ArrayList<View> sharedElementsOut, final ArrayList<View> sharedElementsIn, final Transition enterTransition, final Transition exitTransition)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      */
captureOutSharedElements( ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition, FragmentContainerTransition fragments)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      */
captureInSharedElements( ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition, FragmentContainerTransition fragments)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      */
findKeyForValue(ArrayMap<String, String> map, String value)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      */
getInEpicenterView(ArrayMap<String, View> inSharedElements, FragmentContainerTransition fragments, Transition enterTransition, boolean inIsPop)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      */
setOutEpicenter(TransitionSet sharedElementTransition, Transition exitTransition, ArrayMap<String, View> outSharedElements, boolean outIsPop, BackStackRecord outTransaction)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      */
setEpicenter(Transition transition, View view)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      */
retainValues(ArrayMap<String, String> nameOverrides, ArrayMap<String, View> namedViews)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      */
callSharedElementStartEnd(Fragment inFragment, Fragment outFragment, boolean isPop, ArrayMap<String, View> sharedElements, boolean isStart)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      */
setSharedElementTargets(TransitionSet transition, View nonExistentView, ArrayList<View> sharedViews)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      */
bfsAddViewChildren(final List<View> views, final View startView)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      */
containedBeforeIndex(final List<View> views, final View view, final int maxIndex)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      */
scheduleRemoveTargets(final Transition overalTransition, final Transition enterTransition, final ArrayList<View> enteringViews, final Transition exitTransition, final ArrayList<View> exitingViews, final TransitionSet sharedElementTransition, final ArrayList<View> sharedElementsIn)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             @Override
1018             public void onTransitionEnd(Transition transition) {
1019                 transition.removeListener(this);
1020             }
1021         });
1022     }
1023 
1024     /**
1025      * This method removes the views from transitions that target ONLY those views and
1026      * replaces them with the new targets list.
1027      * The views list should match those added in addTargets and should contain
1028      * one view that is not in the view hierarchy (state.nonExistentView).
1029      */
replaceTargets(Transition transition, ArrayList<View> oldTargets, ArrayList<View> newTargets)1030     public static void replaceTargets(Transition transition, ArrayList<View> oldTargets,
1031             ArrayList<View> newTargets) {
1032         if (transition instanceof TransitionSet) {
1033             TransitionSet set = (TransitionSet) transition;
1034             int numTransitions = set.getTransitionCount();
1035             for (int i = 0; i < numTransitions; i++) {
1036                 Transition child = set.getTransitionAt(i);
1037                 replaceTargets(child, oldTargets, newTargets);
1038             }
1039         } else if (!hasSimpleTarget(transition)) {
1040             List<View> targets = transition.getTargets();
1041             if (targets != null && targets.size() == oldTargets.size() &&
1042                     targets.containsAll(oldTargets)) {
1043                 // We have an exact match. We must have added these earlier in addTargets
1044                 final int targetCount = newTargets == null ? 0 : newTargets.size();
1045                 for (int i = 0; i < targetCount; i++) {
1046                     transition.addTarget(newTargets.get(i));
1047                 }
1048                 for (int i = oldTargets.size() - 1; i >= 0; i--) {
1049                     transition.removeTarget(oldTargets.get(i));
1050                 }
1051             }
1052         }
1053     }
1054 
1055     /**
1056      * This method adds views as targets to the transition, but only if the transition
1057      * doesn't already have a target. It is best for views to contain one View object
1058      * that does not exist in the view hierarchy (state.nonExistentView) so that
1059      * when they are removed later, a list match will suffice to remove the targets.
1060      * Otherwise, if you happened to have targeted the exact views for the transition,
1061      * the replaceTargets call will remove them unexpectedly.
1062      */
addTargets(Transition transition, ArrayList<View> views)1063     public static void addTargets(Transition transition, ArrayList<View> views) {
1064         if (transition == null) {
1065             return;
1066         }
1067         if (transition instanceof TransitionSet) {
1068             TransitionSet set = (TransitionSet) transition;
1069             int numTransitions = set.getTransitionCount();
1070             for (int i = 0; i < numTransitions; i++) {
1071                 Transition child = set.getTransitionAt(i);
1072                 addTargets(child, views);
1073             }
1074         } else if (!hasSimpleTarget(transition)) {
1075             List<View> targets = transition.getTargets();
1076             if (isNullOrEmpty(targets)) {
1077                 // We can just add the target views
1078                 int numViews = views.size();
1079                 for (int i = 0; i < numViews; i++) {
1080                     transition.addTarget(views.get(i));
1081                 }
1082             }
1083         }
1084     }
1085 
1086     /**
1087      * Returns true if there are any targets based on ID, transition or type.
1088      */
hasSimpleTarget(Transition transition)1089     private static boolean hasSimpleTarget(Transition transition) {
1090         return !isNullOrEmpty(transition.getTargetIds()) ||
1091                 !isNullOrEmpty(transition.getTargetNames()) ||
1092                 !isNullOrEmpty(transition.getTargetTypes());
1093     }
1094 
1095     /**
1096      * Simple utility to detect if a list is null or has no elements.
1097      */
isNullOrEmpty(List list)1098     private static boolean isNullOrEmpty(List list) {
1099         return list == null || list.isEmpty();
1100     }
1101 
configureEnteringExitingViews(Transition transition, Fragment fragment, ArrayList<View> sharedElements, View nonExistentView)1102     private static ArrayList<View> configureEnteringExitingViews(Transition transition,
1103             Fragment fragment, ArrayList<View> sharedElements, View nonExistentView) {
1104         ArrayList<View> viewList = null;
1105         if (transition != null) {
1106             viewList = new ArrayList<>();
1107             View root = fragment.getView();
1108             if (root != null) {
1109                 root.captureTransitioningViews(viewList);
1110             }
1111             if (sharedElements != null) {
1112                 viewList.removeAll(sharedElements);
1113             }
1114             if (!viewList.isEmpty()) {
1115                 viewList.add(nonExistentView);
1116                 addTargets(transition, viewList);
1117             }
1118         }
1119         return viewList;
1120     }
1121 
1122     /**
1123      * Sets the visibility of all Views in {@code views} to {@code visibility}.
1124      */
setViewVisibility(ArrayList<View> views, @View.Visibility int visibility)1125     private static void setViewVisibility(ArrayList<View> views, @View.Visibility int visibility) {
1126         if (views == null) {
1127             return;
1128         }
1129         for (int i = views.size() - 1; i >= 0; i--) {
1130             final View view = views.get(i);
1131             view.setVisibility(visibility);
1132         }
1133     }
1134 
1135     /**
1136      * Merges exit, shared element, and enter transitions so that they act together or
1137      * sequentially as defined in the fragments.
1138      */
mergeTransitions(Transition enterTransition, Transition exitTransition, Transition sharedElementTransition, Fragment inFragment, boolean isPop)1139     private static Transition mergeTransitions(Transition enterTransition,
1140             Transition exitTransition, Transition sharedElementTransition, Fragment inFragment,
1141             boolean isPop) {
1142         boolean overlap = true;
1143         if (enterTransition != null && exitTransition != null && inFragment != null) {
1144             overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() :
1145                     inFragment.getAllowEnterTransitionOverlap();
1146         }
1147 
1148         // Wrap the transitions. Explicit targets like in enter and exit will cause the
1149         // views to be targeted regardless of excluded views. If that happens, then the
1150         // excluded fragments views (hidden fragments) will still be in the transition.
1151 
1152         Transition transition;
1153         if (overlap) {
1154             // Regular transition -- do it all together
1155             TransitionSet transitionSet = new TransitionSet();
1156             if (enterTransition != null) {
1157                 transitionSet.addTransition(enterTransition);
1158             }
1159             if (exitTransition != null) {
1160                 transitionSet.addTransition(exitTransition);
1161             }
1162             if (sharedElementTransition != null) {
1163                 transitionSet.addTransition(sharedElementTransition);
1164             }
1165             transition = transitionSet;
1166         } else {
1167             // First do exit, then enter, but allow shared element transition to happen
1168             // during both.
1169             Transition staggered = null;
1170             if (exitTransition != null && enterTransition != null) {
1171                 staggered = new TransitionSet()
1172                         .addTransition(exitTransition)
1173                         .addTransition(enterTransition)
1174                         .setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
1175             } else if (exitTransition != null) {
1176                 staggered = exitTransition;
1177             } else if (enterTransition != null) {
1178                 staggered = enterTransition;
1179             }
1180             if (sharedElementTransition != null) {
1181                 TransitionSet together = new TransitionSet();
1182                 if (staggered != null) {
1183                     together.addTransition(staggered);
1184                 }
1185                 together.addTransition(sharedElementTransition);
1186                 transition = together;
1187             } else {
1188                 transition = staggered;
1189             }
1190         }
1191         return transition;
1192     }
1193 
1194     /**
1195      * Finds the first removed fragment and last added fragments when going forward.
1196      * If none of the fragments have transitions, then both lists will be empty.
1197      *
1198      * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
1199      *                               and last fragments to be added. This will be modified by
1200      *                               this method.
1201      */
calculateFragments(BackStackRecord transaction, SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered)1202     public static void calculateFragments(BackStackRecord transaction,
1203             SparseArray<FragmentContainerTransition> transitioningFragments,
1204             boolean isReordered) {
1205         final int numOps = transaction.mOps.size();
1206         for (int opNum = 0; opNum < numOps; opNum++) {
1207             final BackStackRecord.Op op = transaction.mOps.get(opNum);
1208             addToFirstInLastOut(transaction, op, transitioningFragments, false, isReordered);
1209         }
1210     }
1211 
1212     /**
1213      * Finds the first removed fragment and last added fragments when popping the back stack.
1214      * If none of the fragments have transitions, then both lists will be empty.
1215      *
1216      * @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
1217      *                               and last fragments to be added. This will be modified by
1218      *                               this method.
1219      */
calculatePopFragments(BackStackRecord transaction, SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered)1220     public static void calculatePopFragments(BackStackRecord transaction,
1221             SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered) {
1222         if (!transaction.mManager.mContainer.onHasView()) {
1223             return; // nothing to see, so no transitions
1224         }
1225         final int numOps = transaction.mOps.size();
1226         for (int opNum = numOps - 1; opNum >= 0; opNum--) {
1227             final BackStackRecord.Op op = transaction.mOps.get(opNum);
1228             addToFirstInLastOut(transaction, op, transitioningFragments, true, isReordered);
1229         }
1230     }
1231 
1232     /**
1233      * Examines the {@code command} and may set the first out or last in fragment for the fragment's
1234      * container.
1235      *
1236      * @param transaction The executing transaction
1237      * @param op The operation being run.
1238      * @param transitioningFragments A structure holding the first in and last out fragments
1239      *                               for each fragment container.
1240      * @param isPop Is the operation a pop?
1241      * @param isReorderedTransaction True if the operations have been partially executed and the
1242      *                               added fragments have Views in the hierarchy or false if the
1243      *                               operations haven't been executed yet.
1244      */
1245     @SuppressWarnings("ReferenceEquality")
addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op, SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop, boolean isReorderedTransaction)1246     private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op,
1247             SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop,
1248             boolean isReorderedTransaction) {
1249         final Fragment fragment = op.fragment;
1250         if (fragment == null) {
1251             return; // no fragment, no transition
1252         }
1253         final int containerId = fragment.mContainerId;
1254         if (containerId == 0) {
1255             return; // no container, no transition
1256         }
1257         final int command = isPop ? INVERSE_OPS[op.cmd] : op.cmd;
1258         boolean setLastIn = false;
1259         boolean wasRemoved = false;
1260         boolean setFirstOut = false;
1261         boolean wasAdded = false;
1262         switch (command) {
1263             case BackStackRecord.OP_SHOW:
1264                 if (isReorderedTransaction) {
1265                     setLastIn = fragment.mHiddenChanged && !fragment.mHidden &&
1266                             fragment.mAdded;
1267                 } else {
1268                     setLastIn = fragment.mHidden;
1269                 }
1270                 wasAdded = true;
1271                 break;
1272             case BackStackRecord.OP_ADD:
1273             case BackStackRecord.OP_ATTACH:
1274                 if (isReorderedTransaction) {
1275                     setLastIn = fragment.mIsNewlyAdded;
1276                 } else {
1277                     setLastIn = !fragment.mAdded && !fragment.mHidden;
1278                 }
1279                 wasAdded = true;
1280                 break;
1281             case BackStackRecord.OP_HIDE:
1282                 if (isReorderedTransaction) {
1283                     setFirstOut = fragment.mHiddenChanged && fragment.mAdded &&
1284                             fragment.mHidden;
1285                 } else {
1286                     setFirstOut = fragment.mAdded && !fragment.mHidden;
1287                 }
1288                 wasRemoved = true;
1289                 break;
1290             case BackStackRecord.OP_REMOVE:
1291             case BackStackRecord.OP_DETACH:
1292                 if (isReorderedTransaction) {
1293                     setFirstOut = !fragment.mAdded && fragment.mView != null
1294                             && fragment.mView.getVisibility() == View.VISIBLE
1295                             && fragment.mView.getTransitionAlpha() > 0;
1296                 } else {
1297                     setFirstOut = fragment.mAdded && !fragment.mHidden;
1298                 }
1299                 wasRemoved = true;
1300                 break;
1301         }
1302         FragmentContainerTransition containerTransition = transitioningFragments.get(containerId);
1303         if (setLastIn) {
1304             containerTransition =
1305                     ensureContainer(containerTransition, transitioningFragments, containerId);
1306             containerTransition.lastIn = fragment;
1307             containerTransition.lastInIsPop = isPop;
1308             containerTransition.lastInTransaction = transaction;
1309         }
1310         if (!isReorderedTransaction && wasAdded) {
1311             if (containerTransition != null && containerTransition.firstOut == fragment) {
1312                 containerTransition.firstOut = null;
1313             }
1314 
1315             /*
1316              * Ensure that fragments that are entering are at least at the CREATED state
1317              * so that they may load Transitions using TransitionInflater.
1318              */
1319             FragmentManagerImpl manager = transaction.mManager;
1320             if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED &&
1321                     manager.mHost.getContext().getApplicationInfo().targetSdkVersion >=
1322                             Build.VERSION_CODES.N && !transaction.mReorderingAllowed) {
1323                 manager.makeActive(fragment);
1324                 manager.moveToState(fragment, Fragment.CREATED, 0, 0, false);
1325             }
1326         }
1327         if (setFirstOut && (containerTransition == null || containerTransition.firstOut == null)) {
1328             containerTransition =
1329                     ensureContainer(containerTransition, transitioningFragments, containerId);
1330             containerTransition.firstOut = fragment;
1331             containerTransition.firstOutIsPop = isPop;
1332             containerTransition.firstOutTransaction = transaction;
1333         }
1334 
1335         if (!isReorderedTransaction && wasRemoved &&
1336                 (containerTransition != null && containerTransition.lastIn == fragment)) {
1337             containerTransition.lastIn = null;
1338         }
1339     }
1340 
1341     /**
1342      * Ensures that a FragmentContainerTransition has been added to the SparseArray. If so,
1343      * it returns the existing one. If not, one is created and added to the SparseArray and
1344      * returned.
1345      */
ensureContainer( FragmentContainerTransition containerTransition, SparseArray<FragmentContainerTransition> transitioningFragments, int containerId)1346     private static FragmentContainerTransition ensureContainer(
1347             FragmentContainerTransition containerTransition,
1348             SparseArray<FragmentContainerTransition> transitioningFragments, int containerId) {
1349         if (containerTransition == null) {
1350             containerTransition = new FragmentContainerTransition();
1351             transitioningFragments.put(containerId, containerTransition);
1352         }
1353         return containerTransition;
1354     }
1355 
1356     /**
1357      * Tracks the last fragment added and first fragment removed for fragment transitions.
1358      * This also tracks which fragments are changed by push or pop transactions.
1359      */
1360     public static class FragmentContainerTransition {
1361         /**
1362          * The last fragment added/attached/shown in its container
1363          */
1364         public Fragment lastIn;
1365 
1366         /**
1367          * true when lastIn was added during a pop transaction or false if added with a push
1368          */
1369         public boolean lastInIsPop;
1370 
1371         /**
1372          * The transaction that included the last in fragment
1373          */
1374         public BackStackRecord lastInTransaction;
1375 
1376         /**
1377          * The first fragment with a View that was removed/detached/hidden in its container.
1378          */
1379         public Fragment firstOut;
1380 
1381         /**
1382          * true when firstOut was removed during a pop transaction or false otherwise
1383          */
1384         public boolean firstOutIsPop;
1385 
1386         /**
1387          * The transaction that included the first out fragment
1388          */
1389         public BackStackRecord firstOutTransaction;
1390     }
1391 }
1392