1392 lines
63 KiB
Java
1392 lines
63 KiB
Java
/*
|
|
* Copyright (C) 2016 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package android.app;
|
|
|
|
import android.graphics.Rect;
|
|
import android.os.Build;
|
|
import android.transition.Transition;
|
|
import android.transition.TransitionListenerAdapter;
|
|
import android.transition.TransitionManager;
|
|
import android.transition.TransitionSet;
|
|
import android.util.ArrayMap;
|
|
import android.util.SparseArray;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
|
|
import com.android.internal.view.OneShotPreDrawListener;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* Contains the Fragment Transition functionality for both ordered and reordered
|
|
* Fragment Transactions. With reordered fragment transactions, all Views have been
|
|
* added to the View hierarchy prior to calling startTransitions. With ordered
|
|
* fragment transactions, Views will be removed and added after calling startTransitions.
|
|
*/
|
|
class FragmentTransition {
|
|
/**
|
|
* The inverse of all BackStackRecord operation commands. This assumes that
|
|
* REPLACE operations have already been replaced by add/remove operations.
|
|
*/
|
|
private static final int[] INVERSE_OPS = {
|
|
BackStackRecord.OP_NULL, // inverse of OP_NULL (error)
|
|
BackStackRecord.OP_REMOVE, // inverse of OP_ADD
|
|
BackStackRecord.OP_NULL, // inverse of OP_REPLACE (error)
|
|
BackStackRecord.OP_ADD, // inverse of OP_REMOVE
|
|
BackStackRecord.OP_SHOW, // inverse of OP_HIDE
|
|
BackStackRecord.OP_HIDE, // inverse of OP_SHOW
|
|
BackStackRecord.OP_ATTACH, // inverse of OP_DETACH
|
|
BackStackRecord.OP_DETACH, // inverse of OP_ATTACH
|
|
BackStackRecord.OP_UNSET_PRIMARY_NAV, // inverse of OP_SET_PRIMARY_NAV
|
|
BackStackRecord.OP_SET_PRIMARY_NAV, // inverse of OP_UNSET_PRIMARY_NAV
|
|
};
|
|
|
|
/**
|
|
* The main entry point for Fragment Transitions, this starts the transitions
|
|
* set on the leaving Fragment's {@link Fragment#getExitTransition()}, the
|
|
* entering Fragment's {@link Fragment#getEnterTransition()} and
|
|
* {@link Fragment#getSharedElementEnterTransition()}. When popping,
|
|
* the leaving Fragment's {@link Fragment#getReturnTransition()} and
|
|
* {@link Fragment#getSharedElementReturnTransition()} and the entering
|
|
* {@link Fragment#getReenterTransition()} will be run.
|
|
* <p>
|
|
* With reordered Fragment Transitions, all Views have been added to the
|
|
* View hierarchy prior to calling this method. The incoming Fragment's Views
|
|
* will be INVISIBLE. With ordered Fragment Transitions, this method
|
|
* is called before any change has been made to the hierarchy. That means
|
|
* that the added Fragments have not created their Views yet and the hierarchy
|
|
* is unknown.
|
|
*
|
|
* @param fragmentManager The executing FragmentManagerImpl
|
|
* @param records The list of transactions being executed.
|
|
* @param isRecordPop For each transaction, whether it is a pop transaction or not.
|
|
* @param startIndex The first index into records and isRecordPop to execute as
|
|
* part of this transition.
|
|
* @param endIndex One past the last index into records and isRecordPop to execute
|
|
* as part of this transition.
|
|
* @param isReordered true if this is a reordered transaction, meaning that the
|
|
* Views of incoming fragments have been added. false if the
|
|
* transaction has yet to be run and Views haven't been created.
|
|
*/
|
|
static void startTransitions(FragmentManagerImpl fragmentManager,
|
|
ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
|
|
int startIndex, int endIndex, boolean isReordered) {
|
|
if (fragmentManager.mCurState < Fragment.CREATED) {
|
|
return;
|
|
}
|
|
SparseArray<FragmentContainerTransition> transitioningFragments =
|
|
new SparseArray<>();
|
|
for (int i = startIndex; i < endIndex; i++) {
|
|
final BackStackRecord record = records.get(i);
|
|
final boolean isPop = isRecordPop.get(i);
|
|
if (isPop) {
|
|
calculatePopFragments(record, transitioningFragments, isReordered);
|
|
} else {
|
|
calculateFragments(record, transitioningFragments, isReordered);
|
|
}
|
|
}
|
|
|
|
if (transitioningFragments.size() != 0) {
|
|
final View nonExistentView = new View(fragmentManager.mHost.getContext());
|
|
final int numContainers = transitioningFragments.size();
|
|
for (int i = 0; i < numContainers; i++) {
|
|
int containerId = transitioningFragments.keyAt(i);
|
|
ArrayMap<String, String> nameOverrides = calculateNameOverrides(containerId,
|
|
records, isRecordPop, startIndex, endIndex);
|
|
|
|
FragmentContainerTransition containerTransition = transitioningFragments.valueAt(i);
|
|
|
|
if (isReordered) {
|
|
configureTransitionsReordered(fragmentManager, containerId,
|
|
containerTransition, nonExistentView, nameOverrides);
|
|
} else {
|
|
configureTransitionsOrdered(fragmentManager, containerId,
|
|
containerTransition, nonExistentView, nameOverrides);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Iterates through the transactions that affect a given fragment container
|
|
* and tracks the shared element names across transactions. This is most useful
|
|
* in pop transactions where the names of shared elements are known.
|
|
*
|
|
* @param containerId The container ID that is executing the transition.
|
|
* @param records The list of transactions being executed.
|
|
* @param isRecordPop For each transaction, whether it is a pop transaction or not.
|
|
* @param startIndex The first index into records and isRecordPop to execute as
|
|
* part of this transition.
|
|
* @param endIndex One past the last index into records and isRecordPop to execute
|
|
* as part of this transition.
|
|
* @return A map from the initial shared element name to the final shared element name
|
|
* before any onMapSharedElements is run.
|
|
*/
|
|
private static ArrayMap<String, String> calculateNameOverrides(int containerId,
|
|
ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
|
|
int startIndex, int endIndex) {
|
|
ArrayMap<String, String> nameOverrides = new ArrayMap<>();
|
|
for (int recordNum = endIndex - 1; recordNum >= startIndex; recordNum--) {
|
|
final BackStackRecord record = records.get(recordNum);
|
|
if (!record.interactsWith(containerId)) {
|
|
continue;
|
|
}
|
|
final boolean isPop = isRecordPop.get(recordNum);
|
|
if (record.mSharedElementSourceNames != null) {
|
|
final int numSharedElements = record.mSharedElementSourceNames.size();
|
|
final ArrayList<String> sources;
|
|
final ArrayList<String> targets;
|
|
if (isPop) {
|
|
targets = record.mSharedElementSourceNames;
|
|
sources = record.mSharedElementTargetNames;
|
|
} else {
|
|
sources = record.mSharedElementSourceNames;
|
|
targets = record.mSharedElementTargetNames;
|
|
}
|
|
for (int i = 0; i < numSharedElements; i++) {
|
|
String sourceName = sources.get(i);
|
|
String targetName = targets.get(i);
|
|
String previousTarget = nameOverrides.remove(targetName);
|
|
if (previousTarget != null) {
|
|
nameOverrides.put(sourceName, previousTarget);
|
|
} else {
|
|
nameOverrides.put(sourceName, targetName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nameOverrides;
|
|
}
|
|
|
|
/**
|
|
* Configures a transition for a single fragment container for which the transaction was
|
|
* reordered. That means that all Fragment Views have been added and incoming fragment
|
|
* Views are marked invisible.
|
|
*
|
|
* @param fragmentManager The executing FragmentManagerImpl
|
|
* @param containerId The container ID that is executing the transition.
|
|
* @param fragments A structure holding the transitioning fragments in this container.
|
|
* @param nonExistentView A View that does not exist in the hierarchy. This is used to
|
|
* prevent transitions from acting on other Views when there is no
|
|
* other target.
|
|
* @param nameOverrides A map of the shared element names from the starting fragment to
|
|
* the final fragment's Views as given in
|
|
* {@link FragmentTransaction#addSharedElement(View, String)}.
|
|
*/
|
|
private static void configureTransitionsReordered(FragmentManagerImpl fragmentManager,
|
|
int containerId, FragmentContainerTransition fragments,
|
|
View nonExistentView, ArrayMap<String, String> nameOverrides) {
|
|
ViewGroup sceneRoot = null;
|
|
if (fragmentManager.mContainer.onHasView()) {
|
|
sceneRoot = fragmentManager.mContainer.onFindViewById(containerId);
|
|
}
|
|
if (sceneRoot == null) {
|
|
return;
|
|
}
|
|
final Fragment inFragment = fragments.lastIn;
|
|
final Fragment outFragment = fragments.firstOut;
|
|
final boolean inIsPop = fragments.lastInIsPop;
|
|
final boolean outIsPop = fragments.firstOutIsPop;
|
|
|
|
ArrayList<View> sharedElementsIn = new ArrayList<>();
|
|
ArrayList<View> sharedElementsOut = new ArrayList<>();
|
|
Transition enterTransition = getEnterTransition(inFragment, inIsPop);
|
|
Transition exitTransition = getExitTransition(outFragment, outIsPop);
|
|
|
|
TransitionSet sharedElementTransition = configureSharedElementsReordered(sceneRoot,
|
|
nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,
|
|
enterTransition, exitTransition);
|
|
|
|
if (enterTransition == null && sharedElementTransition == null &&
|
|
exitTransition == null) {
|
|
return; // no transitions!
|
|
}
|
|
|
|
ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition,
|
|
outFragment, sharedElementsOut, nonExistentView);
|
|
|
|
ArrayList<View> enteringViews = configureEnteringExitingViews(enterTransition,
|
|
inFragment, sharedElementsIn, nonExistentView);
|
|
|
|
setViewVisibility(enteringViews, View.INVISIBLE);
|
|
|
|
Transition transition = mergeTransitions(enterTransition, exitTransition,
|
|
sharedElementTransition, inFragment, inIsPop);
|
|
|
|
if (transition != null) {
|
|
replaceHide(exitTransition, outFragment, exitingViews);
|
|
transition.setNameOverrides(nameOverrides);
|
|
scheduleRemoveTargets(transition,
|
|
enterTransition, enteringViews, exitTransition, exitingViews,
|
|
sharedElementTransition, sharedElementsIn);
|
|
TransitionManager.beginDelayedTransition(sceneRoot, transition);
|
|
setViewVisibility(enteringViews, View.VISIBLE);
|
|
// Swap the shared element targets
|
|
if (sharedElementTransition != null) {
|
|
sharedElementTransition.getTargets().clear();
|
|
sharedElementTransition.getTargets().addAll(sharedElementsIn);
|
|
replaceTargets(sharedElementTransition, sharedElementsOut, sharedElementsIn);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configures a transition for a single fragment container for which the transaction was
|
|
* ordered. That means that the transaction has not been executed yet, so incoming
|
|
* Views are not yet known.
|
|
*
|
|
* @param fragmentManager The executing FragmentManagerImpl
|
|
* @param containerId The container ID that is executing the transition.
|
|
* @param fragments A structure holding the transitioning fragments in this container.
|
|
* @param nonExistentView A View that does not exist in the hierarchy. This is used to
|
|
* prevent transitions from acting on other Views when there is no
|
|
* other target.
|
|
* @param nameOverrides A map of the shared element names from the starting fragment to
|
|
* the final fragment's Views as given in
|
|
* {@link FragmentTransaction#addSharedElement(View, String)}.
|
|
*/
|
|
private static void configureTransitionsOrdered(FragmentManagerImpl fragmentManager,
|
|
int containerId, FragmentContainerTransition fragments,
|
|
View nonExistentView, ArrayMap<String, String> nameOverrides) {
|
|
ViewGroup sceneRoot = null;
|
|
if (fragmentManager.mContainer.onHasView()) {
|
|
sceneRoot = fragmentManager.mContainer.onFindViewById(containerId);
|
|
}
|
|
if (sceneRoot == null) {
|
|
return;
|
|
}
|
|
final Fragment inFragment = fragments.lastIn;
|
|
final Fragment outFragment = fragments.firstOut;
|
|
final boolean inIsPop = fragments.lastInIsPop;
|
|
final boolean outIsPop = fragments.firstOutIsPop;
|
|
|
|
Transition enterTransition = getEnterTransition(inFragment, inIsPop);
|
|
Transition exitTransition = getExitTransition(outFragment, outIsPop);
|
|
|
|
ArrayList<View> sharedElementsOut = new ArrayList<>();
|
|
ArrayList<View> sharedElementsIn = new ArrayList<>();
|
|
|
|
TransitionSet sharedElementTransition = configureSharedElementsOrdered(sceneRoot,
|
|
nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn,
|
|
enterTransition, exitTransition);
|
|
|
|
if (enterTransition == null && sharedElementTransition == null &&
|
|
exitTransition == null) {
|
|
return; // no transitions!
|
|
}
|
|
|
|
ArrayList<View> exitingViews = configureEnteringExitingViews(exitTransition,
|
|
outFragment, sharedElementsOut, nonExistentView);
|
|
|
|
if (exitingViews == null || exitingViews.isEmpty()) {
|
|
exitTransition = null;
|
|
}
|
|
|
|
if (enterTransition != null) {
|
|
// Ensure the entering transition doesn't target anything until the views are made
|
|
// visible
|
|
enterTransition.addTarget(nonExistentView);
|
|
}
|
|
|
|
Transition transition = mergeTransitions(enterTransition, exitTransition,
|
|
sharedElementTransition, inFragment, fragments.lastInIsPop);
|
|
|
|
if (transition != null) {
|
|
transition.setNameOverrides(nameOverrides);
|
|
final ArrayList<View> enteringViews = new ArrayList<>();
|
|
scheduleRemoveTargets(transition,
|
|
enterTransition, enteringViews, exitTransition, exitingViews,
|
|
sharedElementTransition, sharedElementsIn);
|
|
scheduleTargetChange(sceneRoot, inFragment, nonExistentView, sharedElementsIn,
|
|
enterTransition, enteringViews, exitTransition, exitingViews);
|
|
|
|
TransitionManager.beginDelayedTransition(sceneRoot, transition);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replace hide operations with visibility changes on the exiting views. Instead of making
|
|
* the entire fragment's view GONE, make each exiting view INVISIBLE. At the end of the
|
|
* transition, make the fragment's view GONE.
|
|
*/
|
|
private static void replaceHide(Transition exitTransition, Fragment exitingFragment,
|
|
final ArrayList<View> exitingViews) {
|
|
if (exitingFragment != null && exitTransition != null && exitingFragment.mAdded
|
|
&& exitingFragment.mHidden && exitingFragment.mHiddenChanged) {
|
|
exitingFragment.setHideReplaced(true);
|
|
final View fragmentView = exitingFragment.getView();
|
|
OneShotPreDrawListener.add(exitingFragment.mContainer, () -> {
|
|
setViewVisibility(exitingViews, View.INVISIBLE);
|
|
});
|
|
exitTransition.addListener(new TransitionListenerAdapter() {
|
|
@Override
|
|
public void onTransitionEnd(Transition transition) {
|
|
transition.removeListener(this);
|
|
fragmentView.setVisibility(View.GONE);
|
|
setViewVisibility(exitingViews, View.VISIBLE);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method is used for fragment transitions for ordered transactions to change the
|
|
* enter and exit transition targets after the call to
|
|
* {@link TransitionManager#beginDelayedTransition(ViewGroup, Transition)}. The exit transition
|
|
* must ensure that it does not target any Views and the enter transition must start targeting
|
|
* the Views of the incoming Fragment.
|
|
*
|
|
* @param sceneRoot The fragment container View
|
|
* @param inFragment The last fragment that is entering
|
|
* @param nonExistentView A view that does not exist in the hierarchy that is used as a
|
|
* transition target to ensure no View is targeted.
|
|
* @param sharedElementsIn The shared element Views of the incoming fragment
|
|
* @param enterTransition The enter transition of the incoming fragment
|
|
* @param enteringViews The entering Views of the incoming fragment
|
|
* @param exitTransition The exit transition of the outgoing fragment
|
|
* @param exitingViews The exiting views of the outgoing fragment
|
|
*/
|
|
private static void 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) {
|
|
|
|
OneShotPreDrawListener.add(sceneRoot, () -> {
|
|
if (enterTransition != null) {
|
|
enterTransition.removeTarget(nonExistentView);
|
|
ArrayList<View> views = configureEnteringExitingViews(
|
|
enterTransition, inFragment, sharedElementsIn, nonExistentView);
|
|
enteringViews.addAll(views);
|
|
}
|
|
|
|
if (exitingViews != null) {
|
|
if (exitTransition != null) {
|
|
ArrayList<View> tempExiting = new ArrayList<>();
|
|
tempExiting.add(nonExistentView);
|
|
replaceTargets(exitTransition, exitingViews, tempExiting);
|
|
}
|
|
exitingViews.clear();
|
|
exitingViews.add(nonExistentView);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns a TransitionSet containing the shared element transition. The wrapping TransitionSet
|
|
* targets all shared elements to ensure that no other Views are targeted. The shared element
|
|
* transition can then target any or all shared elements without worrying about accidentally
|
|
* targeting entering or exiting Views.
|
|
*
|
|
* @param inFragment The incoming fragment
|
|
* @param outFragment the outgoing fragment
|
|
* @param isPop True if this is a pop transaction or false if it is a normal (add) transaction.
|
|
* @return A TransitionSet wrapping the shared element transition or null if no such transition
|
|
* exists.
|
|
*/
|
|
private static TransitionSet getSharedElementTransition(Fragment inFragment,
|
|
Fragment outFragment, boolean isPop) {
|
|
if (inFragment == null || outFragment == null) {
|
|
return null;
|
|
}
|
|
Transition transition = cloneTransition(isPop
|
|
? outFragment.getSharedElementReturnTransition()
|
|
: inFragment.getSharedElementEnterTransition());
|
|
if (transition == null) {
|
|
return null;
|
|
}
|
|
TransitionSet transitionSet = new TransitionSet();
|
|
transitionSet.addTransition(transition);
|
|
return transitionSet;
|
|
}
|
|
|
|
/**
|
|
* Returns a clone of the enter transition or null if no such transition exists.
|
|
*/
|
|
private static Transition getEnterTransition(Fragment inFragment, boolean isPop) {
|
|
if (inFragment == null) {
|
|
return null;
|
|
}
|
|
return cloneTransition(isPop ? inFragment.getReenterTransition() :
|
|
inFragment.getEnterTransition());
|
|
}
|
|
|
|
/**
|
|
* Returns a clone of the exit transition or null if no such transition exists.
|
|
*/
|
|
private static Transition getExitTransition(Fragment outFragment, boolean isPop) {
|
|
if (outFragment == null) {
|
|
return null;
|
|
}
|
|
return cloneTransition(isPop ? outFragment.getReturnTransition() :
|
|
outFragment.getExitTransition());
|
|
}
|
|
|
|
/**
|
|
* Returns a clone of a transition or null if it is null
|
|
*/
|
|
private static Transition cloneTransition(Transition transition) {
|
|
if (transition != null) {
|
|
transition = transition.clone();
|
|
}
|
|
return transition;
|
|
}
|
|
|
|
/**
|
|
* Configures the shared elements of an reordered fragment transaction's transition.
|
|
* This retrieves the shared elements of the outgoing and incoming fragments, maps the
|
|
* views, and sets up the epicenter on the transitions.
|
|
* <p>
|
|
* The epicenter of exit and shared element transitions is the first shared element
|
|
* in the outgoing fragment. The epicenter of the entering transition is the first shared
|
|
* element in the incoming fragment.
|
|
*
|
|
* @param sceneRoot The fragment container View
|
|
* @param nonExistentView A View that does not exist in the hierarchy. This is used to
|
|
* prevent transitions from acting on other Views when there is no
|
|
* other target.
|
|
* @param nameOverrides A map of the shared element names from the starting fragment to
|
|
* the final fragment's Views as given in
|
|
* {@link FragmentTransaction#addSharedElement(View, String)}.
|
|
* @param fragments A structure holding the transitioning fragments in this container.
|
|
* @param sharedElementsOut A list modified to contain the shared elements in the outgoing
|
|
* fragment
|
|
* @param sharedElementsIn A list modified to contain the shared elements in the incoming
|
|
* fragment
|
|
* @param enterTransition The transition used for entering Views, modified by applying the
|
|
* epicenter
|
|
* @param exitTransition The transition used for exiting Views, modified by applying the
|
|
* epicenter
|
|
* @return The shared element transition or null if no shared elements exist
|
|
*/
|
|
private static TransitionSet 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) {
|
|
final Fragment inFragment = fragments.lastIn;
|
|
final Fragment outFragment = fragments.firstOut;
|
|
if (inFragment != null) {
|
|
inFragment.getView().setVisibility(View.VISIBLE);
|
|
}
|
|
if (inFragment == null || outFragment == null) {
|
|
return null; // no shared element without a fragment
|
|
}
|
|
|
|
final boolean inIsPop = fragments.lastInIsPop;
|
|
TransitionSet sharedElementTransition = nameOverrides.isEmpty() ? null
|
|
: getSharedElementTransition(inFragment, outFragment, inIsPop);
|
|
|
|
ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides,
|
|
sharedElementTransition, fragments);
|
|
|
|
ArrayMap<String, View> inSharedElements = captureInSharedElements(nameOverrides,
|
|
sharedElementTransition, fragments);
|
|
|
|
if (nameOverrides.isEmpty()) {
|
|
sharedElementTransition = null;
|
|
if (outSharedElements != null) {
|
|
outSharedElements.clear();
|
|
}
|
|
if (inSharedElements != null) {
|
|
inSharedElements.clear();
|
|
}
|
|
} else {
|
|
addSharedElementsWithMatchingNames(sharedElementsOut, outSharedElements,
|
|
nameOverrides.keySet());
|
|
addSharedElementsWithMatchingNames(sharedElementsIn, inSharedElements,
|
|
nameOverrides.values());
|
|
}
|
|
|
|
if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
|
|
// don't call onSharedElementStart/End since there is no transition
|
|
return null;
|
|
}
|
|
|
|
callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);
|
|
|
|
final Rect epicenter;
|
|
final View epicenterView;
|
|
if (sharedElementTransition != null) {
|
|
sharedElementsIn.add(nonExistentView);
|
|
setSharedElementTargets(sharedElementTransition, nonExistentView, sharedElementsOut);
|
|
final boolean outIsPop = fragments.firstOutIsPop;
|
|
final BackStackRecord outTransaction = fragments.firstOutTransaction;
|
|
setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop,
|
|
outTransaction);
|
|
epicenter = new Rect();
|
|
epicenterView = getInEpicenterView(inSharedElements, fragments,
|
|
enterTransition, inIsPop);
|
|
if (epicenterView != null) {
|
|
enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
|
|
@Override
|
|
public Rect onGetEpicenter(Transition transition) {
|
|
return epicenter;
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
epicenter = null;
|
|
epicenterView = null;
|
|
}
|
|
|
|
OneShotPreDrawListener.add(sceneRoot, () -> {
|
|
callSharedElementStartEnd(inFragment, outFragment, inIsPop,
|
|
inSharedElements, false);
|
|
if (epicenterView != null) {
|
|
epicenterView.getBoundsOnScreen(epicenter);
|
|
}
|
|
});
|
|
return sharedElementTransition;
|
|
}
|
|
|
|
/**
|
|
* Add Views from sharedElements into views that have the transitionName in the
|
|
* nameOverridesSet.
|
|
*
|
|
* @param views Views list to add shared elements to
|
|
* @param sharedElements List of shared elements
|
|
* @param nameOverridesSet The transition names for all views to be copied from
|
|
* sharedElements to views.
|
|
*/
|
|
private static void addSharedElementsWithMatchingNames(ArrayList<View> views,
|
|
ArrayMap<String, View> sharedElements, Collection<String> nameOverridesSet) {
|
|
for (int i = sharedElements.size() - 1; i >= 0; i--) {
|
|
View view = sharedElements.valueAt(i);
|
|
if (view != null && nameOverridesSet.contains(view.getTransitionName())) {
|
|
views.add(view);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configures the shared elements of an ordered fragment transaction's transition.
|
|
* This retrieves the shared elements of the incoming fragments, and schedules capturing
|
|
* the incoming fragment's shared elements. It also maps the views, and sets up the epicenter
|
|
* on the transitions.
|
|
* <p>
|
|
* The epicenter of exit and shared element transitions is the first shared element
|
|
* in the outgoing fragment. The epicenter of the entering transition is the first shared
|
|
* element in the incoming fragment.
|
|
*
|
|
* @param sceneRoot The fragment container View
|
|
* @param nonExistentView A View that does not exist in the hierarchy. This is used to
|
|
* prevent transitions from acting on other Views when there is no
|
|
* other target.
|
|
* @param nameOverrides A map of the shared element names from the starting fragment to
|
|
* the final fragment's Views as given in
|
|
* {@link FragmentTransaction#addSharedElement(View, String)}.
|
|
* @param fragments A structure holding the transitioning fragments in this container.
|
|
* @param sharedElementsOut A list modified to contain the shared elements in the outgoing
|
|
* fragment
|
|
* @param sharedElementsIn A list modified to contain the shared elements in the incoming
|
|
* fragment
|
|
* @param enterTransition The transition used for entering Views, modified by applying the
|
|
* epicenter
|
|
* @param exitTransition The transition used for exiting Views, modified by applying the
|
|
* epicenter
|
|
* @return The shared element transition or null if no shared elements exist
|
|
*/
|
|
private static TransitionSet 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) {
|
|
final Fragment inFragment = fragments.lastIn;
|
|
final Fragment outFragment = fragments.firstOut;
|
|
|
|
if (inFragment == null || outFragment == null) {
|
|
return null; // no transition
|
|
}
|
|
|
|
final boolean inIsPop = fragments.lastInIsPop;
|
|
TransitionSet sharedElementTransition = nameOverrides.isEmpty() ? null
|
|
: getSharedElementTransition(inFragment, outFragment, inIsPop);
|
|
|
|
ArrayMap<String, View> outSharedElements = captureOutSharedElements(nameOverrides,
|
|
sharedElementTransition, fragments);
|
|
|
|
if (nameOverrides.isEmpty()) {
|
|
sharedElementTransition = null;
|
|
} else {
|
|
sharedElementsOut.addAll(outSharedElements.values());
|
|
}
|
|
|
|
if (enterTransition == null && exitTransition == null && sharedElementTransition == null) {
|
|
// don't call onSharedElementStart/End since there is no transition
|
|
return null;
|
|
}
|
|
|
|
callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true);
|
|
|
|
final Rect inEpicenter;
|
|
if (sharedElementTransition != null) {
|
|
inEpicenter = new Rect();
|
|
setSharedElementTargets(sharedElementTransition, nonExistentView, sharedElementsOut);
|
|
final boolean outIsPop = fragments.firstOutIsPop;
|
|
final BackStackRecord outTransaction = fragments.firstOutTransaction;
|
|
setOutEpicenter(sharedElementTransition, exitTransition, outSharedElements, outIsPop,
|
|
outTransaction);
|
|
if (enterTransition != null) {
|
|
enterTransition.setEpicenterCallback(new Transition.EpicenterCallback() {
|
|
@Override
|
|
public Rect onGetEpicenter(Transition transition) {
|
|
if (inEpicenter.isEmpty()) {
|
|
return null;
|
|
}
|
|
return inEpicenter;
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
inEpicenter = null;
|
|
}
|
|
|
|
TransitionSet finalSharedElementTransition = sharedElementTransition;
|
|
|
|
OneShotPreDrawListener.add(sceneRoot, () -> {
|
|
ArrayMap<String, View> inSharedElements = captureInSharedElements(
|
|
nameOverrides, finalSharedElementTransition, fragments);
|
|
|
|
if (inSharedElements != null) {
|
|
sharedElementsIn.addAll(inSharedElements.values());
|
|
sharedElementsIn.add(nonExistentView);
|
|
}
|
|
|
|
callSharedElementStartEnd(inFragment, outFragment, inIsPop,
|
|
inSharedElements, false);
|
|
if (finalSharedElementTransition != null) {
|
|
finalSharedElementTransition.getTargets().clear();
|
|
finalSharedElementTransition.getTargets().addAll(sharedElementsIn);
|
|
replaceTargets(finalSharedElementTransition, sharedElementsOut,
|
|
sharedElementsIn);
|
|
|
|
final View inEpicenterView = getInEpicenterView(inSharedElements,
|
|
fragments, enterTransition, inIsPop);
|
|
if (inEpicenterView != null) {
|
|
inEpicenterView.getBoundsOnScreen(inEpicenter);
|
|
}
|
|
}
|
|
});
|
|
return sharedElementTransition;
|
|
}
|
|
|
|
/**
|
|
* Finds the shared elements in the outgoing fragment. It also calls
|
|
* {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control
|
|
* of the shared element mapping. {@code nameOverrides} is updated to match the
|
|
* actual transition name of the mapped shared elements.
|
|
*
|
|
* @param nameOverrides A map of the shared element names from the starting fragment to
|
|
* the final fragment's Views as given in
|
|
* {@link FragmentTransaction#addSharedElement(View, String)}.
|
|
* @param sharedElementTransition The shared element transition
|
|
* @param fragments A structure holding the transitioning fragments in this container.
|
|
* @return The mapping of shared element names to the Views in the hierarchy or null
|
|
* if there is no shared element transition.
|
|
*/
|
|
private static ArrayMap<String, View> captureOutSharedElements(
|
|
ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition,
|
|
FragmentContainerTransition fragments) {
|
|
if (nameOverrides.isEmpty() || sharedElementTransition == null) {
|
|
nameOverrides.clear();
|
|
return null;
|
|
}
|
|
final Fragment outFragment = fragments.firstOut;
|
|
final ArrayMap<String, View> outSharedElements = new ArrayMap<>();
|
|
outFragment.getView().findNamedViews(outSharedElements);
|
|
|
|
final SharedElementCallback sharedElementCallback;
|
|
final ArrayList<String> names;
|
|
final BackStackRecord outTransaction = fragments.firstOutTransaction;
|
|
if (fragments.firstOutIsPop) {
|
|
sharedElementCallback = outFragment.getEnterTransitionCallback();
|
|
names = outTransaction.mSharedElementTargetNames;
|
|
} else {
|
|
sharedElementCallback = outFragment.getExitTransitionCallback();
|
|
names = outTransaction.mSharedElementSourceNames;
|
|
}
|
|
|
|
outSharedElements.retainAll(names);
|
|
if (sharedElementCallback != null) {
|
|
sharedElementCallback.onMapSharedElements(names, outSharedElements);
|
|
for (int i = names.size() - 1; i >= 0; i--) {
|
|
String name = names.get(i);
|
|
View view = outSharedElements.get(name);
|
|
if (view == null) {
|
|
nameOverrides.remove(name);
|
|
} else if (!name.equals(view.getTransitionName())) {
|
|
String targetValue = nameOverrides.remove(name);
|
|
nameOverrides.put(view.getTransitionName(), targetValue);
|
|
}
|
|
}
|
|
} else {
|
|
nameOverrides.retainAll(outSharedElements.keySet());
|
|
}
|
|
return outSharedElements;
|
|
}
|
|
|
|
/**
|
|
* Finds the shared elements in the incoming fragment. It also calls
|
|
* {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control
|
|
* of the shared element mapping. {@code nameOverrides} is updated to match the
|
|
* actual transition name of the mapped shared elements.
|
|
*
|
|
* @param nameOverrides A map of the shared element names from the starting fragment to
|
|
* the final fragment's Views as given in
|
|
* {@link FragmentTransaction#addSharedElement(View, String)}.
|
|
* @param sharedElementTransition The shared element transition
|
|
* @param fragments A structure holding the transitioning fragments in this container.
|
|
* @return The mapping of shared element names to the Views in the hierarchy or null
|
|
* if there is no shared element transition.
|
|
*/
|
|
private static ArrayMap<String, View> captureInSharedElements(
|
|
ArrayMap<String, String> nameOverrides, TransitionSet sharedElementTransition,
|
|
FragmentContainerTransition fragments) {
|
|
Fragment inFragment = fragments.lastIn;
|
|
final View fragmentView = inFragment.getView();
|
|
if (nameOverrides.isEmpty() || sharedElementTransition == null || fragmentView == null) {
|
|
nameOverrides.clear();
|
|
return null;
|
|
}
|
|
final ArrayMap<String, View> inSharedElements = new ArrayMap<>();
|
|
fragmentView.findNamedViews(inSharedElements);
|
|
|
|
final SharedElementCallback sharedElementCallback;
|
|
final ArrayList<String> names;
|
|
final BackStackRecord inTransaction = fragments.lastInTransaction;
|
|
if (fragments.lastInIsPop) {
|
|
sharedElementCallback = inFragment.getExitTransitionCallback();
|
|
names = inTransaction.mSharedElementSourceNames;
|
|
} else {
|
|
sharedElementCallback = inFragment.getEnterTransitionCallback();
|
|
names = inTransaction.mSharedElementTargetNames;
|
|
}
|
|
|
|
if (names != null) {
|
|
inSharedElements.retainAll(names);
|
|
}
|
|
if (names != null && sharedElementCallback != null) {
|
|
sharedElementCallback.onMapSharedElements(names, inSharedElements);
|
|
for (int i = names.size() - 1; i >= 0; i--) {
|
|
String name = names.get(i);
|
|
View view = inSharedElements.get(name);
|
|
if (view == null) {
|
|
String key = findKeyForValue(nameOverrides, name);
|
|
if (key != null) {
|
|
nameOverrides.remove(key);
|
|
}
|
|
} else if (!name.equals(view.getTransitionName())) {
|
|
String key = findKeyForValue(nameOverrides, name);
|
|
if (key != null) {
|
|
nameOverrides.put(key, view.getTransitionName());
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
retainValues(nameOverrides, inSharedElements);
|
|
}
|
|
return inSharedElements;
|
|
}
|
|
|
|
/**
|
|
* Utility to find the String key in {@code map} that maps to {@code value}.
|
|
*/
|
|
private static String findKeyForValue(ArrayMap<String, String> map, String value) {
|
|
final int numElements = map.size();
|
|
for (int i = 0; i < numElements; i++) {
|
|
if (value.equals(map.valueAt(i))) {
|
|
return map.keyAt(i);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the View in the incoming Fragment that should be used as the epicenter.
|
|
*
|
|
* @param inSharedElements The mapping of shared element names to Views in the
|
|
* incoming fragment.
|
|
* @param fragments A structure holding the transitioning fragments in this container.
|
|
* @param enterTransition The transition used for the incoming Fragment's views
|
|
* @param inIsPop Is the incoming fragment being added as a pop transaction?
|
|
*/
|
|
private static View getInEpicenterView(ArrayMap<String, View> inSharedElements,
|
|
FragmentContainerTransition fragments,
|
|
Transition enterTransition, boolean inIsPop) {
|
|
BackStackRecord inTransaction = fragments.lastInTransaction;
|
|
if (enterTransition != null && inSharedElements != null
|
|
&& inTransaction.mSharedElementSourceNames != null
|
|
&& !inTransaction.mSharedElementSourceNames.isEmpty()) {
|
|
final String targetName = inIsPop
|
|
? inTransaction.mSharedElementSourceNames.get(0)
|
|
: inTransaction.mSharedElementTargetNames.get(0);
|
|
return inSharedElements.get(targetName);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sets the epicenter for the exit transition.
|
|
*
|
|
* @param sharedElementTransition The shared element transition
|
|
* @param exitTransition The transition for the outgoing fragment's views
|
|
* @param outSharedElements Shared elements in the outgoing fragment
|
|
* @param outIsPop Is the outgoing fragment being removed as a pop transaction?
|
|
* @param outTransaction The transaction that caused the fragment to be removed.
|
|
*/
|
|
private static void setOutEpicenter(TransitionSet sharedElementTransition,
|
|
Transition exitTransition, ArrayMap<String, View> outSharedElements, boolean outIsPop,
|
|
BackStackRecord outTransaction) {
|
|
if (outTransaction.mSharedElementSourceNames != null &&
|
|
!outTransaction.mSharedElementSourceNames.isEmpty()) {
|
|
final String sourceName = outIsPop
|
|
? outTransaction.mSharedElementTargetNames.get(0)
|
|
: outTransaction.mSharedElementSourceNames.get(0);
|
|
final View outEpicenterView = outSharedElements.get(sourceName);
|
|
setEpicenter(sharedElementTransition, outEpicenterView);
|
|
|
|
if (exitTransition != null) {
|
|
setEpicenter(exitTransition, outEpicenterView);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a transition epicenter to the rectangle of a given View.
|
|
*/
|
|
private static void setEpicenter(Transition transition, View view) {
|
|
if (view != null) {
|
|
final Rect epicenter = new Rect();
|
|
view.getBoundsOnScreen(epicenter);
|
|
|
|
transition.setEpicenterCallback(new Transition.EpicenterCallback() {
|
|
@Override
|
|
public Rect onGetEpicenter(Transition transition) {
|
|
return epicenter;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A utility to retain only the mappings in {@code nameOverrides} that have a value
|
|
* that has a key in {@code namedViews}. This is a useful equivalent to
|
|
* {@link ArrayMap#retainAll(Collection)} for values.
|
|
*/
|
|
private static void retainValues(ArrayMap<String, String> nameOverrides,
|
|
ArrayMap<String, View> namedViews) {
|
|
for (int i = nameOverrides.size() - 1; i >= 0; i--) {
|
|
final String targetName = nameOverrides.valueAt(i);
|
|
if (!namedViews.containsKey(targetName)) {
|
|
nameOverrides.removeAt(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls the {@link SharedElementCallback#onSharedElementStart(List, List, List)} or
|
|
* {@link SharedElementCallback#onSharedElementEnd(List, List, List)} on the appropriate
|
|
* incoming or outgoing fragment.
|
|
*
|
|
* @param inFragment The incoming fragment
|
|
* @param outFragment The outgoing fragment
|
|
* @param isPop Is the incoming fragment part of a pop transaction?
|
|
* @param sharedElements The shared element Views
|
|
* @param isStart Call the start or end call on the SharedElementCallback
|
|
*/
|
|
private static void callSharedElementStartEnd(Fragment inFragment, Fragment outFragment,
|
|
boolean isPop, ArrayMap<String, View> sharedElements, boolean isStart) {
|
|
SharedElementCallback sharedElementCallback = isPop
|
|
? outFragment.getEnterTransitionCallback()
|
|
: inFragment.getEnterTransitionCallback();
|
|
if (sharedElementCallback != null) {
|
|
ArrayList<View> views = new ArrayList<>();
|
|
ArrayList<String> names = new ArrayList<>();
|
|
final int count = sharedElements == null ? 0 : sharedElements.size();
|
|
for (int i = 0; i < count; i++) {
|
|
names.add(sharedElements.keyAt(i));
|
|
views.add(sharedElements.valueAt(i));
|
|
}
|
|
if (isStart) {
|
|
sharedElementCallback.onSharedElementStart(names, views, null);
|
|
} else {
|
|
sharedElementCallback.onSharedElementEnd(names, views, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds all children of the shared elements and sets the wrapping TransitionSet
|
|
* targets to point to those. It also limits transitions that have no targets to the
|
|
* specific shared elements. This allows developers to target child views of the
|
|
* shared elements specifically, but this doesn't happen by default.
|
|
*/
|
|
private static void setSharedElementTargets(TransitionSet transition,
|
|
View nonExistentView, ArrayList<View> sharedViews) {
|
|
final List<View> views = transition.getTargets();
|
|
views.clear();
|
|
final int count = sharedViews.size();
|
|
for (int i = 0; i < count; i++) {
|
|
final View view = sharedViews.get(i);
|
|
bfsAddViewChildren(views, view);
|
|
}
|
|
views.add(nonExistentView);
|
|
sharedViews.add(nonExistentView);
|
|
addTargets(transition, sharedViews);
|
|
}
|
|
|
|
/**
|
|
* Uses a breadth-first scheme to add startView and all of its children to views.
|
|
* It won't add a child if it is already in views.
|
|
*/
|
|
private static void bfsAddViewChildren(final List<View> views, final View startView) {
|
|
final int startIndex = views.size();
|
|
if (containedBeforeIndex(views, startView, startIndex)) {
|
|
return; // This child is already in the list, so all its children are also.
|
|
}
|
|
views.add(startView);
|
|
for (int index = startIndex; index < views.size(); index++) {
|
|
final View view = views.get(index);
|
|
if (view instanceof ViewGroup) {
|
|
ViewGroup viewGroup = (ViewGroup) view;
|
|
final int childCount = viewGroup.getChildCount();
|
|
for (int childIndex = 0; childIndex < childCount; childIndex++) {
|
|
final View child = viewGroup.getChildAt(childIndex);
|
|
if (!containedBeforeIndex(views, child, startIndex)) {
|
|
views.add(child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Does a linear search through views for view, limited to maxIndex.
|
|
*/
|
|
private static boolean containedBeforeIndex(final List<View> views, final View view,
|
|
final int maxIndex) {
|
|
for (int i = 0; i < maxIndex; i++) {
|
|
if (views.get(i) == view) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* After the transition has started, remove all targets that we added to the transitions
|
|
* so that the transitions are left in a clean state.
|
|
*/
|
|
private static void 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) {
|
|
overalTransition.addListener(new TransitionListenerAdapter() {
|
|
@Override
|
|
public void onTransitionStart(Transition transition) {
|
|
if (enterTransition != null) {
|
|
replaceTargets(enterTransition, enteringViews, null);
|
|
}
|
|
if (exitTransition != null) {
|
|
replaceTargets(exitTransition, exitingViews, null);
|
|
}
|
|
if (sharedElementTransition != null) {
|
|
replaceTargets(sharedElementTransition, sharedElementsIn, null);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onTransitionEnd(Transition transition) {
|
|
transition.removeListener(this);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This method removes the views from transitions that target ONLY those views and
|
|
* replaces them with the new targets list.
|
|
* The views list should match those added in addTargets and should contain
|
|
* one view that is not in the view hierarchy (state.nonExistentView).
|
|
*/
|
|
public static void replaceTargets(Transition transition, ArrayList<View> oldTargets,
|
|
ArrayList<View> newTargets) {
|
|
if (transition instanceof TransitionSet) {
|
|
TransitionSet set = (TransitionSet) transition;
|
|
int numTransitions = set.getTransitionCount();
|
|
for (int i = 0; i < numTransitions; i++) {
|
|
Transition child = set.getTransitionAt(i);
|
|
replaceTargets(child, oldTargets, newTargets);
|
|
}
|
|
} else if (!hasSimpleTarget(transition)) {
|
|
List<View> targets = transition.getTargets();
|
|
if (targets != null && targets.size() == oldTargets.size() &&
|
|
targets.containsAll(oldTargets)) {
|
|
// We have an exact match. We must have added these earlier in addTargets
|
|
final int targetCount = newTargets == null ? 0 : newTargets.size();
|
|
for (int i = 0; i < targetCount; i++) {
|
|
transition.addTarget(newTargets.get(i));
|
|
}
|
|
for (int i = oldTargets.size() - 1; i >= 0; i--) {
|
|
transition.removeTarget(oldTargets.get(i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method adds views as targets to the transition, but only if the transition
|
|
* doesn't already have a target. It is best for views to contain one View object
|
|
* that does not exist in the view hierarchy (state.nonExistentView) so that
|
|
* when they are removed later, a list match will suffice to remove the targets.
|
|
* Otherwise, if you happened to have targeted the exact views for the transition,
|
|
* the replaceTargets call will remove them unexpectedly.
|
|
*/
|
|
public static void addTargets(Transition transition, ArrayList<View> views) {
|
|
if (transition == null) {
|
|
return;
|
|
}
|
|
if (transition instanceof TransitionSet) {
|
|
TransitionSet set = (TransitionSet) transition;
|
|
int numTransitions = set.getTransitionCount();
|
|
for (int i = 0; i < numTransitions; i++) {
|
|
Transition child = set.getTransitionAt(i);
|
|
addTargets(child, views);
|
|
}
|
|
} else if (!hasSimpleTarget(transition)) {
|
|
List<View> targets = transition.getTargets();
|
|
if (isNullOrEmpty(targets)) {
|
|
// We can just add the target views
|
|
int numViews = views.size();
|
|
for (int i = 0; i < numViews; i++) {
|
|
transition.addTarget(views.get(i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if there are any targets based on ID, transition or type.
|
|
*/
|
|
private static boolean hasSimpleTarget(Transition transition) {
|
|
return !isNullOrEmpty(transition.getTargetIds()) ||
|
|
!isNullOrEmpty(transition.getTargetNames()) ||
|
|
!isNullOrEmpty(transition.getTargetTypes());
|
|
}
|
|
|
|
/**
|
|
* Simple utility to detect if a list is null or has no elements.
|
|
*/
|
|
private static boolean isNullOrEmpty(List list) {
|
|
return list == null || list.isEmpty();
|
|
}
|
|
|
|
private static ArrayList<View> configureEnteringExitingViews(Transition transition,
|
|
Fragment fragment, ArrayList<View> sharedElements, View nonExistentView) {
|
|
ArrayList<View> viewList = null;
|
|
if (transition != null) {
|
|
viewList = new ArrayList<>();
|
|
View root = fragment.getView();
|
|
if (root != null) {
|
|
root.captureTransitioningViews(viewList);
|
|
}
|
|
if (sharedElements != null) {
|
|
viewList.removeAll(sharedElements);
|
|
}
|
|
if (!viewList.isEmpty()) {
|
|
viewList.add(nonExistentView);
|
|
addTargets(transition, viewList);
|
|
}
|
|
}
|
|
return viewList;
|
|
}
|
|
|
|
/**
|
|
* Sets the visibility of all Views in {@code views} to {@code visibility}.
|
|
*/
|
|
private static void setViewVisibility(ArrayList<View> views, @View.Visibility int visibility) {
|
|
if (views == null) {
|
|
return;
|
|
}
|
|
for (int i = views.size() - 1; i >= 0; i--) {
|
|
final View view = views.get(i);
|
|
view.setVisibility(visibility);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merges exit, shared element, and enter transitions so that they act together or
|
|
* sequentially as defined in the fragments.
|
|
*/
|
|
private static Transition mergeTransitions(Transition enterTransition,
|
|
Transition exitTransition, Transition sharedElementTransition, Fragment inFragment,
|
|
boolean isPop) {
|
|
boolean overlap = true;
|
|
if (enterTransition != null && exitTransition != null && inFragment != null) {
|
|
overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() :
|
|
inFragment.getAllowEnterTransitionOverlap();
|
|
}
|
|
|
|
// Wrap the transitions. Explicit targets like in enter and exit will cause the
|
|
// views to be targeted regardless of excluded views. If that happens, then the
|
|
// excluded fragments views (hidden fragments) will still be in the transition.
|
|
|
|
Transition transition;
|
|
if (overlap) {
|
|
// Regular transition -- do it all together
|
|
TransitionSet transitionSet = new TransitionSet();
|
|
if (enterTransition != null) {
|
|
transitionSet.addTransition(enterTransition);
|
|
}
|
|
if (exitTransition != null) {
|
|
transitionSet.addTransition(exitTransition);
|
|
}
|
|
if (sharedElementTransition != null) {
|
|
transitionSet.addTransition(sharedElementTransition);
|
|
}
|
|
transition = transitionSet;
|
|
} else {
|
|
// First do exit, then enter, but allow shared element transition to happen
|
|
// during both.
|
|
Transition staggered = null;
|
|
if (exitTransition != null && enterTransition != null) {
|
|
staggered = new TransitionSet()
|
|
.addTransition(exitTransition)
|
|
.addTransition(enterTransition)
|
|
.setOrdering(TransitionSet.ORDERING_SEQUENTIAL);
|
|
} else if (exitTransition != null) {
|
|
staggered = exitTransition;
|
|
} else if (enterTransition != null) {
|
|
staggered = enterTransition;
|
|
}
|
|
if (sharedElementTransition != null) {
|
|
TransitionSet together = new TransitionSet();
|
|
if (staggered != null) {
|
|
together.addTransition(staggered);
|
|
}
|
|
together.addTransition(sharedElementTransition);
|
|
transition = together;
|
|
} else {
|
|
transition = staggered;
|
|
}
|
|
}
|
|
return transition;
|
|
}
|
|
|
|
/**
|
|
* Finds the first removed fragment and last added fragments when going forward.
|
|
* If none of the fragments have transitions, then both lists will be empty.
|
|
*
|
|
* @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
|
|
* and last fragments to be added. This will be modified by
|
|
* this method.
|
|
*/
|
|
public static void calculateFragments(BackStackRecord transaction,
|
|
SparseArray<FragmentContainerTransition> transitioningFragments,
|
|
boolean isReordered) {
|
|
final int numOps = transaction.mOps.size();
|
|
for (int opNum = 0; opNum < numOps; opNum++) {
|
|
final BackStackRecord.Op op = transaction.mOps.get(opNum);
|
|
addToFirstInLastOut(transaction, op, transitioningFragments, false, isReordered);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the first removed fragment and last added fragments when popping the back stack.
|
|
* If none of the fragments have transitions, then both lists will be empty.
|
|
*
|
|
* @param transitioningFragments Keyed on the container ID, the first fragments to be removed,
|
|
* and last fragments to be added. This will be modified by
|
|
* this method.
|
|
*/
|
|
public static void calculatePopFragments(BackStackRecord transaction,
|
|
SparseArray<FragmentContainerTransition> transitioningFragments, boolean isReordered) {
|
|
if (!transaction.mManager.mContainer.onHasView()) {
|
|
return; // nothing to see, so no transitions
|
|
}
|
|
final int numOps = transaction.mOps.size();
|
|
for (int opNum = numOps - 1; opNum >= 0; opNum--) {
|
|
final BackStackRecord.Op op = transaction.mOps.get(opNum);
|
|
addToFirstInLastOut(transaction, op, transitioningFragments, true, isReordered);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Examines the {@code command} and may set the first out or last in fragment for the fragment's
|
|
* container.
|
|
*
|
|
* @param transaction The executing transaction
|
|
* @param op The operation being run.
|
|
* @param transitioningFragments A structure holding the first in and last out fragments
|
|
* for each fragment container.
|
|
* @param isPop Is the operation a pop?
|
|
* @param isReorderedTransaction True if the operations have been partially executed and the
|
|
* added fragments have Views in the hierarchy or false if the
|
|
* operations haven't been executed yet.
|
|
*/
|
|
@SuppressWarnings("ReferenceEquality")
|
|
private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op,
|
|
SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop,
|
|
boolean isReorderedTransaction) {
|
|
final Fragment fragment = op.fragment;
|
|
if (fragment == null) {
|
|
return; // no fragment, no transition
|
|
}
|
|
final int containerId = fragment.mContainerId;
|
|
if (containerId == 0) {
|
|
return; // no container, no transition
|
|
}
|
|
final int command = isPop ? INVERSE_OPS[op.cmd] : op.cmd;
|
|
boolean setLastIn = false;
|
|
boolean wasRemoved = false;
|
|
boolean setFirstOut = false;
|
|
boolean wasAdded = false;
|
|
switch (command) {
|
|
case BackStackRecord.OP_SHOW:
|
|
if (isReorderedTransaction) {
|
|
setLastIn = fragment.mHiddenChanged && !fragment.mHidden &&
|
|
fragment.mAdded;
|
|
} else {
|
|
setLastIn = fragment.mHidden;
|
|
}
|
|
wasAdded = true;
|
|
break;
|
|
case BackStackRecord.OP_ADD:
|
|
case BackStackRecord.OP_ATTACH:
|
|
if (isReorderedTransaction) {
|
|
setLastIn = fragment.mIsNewlyAdded;
|
|
} else {
|
|
setLastIn = !fragment.mAdded && !fragment.mHidden;
|
|
}
|
|
wasAdded = true;
|
|
break;
|
|
case BackStackRecord.OP_HIDE:
|
|
if (isReorderedTransaction) {
|
|
setFirstOut = fragment.mHiddenChanged && fragment.mAdded &&
|
|
fragment.mHidden;
|
|
} else {
|
|
setFirstOut = fragment.mAdded && !fragment.mHidden;
|
|
}
|
|
wasRemoved = true;
|
|
break;
|
|
case BackStackRecord.OP_REMOVE:
|
|
case BackStackRecord.OP_DETACH:
|
|
if (isReorderedTransaction) {
|
|
setFirstOut = !fragment.mAdded && fragment.mView != null
|
|
&& fragment.mView.getVisibility() == View.VISIBLE
|
|
&& fragment.mView.getTransitionAlpha() > 0;
|
|
} else {
|
|
setFirstOut = fragment.mAdded && !fragment.mHidden;
|
|
}
|
|
wasRemoved = true;
|
|
break;
|
|
}
|
|
FragmentContainerTransition containerTransition = transitioningFragments.get(containerId);
|
|
if (setLastIn) {
|
|
containerTransition =
|
|
ensureContainer(containerTransition, transitioningFragments, containerId);
|
|
containerTransition.lastIn = fragment;
|
|
containerTransition.lastInIsPop = isPop;
|
|
containerTransition.lastInTransaction = transaction;
|
|
}
|
|
if (!isReorderedTransaction && wasAdded) {
|
|
if (containerTransition != null && containerTransition.firstOut == fragment) {
|
|
containerTransition.firstOut = null;
|
|
}
|
|
|
|
/*
|
|
* Ensure that fragments that are entering are at least at the CREATED state
|
|
* so that they may load Transitions using TransitionInflater.
|
|
*/
|
|
FragmentManagerImpl manager = transaction.mManager;
|
|
if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED &&
|
|
manager.mHost.getContext().getApplicationInfo().targetSdkVersion >=
|
|
Build.VERSION_CODES.N && !transaction.mReorderingAllowed) {
|
|
manager.makeActive(fragment);
|
|
manager.moveToState(fragment, Fragment.CREATED, 0, 0, false);
|
|
}
|
|
}
|
|
if (setFirstOut && (containerTransition == null || containerTransition.firstOut == null)) {
|
|
containerTransition =
|
|
ensureContainer(containerTransition, transitioningFragments, containerId);
|
|
containerTransition.firstOut = fragment;
|
|
containerTransition.firstOutIsPop = isPop;
|
|
containerTransition.firstOutTransaction = transaction;
|
|
}
|
|
|
|
if (!isReorderedTransaction && wasRemoved &&
|
|
(containerTransition != null && containerTransition.lastIn == fragment)) {
|
|
containerTransition.lastIn = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensures that a FragmentContainerTransition has been added to the SparseArray. If so,
|
|
* it returns the existing one. If not, one is created and added to the SparseArray and
|
|
* returned.
|
|
*/
|
|
private static FragmentContainerTransition ensureContainer(
|
|
FragmentContainerTransition containerTransition,
|
|
SparseArray<FragmentContainerTransition> transitioningFragments, int containerId) {
|
|
if (containerTransition == null) {
|
|
containerTransition = new FragmentContainerTransition();
|
|
transitioningFragments.put(containerId, containerTransition);
|
|
}
|
|
return containerTransition;
|
|
}
|
|
|
|
/**
|
|
* Tracks the last fragment added and first fragment removed for fragment transitions.
|
|
* This also tracks which fragments are changed by push or pop transactions.
|
|
*/
|
|
public static class FragmentContainerTransition {
|
|
/**
|
|
* The last fragment added/attached/shown in its container
|
|
*/
|
|
public Fragment lastIn;
|
|
|
|
/**
|
|
* true when lastIn was added during a pop transaction or false if added with a push
|
|
*/
|
|
public boolean lastInIsPop;
|
|
|
|
/**
|
|
* The transaction that included the last in fragment
|
|
*/
|
|
public BackStackRecord lastInTransaction;
|
|
|
|
/**
|
|
* The first fragment with a View that was removed/detached/hidden in its container.
|
|
*/
|
|
public Fragment firstOut;
|
|
|
|
/**
|
|
* true when firstOut was removed during a pop transaction or false otherwise
|
|
*/
|
|
public boolean firstOutIsPop;
|
|
|
|
/**
|
|
* The transaction that included the first out fragment
|
|
*/
|
|
public BackStackRecord firstOutTransaction;
|
|
}
|
|
}
|