diff options
| -rw-r--r-- | api/current.txt | 2 | ||||
| -rw-r--r-- | api/system-current.txt | 2 | ||||
| -rw-r--r-- | api/test-current.txt | 2 | ||||
| -rw-r--r-- | core/java/android/app/BackStackRecord.java | 954 | ||||
| -rw-r--r-- | core/java/android/app/Fragment.java | 437 | ||||
| -rw-r--r-- | core/java/android/app/FragmentHostCallback.java | 16 | ||||
| -rw-r--r-- | core/java/android/app/FragmentManager.java | 430 | ||||
| -rw-r--r-- | core/java/android/app/FragmentTransition.java | 1330 |
8 files changed, 2097 insertions, 1076 deletions
diff --git a/api/current.txt b/api/current.txt index a3d8131758d0..662cca9e4f8c 100644 --- a/api/current.txt +++ b/api/current.txt @@ -4489,6 +4489,7 @@ package android.app { method public void onTrimMemory(int); method public void onViewCreated(android.view.View, android.os.Bundle); method public void onViewStateRestored(android.os.Bundle); + method public void postponeEnterTransition(); method public void registerForContextMenu(android.view.View); method public final void requestPermissions(java.lang.String[], int); method public void setAllowEnterTransitionOverlap(boolean); @@ -4514,6 +4515,7 @@ package android.app { method public void startActivityForResult(android.content.Intent, int); method public void startActivityForResult(android.content.Intent, int, android.os.Bundle); method public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent, int, int, int, android.os.Bundle) throws android.content.IntentSender.SendIntentException; + method public void startPostponedEnterTransition(); method public void unregisterForContextMenu(android.view.View); } diff --git a/api/system-current.txt b/api/system-current.txt index b2f72fc20139..32eb682057be 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4634,6 +4634,7 @@ package android.app { method public void onTrimMemory(int); method public void onViewCreated(android.view.View, android.os.Bundle); method public void onViewStateRestored(android.os.Bundle); + method public void postponeEnterTransition(); method public void registerForContextMenu(android.view.View); method public final void requestPermissions(java.lang.String[], int); method public void setAllowEnterTransitionOverlap(boolean); @@ -4659,6 +4660,7 @@ package android.app { method public void startActivityForResult(android.content.Intent, int); method public void startActivityForResult(android.content.Intent, int, android.os.Bundle); method public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent, int, int, int, android.os.Bundle) throws android.content.IntentSender.SendIntentException; + method public void startPostponedEnterTransition(); method public void unregisterForContextMenu(android.view.View); } diff --git a/api/test-current.txt b/api/test-current.txt index 22fd95547a71..da172b638049 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -4492,6 +4492,7 @@ package android.app { method public void onTrimMemory(int); method public void onViewCreated(android.view.View, android.os.Bundle); method public void onViewStateRestored(android.os.Bundle); + method public void postponeEnterTransition(); method public void registerForContextMenu(android.view.View); method public final void requestPermissions(java.lang.String[], int); method public void setAllowEnterTransitionOverlap(boolean); @@ -4517,6 +4518,7 @@ package android.app { method public void startActivityForResult(android.content.Intent, int); method public void startActivityForResult(android.content.Intent, int, android.os.Bundle); method public void startIntentSenderForResult(android.content.IntentSender, int, android.content.Intent, int, int, int, android.os.Bundle) throws android.content.IntentSender.SendIntentException; + method public void startPostponedEnterTransition(); method public void unregisterForContextMenu(android.view.View); } diff --git a/core/java/android/app/BackStackRecord.java b/core/java/android/app/BackStackRecord.java index c589466f3b13..cf794c5b410e 100644 --- a/core/java/android/app/BackStackRecord.java +++ b/core/java/android/app/BackStackRecord.java @@ -16,22 +16,13 @@ package android.app; -import android.content.Context; -import android.graphics.Rect; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import android.transition.Transition; -import android.transition.TransitionManager; -import android.transition.TransitionSet; -import android.util.ArrayMap; import android.util.Log; import android.util.LogWriter; -import android.util.SparseArray; import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; import com.android.internal.util.FastPrintWriter; @@ -39,7 +30,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.reflect.Modifier; import java.util.ArrayList; -import java.util.List; final class BackStackState implements Parcelable { final int[] mOps; @@ -669,7 +659,7 @@ final class BackStackRecord extends FragmentTransaction implements } /** - * Implementation of {@link FragmentManagerImpl.android.app.FragmentManagerImpl.OpGenerator}. + * Implementation of {@link android.app.FragmentManagerImpl.OpGenerator}. * This operation is added to the list of pending actions during {@link #commit()}, and * will be executed on the UI thread to run this FragmentTransaction. * @@ -691,6 +681,43 @@ final class BackStackRecord extends FragmentTransaction implements return true; } + boolean interactsWith(int containerId) { + final int numOps = mOps.size(); + for (int opNum = 0; opNum < numOps; opNum++) { + final Op op = mOps.get(opNum); + if (op.fragment.mContainerId == containerId) { + return true; + } + } + return false; + } + + boolean interactsWith(ArrayList<BackStackRecord> records, int startIndex, int endIndex) { + if (endIndex == startIndex) { + return false; + } + final int numOps = mOps.size(); + int lastContainer = -1; + for (int opNum = 0; opNum < numOps; opNum++) { + final Op op = mOps.get(opNum); + final int container = op.fragment.mContainerId; + if (container != 0 && container != lastContainer) { + lastContainer = container; + for (int i = startIndex; i < endIndex; i++) { + BackStackRecord record = records.get(i); + final int numThoseOps = record.mOps.size(); + for (int thoseOpIndex = 0; thoseOpIndex < numThoseOps; thoseOpIndex++) { + final Op thatOp = record.mOps.get(thoseOpIndex); + if (thatOp.fragment.mContainerId == container) { + return true; + } + } + } + } + } + return false; + } + /** * Executes the operations contained within this transaction. The Fragment states will only * be modified if optimizations are not allowed. @@ -700,31 +727,30 @@ final class BackStackRecord extends FragmentTransaction implements for (int opNum = 0; opNum < numOps; opNum++) { final Op op = mOps.get(opNum); final Fragment f = op.fragment; - f.mNextTransition = mTransition; - f.mNextTransitionStyle = mTransitionStyle; + f.setNextTransition(mTransition, mTransitionStyle); switch (op.cmd) { case OP_ADD: - f.mNextAnim = op.enterAnim; + f.setNextAnim(op.enterAnim); mManager.addFragment(f, false); break; case OP_REMOVE: - f.mNextAnim = op.exitAnim; + f.setNextAnim(op.exitAnim); mManager.removeFragment(f); break; case OP_HIDE: - f.mNextAnim = op.exitAnim; + f.setNextAnim(op.exitAnim); mManager.hideFragment(f); break; case OP_SHOW: - f.mNextAnim = op.enterAnim; + f.setNextAnim(op.enterAnim); mManager.showFragment(f); break; case OP_DETACH: - f.mNextAnim = op.exitAnim; + f.setNextAnim(op.exitAnim); mManager.detachFragment(f); break; case OP_ATTACH: - f.mNextAnim = op.enterAnim; + f.setNextAnim(op.enterAnim); mManager.attachFragment(f); break; default: @@ -748,31 +774,30 @@ final class BackStackRecord extends FragmentTransaction implements for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) { final Op op = mOps.get(opNum); Fragment f = op.fragment; - f.mNextTransition = FragmentManagerImpl.reverseTransit(mTransition); - f.mNextTransitionStyle = mTransitionStyle; + f.setNextTransition(FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); switch (op.cmd) { case OP_ADD: - f.mNextAnim = op.popExitAnim; + f.setNextAnim(op.popExitAnim); mManager.removeFragment(f); break; case OP_REMOVE: - f.mNextAnim = op.popEnterAnim; + f.setNextAnim(op.popEnterAnim); mManager.addFragment(f, false); break; case OP_HIDE: - f.mNextAnim = op.popEnterAnim; + f.setNextAnim(op.popEnterAnim); mManager.showFragment(f); break; case OP_SHOW: - f.mNextAnim = op.popExitAnim; + f.setNextAnim(op.popExitAnim); mManager.hideFragment(f); break; case OP_DETACH: - f.mNextAnim = op.popEnterAnim; + f.setNextAnim(op.popEnterAnim); mManager.attachFragment(f); break; case OP_ATTACH: - f.mNextAnim = op.popExitAnim; + f.setNextAnim(op.popExitAnim); mManager.detachFragment(f); break; default: @@ -803,7 +828,7 @@ final class BackStackRecord extends FragmentTransaction implements case OP_ADD: case OP_ATTACH: added.add(op.fragment); - break; + break; case OP_REMOVE: case OP_DETACH: added.remove(op.fragment); @@ -844,846 +869,29 @@ final class BackStackRecord extends FragmentTransaction implements } } - private static void setFirstOut(SparseArray<FragmentContainerTransition> transitioningFragments, - Fragment fragment, boolean isPop) { - if (fragment != null) { - int containerId = fragment.mContainerId; - if (containerId != 0 && !fragment.isHidden()) { - FragmentContainerTransition fragments = transitioningFragments.get(containerId); - if (fragment.isAdded() && fragment.getView() != null && (fragments == null || - fragments.firstOut == null)) { - if (fragments == null) { - fragments = new FragmentContainerTransition(); - transitioningFragments.put(containerId, fragments); - } - fragments.firstOut = fragment; - fragments.firstOutIsPop = isPop; - } - if (fragments != null && fragments.lastIn == fragment) { - fragments.lastIn = null; - } - } - } - } - - private void setLastIn(SparseArray<FragmentContainerTransition> transitioningFragments, - Fragment fragment, boolean isPop) { - if (fragment != null) { - int containerId = fragment.mContainerId; - if (containerId != 0) { - FragmentContainerTransition fragments = transitioningFragments.get(containerId); - if (!fragment.isAdded()) { - if (fragments == null) { - fragments = new FragmentContainerTransition(); - transitioningFragments.put(containerId, fragments); - } - fragments.lastIn = fragment; - fragments.lastInIsPop = isPop; - } - if (fragments != null && fragments.firstOut == fragment) { - fragments.firstOut = null; - } - } - - /** - * Ensure that fragments that are entering are at least at the CREATED state - * so that they may load Transitions using TransitionInflater. - */ - if (fragment.mState < Fragment.CREATED && mManager.mCurState >= Fragment.CREATED && - mManager.mHost.getContext().getApplicationInfo().targetSdkVersion >= - Build.VERSION_CODES.N && !mAllowOptimization) { - mManager.makeActive(fragment); - mManager.moveToState(fragment, Fragment.CREATED, 0, 0, false); - } - } - } - - /** - * 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 void calculateFragments( - SparseArray<FragmentContainerTransition> transitioningFragments) { - if (!mManager.mContainer.onHasView()) { - return; // nothing to see, so no transitions - } - final int numOps = mOps.size(); - for (int opNum = 0; opNum < numOps; opNum++) { - final Op op = mOps.get(opNum); - switch (op.cmd) { - case OP_ADD: - case OP_SHOW: - case OP_ATTACH: - setLastIn(transitioningFragments, op.fragment, false); - break; - case OP_REMOVE: - case OP_HIDE: - case OP_DETACH: - setFirstOut(transitioningFragments, op.fragment, false); - break; - } - } - } - - /** - * 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 void calculatePopFragments( - SparseArray<FragmentContainerTransition> transitioningFragments) { - if (!mManager.mContainer.onHasView()) { - return; // nothing to see, so no transitions - } - final int numOps = mOps.size(); - for (int opNum = numOps - 1; opNum >= 0; opNum--) { + boolean isPostponed() { + for (int opNum = 0; opNum < mOps.size(); opNum++) { final Op op = mOps.get(opNum); - switch (op.cmd) { - case OP_ADD: - case OP_SHOW: - case OP_ATTACH: - setFirstOut(transitioningFragments, op.fragment, true); - break; - case OP_REMOVE: - case OP_HIDE: - case OP_DETACH: - setLastIn(transitioningFragments, op.fragment, true); - break; - } - } - } - - /** - * When custom fragment transitions are used, this sets up the state for each transition - * and begins the transition. A different transition is started for each fragment container - * and consists of up to 3 different transitions: the exit transition, a shared element - * transition and an enter transition. - * - * <p>The exit transition operates against the leaf nodes of the first fragment - * with a view that was removed. If no such fragment was removed, then no exit - * transition is executed. The exit transition comes from the outgoing fragment.</p> - * - * <p>The enter transition operates against the last fragment that was added. If - * that fragment does not have a view or no fragment was added, then no enter - * transition is executed. The enter transition comes from the incoming fragment.</p> - * - * <p>The shared element transition operates against all views and comes either - * from the outgoing fragment or the incoming fragment, depending on whether this - * is going forward or popping the back stack. When going forward, the incoming - * fragment's enter shared element transition is used, but when going back, the - * outgoing fragment's return shared element transition is used. Shared element - * transitions only operate if there is both an incoming and outgoing fragment.</p> - * - * @param containers The first in and last out fragments that are transitioning. - * @return The TransitionState used to complete the operation of the transition - * in {@link #setNameOverrides(android.app.BackStackRecord.TransitionState, java.util.ArrayList, - * java.util.ArrayList)}. - */ - TransitionState beginTransition(SparseArray<FragmentContainerTransition> containers) { - TransitionState state = new TransitionState(); - - // Adding a non-existent target view makes sure that the transitions don't target - // any views by default. They'll only target the views we tell add. If we don't - // add any, then no views will be targeted. - state.nonExistentView = new View(mManager.mHost.getContext()); - - final int numContainers = containers.size(); - for (int i = 0; i < numContainers; i++) { - int containerId = containers.keyAt(i); - FragmentContainerTransition containerTransition = containers.valueAt(i); - configureTransitions(containerId, state, containerTransition); - } - return state; - } - - private static Transition cloneTransition(Transition transition) { - if (transition != null) { - transition = transition.clone(); - } - return transition; - } - - private static Transition getEnterTransition(Fragment inFragment, boolean isPop) { - if (inFragment == null) { - return null; - } - return cloneTransition(isPop ? inFragment.getReenterTransition() : - inFragment.getEnterTransition()); - } - - private static Transition getExitTransition(Fragment outFragment, boolean isPop) { - if (outFragment == null) { - return null; - } - return cloneTransition(isPop ? outFragment.getReturnTransition() : - outFragment.getExitTransition()); - } - - 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; - } - - private static ArrayList<View> captureExitingViews(Transition exitTransition, - Fragment outFragment, ArrayMap<String, View> namedViews, View nonExistentView) { - ArrayList<View> viewList = null; - if (exitTransition != null) { - viewList = new ArrayList<View>(); - View root = outFragment.getView(); - root.captureTransitioningViews(viewList); - if (namedViews != null) { - viewList.removeAll(namedViews.values()); - } - if (!viewList.isEmpty()) { - viewList.add(nonExistentView); - addTargets(exitTransition, viewList); - } - } - return viewList; - } - - private ArrayMap<String, View> remapSharedElements(TransitionState state, Fragment outFragment, - boolean isPop) { - ArrayMap<String, View> namedViews = new ArrayMap<String, View>(); - if (mSharedElementSourceNames != null) { - outFragment.getView().findNamedViews(namedViews); - if (isPop) { - namedViews.retainAll(mSharedElementTargetNames); - } else { - namedViews = remapNames(mSharedElementSourceNames, mSharedElementTargetNames, - namedViews); - } - } - - if (isPop) { - outFragment.mEnterTransitionCallback.onMapSharedElements( - mSharedElementTargetNames, namedViews); - setBackNameOverrides(state, namedViews, false); - } else { - outFragment.mExitTransitionCallback.onMapSharedElements( - mSharedElementTargetNames, namedViews); - setNameOverrides(state, namedViews, false); - } - - return namedViews; - } - - /** - * Prepares the enter transition by adding a non-existent view to the transition's target list - * and setting it epicenter callback. By adding a non-existent view to the target list, - * we can prevent any view from being targeted at the beginning of the transition. - * We will add to the views before the end state of the transition is captured so that the - * views will appear. At the start of the transition, we clear the list of targets so that - * we can restore the state of the transition and use it again. - * - * <p>The shared element transition maps its shared elements immediately prior to - * capturing the final state of the Transition.</p> - */ - private ArrayList<View> addTransitionTargets(final TransitionState state, - final Transition enterTransition, final TransitionSet sharedElementTransition, - final Transition exitTransition, final Transition overallTransition, - final View container, final Fragment inFragment, final Fragment outFragment, - final ArrayList<View> hiddenFragmentViews, final boolean isPop, - final ArrayList<View> sharedElementTargets) { - if (enterTransition == null && sharedElementTransition == null && - overallTransition == null) { - return null; - } - final ArrayList<View> enteringViews = new ArrayList<View>(); - container.getViewTreeObserver().addOnPreDrawListener( - new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - container.getViewTreeObserver().removeOnPreDrawListener(this); - - // Don't include any newly-hidden fragments in the transition. - if (inFragment != null) { - excludeHiddenFragments(hiddenFragmentViews, inFragment.mContainerId, - overallTransition); - } - - ArrayMap<String, View> namedViews = null; - if (sharedElementTransition != null) { - namedViews = mapSharedElementsIn(state, isPop, inFragment); - removeTargets(sharedElementTransition, sharedElementTargets); - // keep the nonExistentView as excluded so the list doesn't get emptied - sharedElementTargets.remove(state.nonExistentView); - excludeViews(exitTransition, sharedElementTransition, - sharedElementTargets, false); - excludeViews(enterTransition, sharedElementTransition, - sharedElementTargets, false); - - setSharedElementTargets(sharedElementTransition, - state.nonExistentView, namedViews, sharedElementTargets); - - setEpicenterIn(namedViews, state); - - callSharedElementEnd(state, inFragment, outFragment, isPop, - namedViews); - } - - if (enterTransition != null) { - enterTransition.removeTarget(state.nonExistentView); - View view = inFragment.getView(); - if (view != null) { - view.captureTransitioningViews(enteringViews); - if (namedViews != null) { - enteringViews.removeAll(namedViews.values()); - } - enteringViews.add(state.nonExistentView); - // We added this earlier to prevent any views being targeted. - addTargets(enterTransition, enteringViews); - } - setSharedElementEpicenter(enterTransition, state); - } - - excludeViews(exitTransition, enterTransition, enteringViews, true); - excludeViews(exitTransition, sharedElementTransition, sharedElementTargets, - true); - excludeViews(enterTransition, sharedElementTransition, sharedElementTargets, - true); - return true; - } - }); - return enteringViews; - } - - private void callSharedElementEnd(TransitionState state, Fragment inFragment, - Fragment outFragment, boolean isPop, ArrayMap<String, View> namedViews) { - SharedElementCallback sharedElementCallback = isPop ? - outFragment.mEnterTransitionCallback : - inFragment.mEnterTransitionCallback; - ArrayList<String> names = new ArrayList<String>(namedViews.keySet()); - ArrayList<View> views = new ArrayList<View>(namedViews.values()); - sharedElementCallback.onSharedElementEnd(names, views, null); - } - - private void setEpicenterIn(ArrayMap<String, View> namedViews, TransitionState state) { - if (mSharedElementTargetNames != null && !namedViews.isEmpty()) { - // now we know the epicenter of the entering transition. - View epicenter = namedViews - .get(mSharedElementTargetNames.get(0)); - if (epicenter != null) { - state.enteringEpicenterView = epicenter; - } - } - } - - private ArrayMap<String, View> mapSharedElementsIn(TransitionState state, - boolean isPop, Fragment inFragment) { - // Now map the shared elements in the incoming fragment - ArrayMap<String, View> namedViews = mapEnteringSharedElements(state, inFragment, isPop); - - // remap shared elements and set the name mapping used - // in the shared element transition. - if (isPop) { - inFragment.mExitTransitionCallback.onMapSharedElements( - mSharedElementTargetNames, namedViews); - setBackNameOverrides(state, namedViews, true); - } else { - inFragment.mEnterTransitionCallback.onMapSharedElements( - mSharedElementTargetNames, namedViews); - setNameOverrides(state, namedViews, true); - } - return namedViews; - } - - 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; - } - - /** - * Configures custom transitions for a specific fragment container. - * - * @param containerId The container ID of the fragments to configure the transition for. - * @param state The Transition State keeping track of the executing transitions. - * @param transitioningFragments The first out and last in fragments for the fragment container. - */ - private void configureTransitions(int containerId, TransitionState state, - FragmentContainerTransition transitioningFragments) { - ViewGroup sceneRoot = (ViewGroup) mManager.mContainer.onFindViewById(containerId); - if (sceneRoot != null) { - final Fragment inFragment = transitioningFragments.lastIn; - final Fragment outFragment = transitioningFragments.firstOut; - - Transition enterTransition = - getEnterTransition(inFragment, transitioningFragments.lastInIsPop); - TransitionSet sharedElementTransition = getSharedElementTransition(inFragment, - outFragment, transitioningFragments.lastInIsPop); - Transition exitTransition = - getExitTransition(outFragment, transitioningFragments.firstOutIsPop); - - if (enterTransition == null && sharedElementTransition == null && - exitTransition == null) { - return; // no transitions! - } - if (enterTransition != null) { - enterTransition.addTarget(state.nonExistentView); - } - ArrayMap<String, View> namedViews = null; - ArrayList<View> sharedElementTargets = new ArrayList<View>(); - if (sharedElementTransition != null) { - namedViews = remapSharedElements(state, outFragment, - transitioningFragments.firstOutIsPop); - setSharedElementTargets(sharedElementTransition, - state.nonExistentView, namedViews, sharedElementTargets); - - // Notify the start of the transition. - SharedElementCallback callback = transitioningFragments.lastInIsPop ? - outFragment.mEnterTransitionCallback : - inFragment.mEnterTransitionCallback; - ArrayList<String> names = new ArrayList<String>(namedViews.keySet()); - ArrayList<View> views = new ArrayList<View>(namedViews.values()); - callback.onSharedElementStart(names, views, null); - } - - ArrayList<View> exitingViews = captureExitingViews(exitTransition, outFragment, - namedViews, state.nonExistentView); - if (exitingViews == null || exitingViews.isEmpty()) { - exitTransition = null; - } - excludeViews(enterTransition, exitTransition, exitingViews, true); - excludeViews(enterTransition, sharedElementTransition, sharedElementTargets, true); - excludeViews(exitTransition, sharedElementTransition, sharedElementTargets, true); - - // Set the epicenter of the exit transition - if (mSharedElementTargetNames != null && namedViews != null) { - View epicenterView = namedViews.get(mSharedElementTargetNames.get(0)); - if (epicenterView != null) { - if (exitTransition != null) { - setEpicenter(exitTransition, epicenterView); - } - if (sharedElementTransition != null) { - setEpicenter(sharedElementTransition, epicenterView); - } - } - } - - Transition transition = mergeTransitions(enterTransition, exitTransition, - sharedElementTransition, inFragment, transitioningFragments.lastInIsPop); - - if (transition != null) { - ArrayList<View> hiddenFragments = new ArrayList<View>(); - ArrayList<View> enteringViews = addTransitionTargets(state, enterTransition, - sharedElementTransition, exitTransition, transition, sceneRoot, inFragment, - outFragment, hiddenFragments, transitioningFragments.lastInIsPop, - sharedElementTargets); - - transition.setNameOverrides(state.nameOverrides); - // We want to exclude hidden views later, so we need a non-null list in the - // transition now. - transition.excludeTarget(state.nonExistentView, true); - // Now exclude all currently hidden fragments. - excludeHiddenFragments(hiddenFragments, containerId, transition); - TransitionManager.beginDelayedTransition(sceneRoot, transition); - // Remove the view targeting after the transition starts - removeTargetedViewsFromTransitions(sceneRoot, state.nonExistentView, - enterTransition, enteringViews, exitTransition, exitingViews, - sharedElementTransition, sharedElementTargets, transition, - hiddenFragments); - } - } - } - - /** - * 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, ArrayMap<String, View> namedViews, - ArrayList<View> sharedElementTargets) { - sharedElementTargets.clear(); - sharedElementTargets.addAll(namedViews.values()); - - final List<View> views = transition.getTargets(); - views.clear(); - final int count = sharedElementTargets.size(); - for (int i = 0; i < count; i++) { - final View view = sharedElementTargets.get(i); - bfsAddViewChildren(views, view); - } - sharedElementTargets.add(nonExistentView); - addTargets(transition, sharedElementTargets); - } - - /** - * 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) { + if (isFragmentPostponed(op)) { return true; } } return false; } - private static void excludeViews(Transition transition, Transition fromTransition, - ArrayList<View> views, boolean exclude) { - if (transition != null) { - final int viewCount = fromTransition == null ? 0 : views.size(); - for (int i = 0; i < viewCount; i++) { - transition.excludeTarget(views.get(i), exclude); - } - } - } - - /** - * 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 void removeTargetedViewsFromTransitions( - final ViewGroup sceneRoot, final View nonExistingView, - final Transition enterTransition, final ArrayList<View> enteringViews, - final Transition exitTransition, final ArrayList<View> exitingViews, - final Transition sharedElementTransition, final ArrayList<View> sharedElementTargets, - final Transition overallTransition, final ArrayList<View> hiddenViews) { - if (overallTransition != null) { - sceneRoot.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { - @Override - public boolean onPreDraw() { - sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); - if (enterTransition != null) { - removeTargets(enterTransition, enteringViews); - excludeViews(enterTransition, exitTransition, exitingViews, false); - excludeViews(enterTransition, sharedElementTransition, sharedElementTargets, - false); - } - if (exitTransition != null) { - removeTargets(exitTransition, exitingViews); - excludeViews(exitTransition, enterTransition, enteringViews, false); - excludeViews(exitTransition, sharedElementTransition, sharedElementTargets, - false); - } - if (sharedElementTransition != null) { - removeTargets(sharedElementTransition, sharedElementTargets); - } - int numViews = hiddenViews.size(); - for (int i = 0; i < numViews; i++) { - overallTransition.excludeTarget(hiddenViews.get(i), false); - } - overallTransition.excludeTarget(nonExistingView, false); - return true; - } - }); - } - } - - /** - * This method removes the views from transitions that target ONLY those views. - * 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 removeTargets(Transition transition, ArrayList<View> views) { - if (transition instanceof TransitionSet) { - TransitionSet set = (TransitionSet) transition; - int numTransitions = set.getTransitionCount(); - for (int i = 0; i < numTransitions; i++) { - Transition child = set.getTransitionAt(i); - removeTargets(child, views); - } - } else if (!hasSimpleTarget(transition)) { - List<View> targets = transition.getTargets(); - if (targets != null && targets.size() == views.size() && - targets.containsAll(views)) { - // We have an exact match. We must have added these earlier in addTargets - for (int i = views.size() - 1; i >= 0; i--) { - transition.removeTarget(views.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 removeTargets call will remove them unexpectedly. - */ - public static void addTargets(Transition transition, ArrayList<View> views) { - 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)); - } - } - } - } - - private static boolean hasSimpleTarget(Transition transition) { - return !isNullOrEmpty(transition.getTargetIds()) || - !isNullOrEmpty(transition.getTargetNames()) || - !isNullOrEmpty(transition.getTargetTypes()); - } - - private static boolean isNullOrEmpty(List list) { - return list == null || list.isEmpty(); - } - - /** - * Remaps a name-to-View map, substituting different names for keys. - * - * @param inMap A list of keys found in the map, in the order in toGoInMap - * @param toGoInMap A list of keys to use for the new map, in the order of inMap - * @param namedViews The current mapping - * @return a new Map after it has been mapped with the new names as keys. - */ - private static ArrayMap<String, View> remapNames(ArrayList<String> inMap, - ArrayList<String> toGoInMap, ArrayMap<String, View> namedViews) { - ArrayMap<String, View> remappedViews = new ArrayMap<String, View>(); - if (!namedViews.isEmpty()) { - int numKeys = inMap.size(); - for (int i = 0; i < numKeys; i++) { - View view = namedViews.get(inMap.get(i)); - - if (view != null) { - remappedViews.put(toGoInMap.get(i), view); - } - } - } - return remappedViews; - } - - /** - * Maps shared elements to views in the entering fragment. - * - * @param state The transition State as returned from {@link #beginTransition( - * android.util.SparseArray, android.util.SparseArray, boolean)}. - * @param inFragment The last fragment to be added. - * @param isPop true if this is popping the back stack or false if this is a - * forward operation. - */ - private ArrayMap<String, View> mapEnteringSharedElements(TransitionState state, - Fragment inFragment, boolean isPop) { - ArrayMap<String, View> namedViews = new ArrayMap<String, View>(); - View root = inFragment.getView(); - if (root != null) { - if (mSharedElementSourceNames != null) { - root.findNamedViews(namedViews); - if (isPop) { - namedViews = remapNames(mSharedElementSourceNames, - mSharedElementTargetNames, namedViews); - } else { - namedViews.retainAll(mSharedElementTargetNames); - } - } - } - return namedViews; - } - - private void excludeHiddenFragments(final ArrayList<View> hiddenFragmentViews, int containerId, - Transition transition) { - if (mManager.mAdded != null) { - for (int i = 0; i < mManager.mAdded.size(); i++) { - Fragment fragment = mManager.mAdded.get(i); - if (fragment.mView != null && fragment.mContainer != null && - fragment.mContainerId == containerId) { - if (fragment.mHidden) { - if (!hiddenFragmentViews.contains(fragment.mView)) { - transition.excludeTarget(fragment.mView, true); - hiddenFragmentViews.add(fragment.mView); - } - } else { - transition.excludeTarget(fragment.mView, false); - hiddenFragmentViews.remove(fragment.mView); - } - } - } - } - } - - private static void setEpicenter(Transition transition, View view) { - final Rect epicenter = new Rect(); - view.getBoundsOnScreen(epicenter); - - transition.setEpicenterCallback(new Transition.EpicenterCallback() { - @Override - public Rect onGetEpicenter(Transition transition) { - return epicenter; - } - }); - } - - private void setSharedElementEpicenter(Transition transition, final TransitionState state) { - transition.setEpicenterCallback(new Transition.EpicenterCallback() { - private Rect mEpicenter; - - @Override - public Rect onGetEpicenter(Transition transition) { - if (mEpicenter == null && state.enteringEpicenterView != null) { - mEpicenter = new Rect(); - state.enteringEpicenterView.getBoundsOnScreen(mEpicenter); - } - return mEpicenter; - } - }); - } - - private static void setNameOverride(ArrayMap<String, String> overrides, - String source, String target) { - if (source != null && target != null && !source.equals(target)) { - for (int index = 0; index < overrides.size(); index++) { - if (source.equals(overrides.valueAt(index))) { - overrides.setValueAt(index, target); - return; - } - } - overrides.put(source, target); - } - } - - static void setNameOverrides(TransitionState state, ArrayList<String> sourceNames, - ArrayList<String> targetNames) { - if (sourceNames != null && targetNames != null) { - for (int i = 0; i < sourceNames.size(); i++) { - String source = sourceNames.get(i); - String target = targetNames.get(i); - setNameOverride(state.nameOverrides, source, target); + void setOnStartPostponedListener(Fragment.OnStartEnterTransitionListener listener) { + for (int opNum = 0; opNum < mOps.size(); opNum++) { + final Op op = mOps.get(opNum); + if (isFragmentPostponed(op)) { + op.fragment.setOnStartEnterTransitionListener(listener); } } } - private void setBackNameOverrides(TransitionState state, ArrayMap<String, View> namedViews, - boolean isEnd) { - int targetCount = mSharedElementTargetNames == null ? 0 : mSharedElementTargetNames.size(); - int sourceCount = mSharedElementSourceNames == null ? 0 : mSharedElementSourceNames.size(); - final int count = Math.min(targetCount, sourceCount); - for (int i = 0; i < count; i++) { - String source = mSharedElementSourceNames.get(i); - String originalTarget = mSharedElementTargetNames.get(i); - View view = namedViews.get(originalTarget); - if (view != null) { - String target = view.getTransitionName(); - if (isEnd) { - setNameOverride(state.nameOverrides, source, target); - } else { - setNameOverride(state.nameOverrides, target, source); - } - } - } - } - - private void setNameOverrides(TransitionState state, ArrayMap<String, View> namedViews, - boolean isEnd) { - int count = namedViews == null ? 0 : namedViews.size(); - for (int i = 0; i < count; i++) { - String source = namedViews.keyAt(i); - String target = namedViews.valueAt(i).getTransitionName(); - if (isEnd) { - setNameOverride(state.nameOverrides, source, target); - } else { - setNameOverride(state.nameOverrides, target, source); - } - } + private static boolean isFragmentPostponed(Op op) { + final Fragment fragment = op.fragment; + return (fragment.mAdded && fragment.mView != null && !fragment.mDetached && + !fragment.mHidden && fragment.isPostponed()); } public String getName() { @@ -1701,36 +909,4 @@ final class BackStackRecord extends FragmentTransaction implements public boolean isEmpty() { return mOps.isEmpty(); } - - public class TransitionState { - public ArrayMap<String, String> nameOverrides = new ArrayMap<String, String>(); - public View enteringEpicenterView; - public View nonExistentView; - } - - /** - * 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 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; - } } diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java index b72b96026af4..5d1cd3ba8a9c 100644 --- a/core/java/android/app/Fragment.java +++ b/core/java/android/app/Fragment.java @@ -31,6 +31,8 @@ import android.content.res.TypedArray; import android.os.Build; import android.os.Build.VERSION_CODES; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.transition.Transition; @@ -375,15 +377,6 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene int mState = INITIALIZING; - // Non-null if the fragment's view hierarchy is currently animating away, - // meaning we need to wait a bit on completely destroying it. This is the - // animation that is running. - Animator mAnimatingAway; - - // If mAnimatingAway != null, this is the state we should move to once the - // animation is done. - int mStateAfterAnimating; - // When instantiated from saved state, this is the saved state. Bundle mSavedFragmentState; SparseArray<Parcelable> mSavedViewState; @@ -478,15 +471,6 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene // Used to verify that subclasses call through to super class. boolean mCalled; - // If app has requested a specific animation, this is the one to use. - int mNextAnim; - - // If app has requested a specific transition, this is the one to use. - int mNextTransition; - - // If app has requested a specific transition style, this is the one to use. - int mNextTransitionStyle; - // The parent container of the fragment after dynamically added to UI. ViewGroup mContainer; @@ -504,17 +488,15 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene boolean mLoadersStarted; boolean mCheckedForLoaderManager; - private Transition mEnterTransition = null; - private Transition mReturnTransition = USE_DEFAULT_TRANSITION; - private Transition mExitTransition = null; - private Transition mReenterTransition = USE_DEFAULT_TRANSITION; - private Transition mSharedElementEnterTransition = null; - private Transition mSharedElementReturnTransition = USE_DEFAULT_TRANSITION; - private Boolean mAllowReturnTransitionOverlap; - private Boolean mAllowEnterTransitionOverlap; + // The animation and transition information for the fragment. This will be null + // unless the elements are explicitly accessed and should remain null for Fragments + // without Views. + AnimationInfo mAnimationInfo; - SharedElementCallback mEnterTransitionCallback = SharedElementCallback.NULL_CALLBACK; - SharedElementCallback mExitTransitionCallback = SharedElementCallback.NULL_CALLBACK; + // True if the View was added, and its animation has yet to be run. This could + // also indicate that the fragment view hasn't been made visible, even if there is no + // animation for this fragment. + boolean mIsNewlyAdded; // True if mHidden has been changed and the animation should be scheduled. boolean mHiddenChanged; @@ -1399,26 +1381,41 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Fragment); - mEnterTransition = loadTransition(context, a, mEnterTransition, null, - com.android.internal.R.styleable.Fragment_fragmentEnterTransition); - mReturnTransition = loadTransition(context, a, mReturnTransition, USE_DEFAULT_TRANSITION, - com.android.internal.R.styleable.Fragment_fragmentReturnTransition); - mExitTransition = loadTransition(context, a, mExitTransition, null, - com.android.internal.R.styleable.Fragment_fragmentExitTransition); - mReenterTransition = loadTransition(context, a, mReenterTransition, USE_DEFAULT_TRANSITION, - com.android.internal.R.styleable.Fragment_fragmentReenterTransition); - mSharedElementEnterTransition = loadTransition(context, a, mSharedElementEnterTransition, - null, com.android.internal.R.styleable.Fragment_fragmentSharedElementEnterTransition); - mSharedElementReturnTransition = loadTransition(context, a, mSharedElementReturnTransition, + setEnterTransition(loadTransition(context, a, getEnterTransition(), null, + com.android.internal.R.styleable.Fragment_fragmentEnterTransition)); + setReturnTransition(loadTransition(context, a, getReturnTransition(), + USE_DEFAULT_TRANSITION, + com.android.internal.R.styleable.Fragment_fragmentReturnTransition)); + setExitTransition(loadTransition(context, a, getExitTransition(), null, + com.android.internal.R.styleable.Fragment_fragmentExitTransition)); + + setReenterTransition(loadTransition(context, a, getReenterTransition(), USE_DEFAULT_TRANSITION, - com.android.internal.R.styleable.Fragment_fragmentSharedElementReturnTransition); - if (mAllowEnterTransitionOverlap == null) { - mAllowEnterTransitionOverlap = a.getBoolean( - com.android.internal.R.styleable.Fragment_fragmentAllowEnterTransitionOverlap, true); + com.android.internal.R.styleable.Fragment_fragmentReenterTransition)); + setSharedElementEnterTransition(loadTransition(context, a, + getSharedElementEnterTransition(), null, + com.android.internal.R.styleable.Fragment_fragmentSharedElementEnterTransition)); + setSharedElementReturnTransition(loadTransition(context, a, + getSharedElementReturnTransition(), USE_DEFAULT_TRANSITION, + com.android.internal.R.styleable.Fragment_fragmentSharedElementReturnTransition)); + boolean isEnterSet; + boolean isReturnSet; + if (mAnimationInfo == null) { + isEnterSet = false; + isReturnSet = false; + } else { + isEnterSet = mAnimationInfo.mAllowEnterTransitionOverlap != null; + isReturnSet = mAnimationInfo.mAllowReturnTransitionOverlap != null; } - if (mAllowReturnTransitionOverlap == null) { - mAllowReturnTransitionOverlap = a.getBoolean( - com.android.internal.R.styleable.Fragment_fragmentAllowReturnTransitionOverlap, true); + if (!isEnterSet) { + setAllowEnterTransitionOverlap(a.getBoolean( + com.android.internal.R.styleable.Fragment_fragmentAllowEnterTransitionOverlap, + true)); + } + if (!isReturnSet) { + setAllowReturnTransitionOverlap(a.getBoolean( + com.android.internal.R.styleable.Fragment_fragmentAllowReturnTransitionOverlap, + true)); } a.recycle(); @@ -1943,16 +1940,12 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene */ public void setEnterSharedElementCallback(SharedElementCallback callback) { if (callback == null) { + if (mAnimationInfo == null) { + return; // already a null callback + } callback = SharedElementCallback.NULL_CALLBACK; } - mEnterTransitionCallback = callback; - } - - /** - * @hide - */ - public void setEnterSharedElementTransitionCallback(SharedElementCallback callback) { - setEnterSharedElementCallback(callback); + ensureAnimationInfo().mEnterTransitionCallback = callback; } /** @@ -1964,16 +1957,12 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene */ public void setExitSharedElementCallback(SharedElementCallback callback) { if (callback == null) { + if (mAnimationInfo == null) { + return; // already a null callback + } callback = SharedElementCallback.NULL_CALLBACK; } - mExitTransitionCallback = callback; - } - - /** - * @hide - */ - public void setExitSharedElementTransitionCallback(SharedElementCallback callback) { - setExitSharedElementCallback(callback); + ensureAnimationInfo().mExitTransitionCallback = callback; } /** @@ -1988,7 +1977,9 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentEnterTransition */ public void setEnterTransition(Transition transition) { - mEnterTransition = transition; + if (shouldChangeTransition(transition, null)) { + ensureAnimationInfo().mEnterTransition = transition; + } } /** @@ -2002,7 +1993,10 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentEnterTransition */ public Transition getEnterTransition() { - return mEnterTransition; + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mEnterTransition; } /** @@ -2020,7 +2014,9 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentExitTransition */ public void setReturnTransition(Transition transition) { - mReturnTransition = transition; + if (shouldChangeTransition(transition, USE_DEFAULT_TRANSITION)) { + ensureAnimationInfo().mReturnTransition = transition; + } } /** @@ -2037,8 +2033,11 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentExitTransition */ public Transition getReturnTransition() { - return mReturnTransition == USE_DEFAULT_TRANSITION ? getEnterTransition() - : mReturnTransition; + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mReturnTransition == USE_DEFAULT_TRANSITION ? getEnterTransition() + : mAnimationInfo.mReturnTransition; } /** @@ -2055,7 +2054,9 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentExitTransition */ public void setExitTransition(Transition transition) { - mExitTransition = transition; + if (shouldChangeTransition(transition, null)) { + ensureAnimationInfo().mExitTransition = transition; + } } /** @@ -2072,7 +2073,10 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentExitTransition */ public Transition getExitTransition() { - return mExitTransition; + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mExitTransition; } /** @@ -2089,7 +2093,9 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentReenterTransition */ public void setReenterTransition(Transition transition) { - mReenterTransition = transition; + if (shouldChangeTransition(transition, USE_DEFAULT_TRANSITION)) { + ensureAnimationInfo().mReenterTransition = transition; + } } /** @@ -2106,8 +2112,11 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentReenterTransition */ public Transition getReenterTransition() { - return mReenterTransition == USE_DEFAULT_TRANSITION ? getExitTransition() - : mReenterTransition; + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mReenterTransition == USE_DEFAULT_TRANSITION ? getExitTransition() + : mAnimationInfo.mReenterTransition; } /** @@ -2121,7 +2130,9 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentSharedElementEnterTransition */ public void setSharedElementEnterTransition(Transition transition) { - mSharedElementEnterTransition = transition; + if (shouldChangeTransition(transition, null)) { + ensureAnimationInfo().mSharedElementEnterTransition = transition; + } } /** @@ -2135,7 +2146,10 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentSharedElementEnterTransition */ public Transition getSharedElementEnterTransition() { - return mSharedElementEnterTransition; + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mSharedElementEnterTransition; } /** @@ -2152,7 +2166,9 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentSharedElementReturnTransition */ public void setSharedElementReturnTransition(Transition transition) { - mSharedElementReturnTransition = transition; + if (shouldChangeTransition(transition, USE_DEFAULT_TRANSITION)) { + ensureAnimationInfo().mSharedElementReturnTransition = transition; + } } /** @@ -2169,8 +2185,12 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentSharedElementReturnTransition */ public Transition getSharedElementReturnTransition() { - return mSharedElementReturnTransition == USE_DEFAULT_TRANSITION ? - getSharedElementEnterTransition() : mSharedElementReturnTransition; + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mSharedElementReturnTransition == USE_DEFAULT_TRANSITION + ? getSharedElementEnterTransition() + : mAnimationInfo.mSharedElementReturnTransition; } /** @@ -2183,7 +2203,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentAllowEnterTransitionOverlap */ public void setAllowEnterTransitionOverlap(boolean allow) { - mAllowEnterTransitionOverlap = allow; + ensureAnimationInfo().mAllowEnterTransitionOverlap = allow; } /** @@ -2196,7 +2216,8 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentAllowEnterTransitionOverlap */ public boolean getAllowEnterTransitionOverlap() { - return (mAllowEnterTransitionOverlap == null) ? true : mAllowEnterTransitionOverlap; + return (mAnimationInfo == null || mAnimationInfo.mAllowEnterTransitionOverlap == null) + ? true : mAnimationInfo.mAllowEnterTransitionOverlap; } /** @@ -2209,7 +2230,7 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentAllowReturnTransitionOverlap */ public void setAllowReturnTransitionOverlap(boolean allow) { - mAllowReturnTransitionOverlap = allow; + ensureAnimationInfo().mAllowReturnTransitionOverlap = allow; } /** @@ -2222,7 +2243,90 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene * @attr ref android.R.styleable#Fragment_fragmentAllowReturnTransitionOverlap */ public boolean getAllowReturnTransitionOverlap() { - return (mAllowReturnTransitionOverlap == null) ? true : mAllowReturnTransitionOverlap; + return (mAnimationInfo == null || mAnimationInfo.mAllowReturnTransitionOverlap == null) + ? true : mAnimationInfo.mAllowReturnTransitionOverlap; + } + + /** + * Postpone the entering Fragment transition until {@link #startPostponedEnterTransition()} + * or {@link FragmentManager#executePendingTransactions()} has been called. + * <p> + * This method gives the Fragment the ability to delay Fragment animations + * until all data is loaded. Until then, the added, shown, and + * attached Fragments will be INVISIBLE and removed, hidden, and detached Fragments won't + * be have their Views removed. The transaction runs when all postponed added Fragments in the + * transaction have called {@link #startPostponedEnterTransition()}. + * <p> + * This method should be called before being added to the FragmentTransaction or + * in {@link #onCreate(Bundle), {@link #onAttach(Context)}, or + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}}. + * {@link #startPostponedEnterTransition()} must be called to allow the Fragment to + * start the transitions. + * <p> + * When a FragmentTransaction is started that may affect a postponed FragmentTransaction, + * based on which containers are in their operations, the postponed FragmentTransaction + * will have its start triggered. The early triggering may result in faulty or nonexistent + * animations in the postponed transaction. FragmentTransactions that operate only on + * independent containers will not interfere with each other's postponement. + * <p> + * Calling postponeEnterTransition on Fragments with a null View will not postpone the + * transition. Likewise, postponement only works if FragmentTransaction optimizations are + * enabled. + * + * @see Activity#postponeEnterTransition() + * @see FragmentTransaction#setAllowOptimization(boolean) + */ + public void postponeEnterTransition() { + ensureAnimationInfo().mEnterTransitionPostponed = true; + } + + /** + * Begin postponed transitions after {@link #postponeEnterTransition()} was called. + * If postponeEnterTransition() was called, you must call startPostponedEnterTransition() + * or {@link FragmentManager#executePendingTransactions()} to complete the FragmentTransaction. + * If postponement was interrupted with {@link FragmentManager#executePendingTransactions()}, + * before {@code startPostponedEnterTransition()}, animations may not run or may execute + * improperly. + * + * @see Activity#startPostponedEnterTransition() + */ + public void startPostponedEnterTransition() { + if (mFragmentManager == null || mFragmentManager.mHost == null) { + ensureAnimationInfo().mEnterTransitionPostponed = false; + } else if (Looper.myLooper() != mFragmentManager.mHost.getHandler().getLooper()) { + mFragmentManager.mHost.getHandler(). + postAtFrontOfQueue(this::callStartTransitionListener); + } else { + callStartTransitionListener(); + } + } + + /** + * Calls the start transition listener. This must be called on the UI thread. + */ + private void callStartTransitionListener() { + final OnStartEnterTransitionListener listener; + if (mAnimationInfo == null) { + listener = null; + } else { + mAnimationInfo.mEnterTransitionPostponed = false; + listener = mAnimationInfo.mStartEnterTransitionListener; + mAnimationInfo.mStartEnterTransitionListener = null; + } + if (listener != null) { + listener.onStartEnterTransition(); + } + } + + /** + * Returns true if mAnimationInfo is not null or the transition differs from the default value. + * This is broken out to ensure mAnimationInfo is properly locked when checking. + */ + private boolean shouldChangeTransition(Transition transition, Transition defaultValue) { + if (transition == defaultValue) { + return mAnimationInfo != null; + } + return true; } /** @@ -2283,8 +2387,8 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene writer.print(" mTargetRequestCode="); writer.println(mTargetRequestCode); } - if (mNextAnim != 0) { - writer.print(prefix); writer.print("mNextAnim="); writer.println(mNextAnim); + if (getNextAnim() != 0) { + writer.print(prefix); writer.print("mNextAnim="); writer.println(getNextAnim()); } if (mContainer != null) { writer.print(prefix); writer.print("mContainer="); writer.println(mContainer); @@ -2292,10 +2396,11 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene if (mView != null) { writer.print(prefix); writer.print("mView="); writer.println(mView); } - if (mAnimatingAway != null) { - writer.print(prefix); writer.print("mAnimatingAway="); writer.println(mAnimatingAway); + if (getAnimatingAway() != null) { + writer.print(prefix); writer.print("mAnimatingAway="); + writer.println(getAnimatingAway()); writer.print(prefix); writer.print("mStateAfterAnimating="); - writer.println(mStateAfterAnimating); + writer.println(getStateAfterAnimating()); } if (mLoaderManager != null) { writer.print(prefix); writer.println("Loader Manager:"); @@ -2622,6 +2727,23 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene } } + void setOnStartEnterTransitionListener(OnStartEnterTransitionListener listener) { + ensureAnimationInfo(); + if (listener == mAnimationInfo.mStartEnterTransitionListener) { + return; + } + if (listener != null && mAnimationInfo.mStartEnterTransitionListener != null) { + throw new IllegalStateException("Trying to set a replacement " + + "startPostponedEnterTransition on " + this); + } + if (mAnimationInfo.mEnterTransitionPostponed) { + mAnimationInfo.mStartEnterTransitionListener = listener; + } + if (listener != null) { + listener.startListening(); + } + } + private static Transition loadTransition(Context context, TypedArray typedArray, Transition currentValue, Transition defaultValue, int id) { if (currentValue != defaultValue) { @@ -2640,4 +2762,147 @@ public class Fragment implements ComponentCallbacks2, OnCreateContextMenuListene return transition; } + private AnimationInfo ensureAnimationInfo() { + if (mAnimationInfo == null) { + mAnimationInfo = new AnimationInfo(); + } + return mAnimationInfo; + } + + int getNextAnim() { + if (mAnimationInfo == null) { + return 0; + } + return mAnimationInfo.mNextAnim; + } + + void setNextAnim(int animResourceId) { + if (mAnimationInfo == null && animResourceId == 0) { + return; // no change! + } + ensureAnimationInfo().mNextAnim = animResourceId; + } + + int getNextTransition() { + if (mAnimationInfo == null) { + return 0; + } + return mAnimationInfo.mNextTransition; + } + + void setNextTransition(int nextTransition, int nextTransitionStyle) { + if (mAnimationInfo == null && nextTransition == 0 && nextTransitionStyle == 0) { + return; // no change! + } + ensureAnimationInfo(); + mAnimationInfo.mNextTransition = nextTransition; + mAnimationInfo.mNextTransitionStyle = nextTransitionStyle; + } + + int getNextTransitionStyle() { + if (mAnimationInfo == null) { + return 0; + } + return mAnimationInfo.mNextTransitionStyle; + } + + SharedElementCallback getEnterTransitionCallback() { + if (mAnimationInfo == null) { + return SharedElementCallback.NULL_CALLBACK; + } + return mAnimationInfo.mEnterTransitionCallback; + } + + SharedElementCallback getExitTransitionCallback() { + if (mAnimationInfo == null) { + return SharedElementCallback.NULL_CALLBACK; + } + return mAnimationInfo.mExitTransitionCallback; + } + + Animator getAnimatingAway() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mAnimatingAway; + } + + void setAnimatingAway(Animator animator) { + ensureAnimationInfo().mAnimatingAway = animator; + } + + int getStateAfterAnimating() { + if (mAnimationInfo == null) { + return 0; + } + return mAnimationInfo.mStateAfterAnimating; + } + + void setStateAfterAnimating(int state) { + ensureAnimationInfo().mStateAfterAnimating = state; + } + + boolean isPostponed() { + if (mAnimationInfo == null) { + return false; + } + return mAnimationInfo.mEnterTransitionPostponed; + } + + /** + * Used internally to be notified when {@link #startPostponedEnterTransition()} has + * been called. This listener will only be called once and then be removed from the + * listeners. + */ + interface OnStartEnterTransitionListener { + void onStartEnterTransition(); + void startListening(); + } + + /** + * Contains all the animation and transition information for a fragment. This will only + * be instantiated for Fragments that have Views. + */ + static class AnimationInfo { + // Non-null if the fragment's view hierarchy is currently animating away, + // meaning we need to wait a bit on completely destroying it. This is the + // animation that is running. + Animator mAnimatingAway; + + // If mAnimatingAway != null, this is the state we should move to once the + // animation is done. + int mStateAfterAnimating; + + // If app has requested a specific animation, this is the one to use. + int mNextAnim; + + // If app has requested a specific transition, this is the one to use. + int mNextTransition; + + // If app has requested a specific transition style, this is the one to use. + int mNextTransitionStyle; + + private Transition mEnterTransition = null; + private Transition mReturnTransition = USE_DEFAULT_TRANSITION; + private Transition mExitTransition = null; + private Transition mReenterTransition = USE_DEFAULT_TRANSITION; + private Transition mSharedElementEnterTransition = null; + private Transition mSharedElementReturnTransition = USE_DEFAULT_TRANSITION; + private Boolean mAllowReturnTransitionOverlap; + private Boolean mAllowEnterTransitionOverlap; + + SharedElementCallback mEnterTransitionCallback = SharedElementCallback.NULL_CALLBACK; + SharedElementCallback mExitTransitionCallback = SharedElementCallback.NULL_CALLBACK; + + // True when postponeEnterTransition has been called and startPostponeEnterTransition + // hasn't been called yet. + boolean mEnterTransitionPostponed; + + // Listener to wait for startPostponeEnterTransition. After being called, it will + // be set to null + OnStartEnterTransitionListener mStartEnterTransitionListener; + + // True if the View was added, and its animation has yet to be run. + boolean mIsNewlyAdded; + } } diff --git a/core/java/android/app/FragmentHostCallback.java b/core/java/android/app/FragmentHostCallback.java index d869168e2b6a..7e415e9f2f4b 100644 --- a/core/java/android/app/FragmentHostCallback.java +++ b/core/java/android/app/FragmentHostCallback.java @@ -54,7 +54,8 @@ public abstract class FragmentHostCallback<E> extends FragmentContainer { private boolean mLoadersStarted; public FragmentHostCallback(Context context, Handler handler, int windowAnimations) { - this(null /*activity*/, context, handler, windowAnimations); + this((context instanceof Activity) ? (Activity)context : null, context, + chooseHandler(context, handler), windowAnimations); } FragmentHostCallback(Activity activity) { @@ -70,6 +71,19 @@ public abstract class FragmentHostCallback<E> extends FragmentContainer { } /** + * Used internally in {@link #FragmentHostCallback(Context, Handler, int)} to choose + * the Activity's handler or the provided handler. + */ + private static Handler chooseHandler(Context context, Handler handler) { + if (handler == null && context instanceof Activity) { + Activity activity = (Activity) context; + return activity.mHandler; + } else { + return handler; + } + } + + /** * Print internal state into the given stream. * * @param prefix Desired prefix to prepend at each line of output. diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java index b2df1acbadaf..9345a03bb2f2 100644 --- a/core/java/android/app/FragmentManager.java +++ b/core/java/android/app/FragmentManager.java @@ -31,7 +31,6 @@ import android.os.Debug; import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; -import android.transition.Transition; import android.util.AttributeSet; import android.util.DebugUtils; import android.util.Log; @@ -44,6 +43,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; + import com.android.internal.util.FastPrintWriter; import java.io.FileDescriptor; @@ -160,6 +160,9 @@ public abstract class FragmentManager { * can call this function (only from the main thread) to do so. Note that * all callbacks and other related behavior will be done from within this * call, so be careful about where this is called from. + * <p> + * This also forces the start of any postponed Transactions where + * {@link Fragment#postponeEnterTransition()} has been called. * * @return Returns true if there were any pending transactions to be * executed. @@ -206,7 +209,7 @@ public abstract class FragmentManager { /** * Like {@link #popBackStack()}, but performs the operation immediately * inside of the call. This is like calling {@link #executePendingTransactions()} - * afterwards. + * afterwards without forcing the start of postponed Transactions. * @return Returns true if there was something popped, else false. */ public abstract boolean popBackStackImmediate(); @@ -229,7 +232,7 @@ public abstract class FragmentManager { /** * Like {@link #popBackStack(String, int)}, but performs the operation immediately * inside of the call. This is like calling {@link #executePendingTransactions()} - * afterwards. + * afterwards without forcing the start of postponed Transactions. * @return Returns true if there was something popped, else false. */ public abstract boolean popBackStackImmediate(String name, int flags); @@ -253,7 +256,7 @@ public abstract class FragmentManager { /** * Like {@link #popBackStack(int, int)}, but performs the operation immediately * inside of the call. This is like calling {@link #executePendingTransactions()} - * afterwards. + * afterwards without forcing the start of postponed Transactions. * @return Returns true if there was something popped, else false. */ public abstract boolean popBackStackImmediate(int id, int flags); @@ -474,13 +477,15 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate // Temporary vars for optimizing execution of BackStackRecords: ArrayList<BackStackRecord> mTmpRecords; ArrayList<Boolean> mTmpIsPop; - SparseArray<BackStackRecord.FragmentContainerTransition> mTmpFragmentsContainerTransitions; ArrayList<Fragment> mTmpAddedFragments; // Temporary vars for state save and restore. Bundle mStateBundle = null; SparseArray<Parcelable> mStateArray = null; - + + // Postponed transactions. + ArrayList<StartEnterTransitionListener> mPostponedTransactions; + Runnable mExecCommit = new Runnable() { @Override public void run() { @@ -564,7 +569,9 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate @Override public boolean executePendingTransactions() { - return execPendingActions(); + boolean updates = execPendingActions(); + forcePostponedTransactions(); + return updates; } @Override @@ -614,7 +621,7 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate * @return true if the pop operation did anything or false otherwise. */ private boolean popBackStackImmediate(String name, int id, int flags) { - executePendingTransactions(); + execPendingActions(); ensureExecReady(true); boolean executePop = popBackStackState(mTmpRecords, mTmpIsPop, name, id, flags); @@ -831,14 +838,14 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate Animator loadAnimator(Fragment fragment, int transit, boolean enter, int transitionStyle) { - Animator animObj = fragment.onCreateAnimator(transit, enter, - fragment.mNextAnim); + Animator animObj = fragment.onCreateAnimator(transit, enter, fragment.getNextAnim()); if (animObj != null) { return animObj; } - if (fragment.mNextAnim != 0) { - Animator anim = AnimatorInflater.loadAnimator(mHost.getContext(), fragment.mNextAnim); + if (fragment.getNextAnim() != 0) { + Animator anim = AnimatorInflater.loadAnimator(mHost.getContext(), + fragment.getNextAnim()); if (anim != null) { return anim; } @@ -914,13 +921,13 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate if (f.mFromLayout && !f.mInLayout) { return; } - if (f.mAnimatingAway != null) { + if (f.getAnimatingAway() != null) { // The fragment is currently being animated... but! Now we // want to move our state back up. Give up on waiting for the // animation, move to whatever the final state should be once // the animation is done, and then we can proceed from there. - f.mAnimatingAway = null; - moveToState(f, f.mStateAfterAnimating, 0, 0, true); + f.setAnimatingAway(null); + moveToState(f, f.getStateAfterAnimating(), 0, 0, true); } switch (f.mState) { case Fragment.INITIALIZING: @@ -1011,16 +1018,13 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate if (f.mView != null) { f.mView.setSaveFromParentEnabled(false); if (container != null) { - Animator anim = loadAnimator(f, transit, true, - transitionStyle); - if (anim != null) { - anim.setTarget(f.mView); - setHWLayerAnimListenerIfAlpha(f.mView, anim); - anim.start(); - } container.addView(f.mView); + f.mIsNewlyAdded = true; + } + if (f.mHidden) { + f.mView.setVisibility(View.GONE); + f.mIsNewlyAdded = false; // No animation required } - if (f.mHidden) f.mView.setVisibility(View.GONE); f.onViewCreated(f.mView, f.mSavedFragmentState); } } @@ -1075,7 +1079,8 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate f.performDestroyView(); if (f.mView != null && f.mContainer != null) { Animator anim = null; - if (mCurState > Fragment.INITIALIZING && !mDestroyed) { + if (mCurState > Fragment.INITIALIZING && !mDestroyed && + f.mView.getVisibility() == View.VISIBLE) { anim = loadAnimator(f, transit, false, transitionStyle); } @@ -1084,15 +1089,15 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate final View view = f.mView; final Fragment fragment = f; container.startViewTransition(view); - f.mAnimatingAway = anim; - f.mStateAfterAnimating = newState; + f.setAnimatingAway(anim); + f.setStateAfterAnimating(newState); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator anim) { container.endViewTransition(view); - if (fragment.mAnimatingAway != null) { - fragment.mAnimatingAway = null; - moveToState(fragment, fragment.mStateAfterAnimating, + if (fragment.getAnimatingAway() != null) { + fragment.setAnimatingAway(null); + moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0, false); } } @@ -1110,24 +1115,24 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate case Fragment.CREATED: if (newState < Fragment.CREATED) { if (mDestroyed) { - if (f.mAnimatingAway != null) { + if (f.getAnimatingAway() != null) { // The fragment's containing activity is // being destroyed, but this fragment is // currently animating away. Stop the // animation right now -- it is not needed, // and we can't wait any more on destroying // the fragment. - Animator anim = f.mAnimatingAway; - f.mAnimatingAway = null; + Animator anim = f.getAnimatingAway(); + f.setAnimatingAway(null); anim.cancel(); } } - if (f.mAnimatingAway != null) { + if (f.getAnimatingAway() != null) { // We are waiting for the fragment's view to finish // animating away. Just make a note of the state // the fragment now should move to once the animation // is done. - f.mStateAfterAnimating = newState; + f.setStateAfterAnimating(newState); newState = Fragment.CREATED; } else { if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f); @@ -1175,8 +1180,8 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate */ void completeShowHideFragment(final Fragment fragment) { if (fragment.mView != null) { - Animator anim = loadAnimator(fragment, fragment.mNextTransition, !fragment.mHidden, - fragment.mNextTransitionStyle); + Animator anim = loadAnimator(fragment, fragment.getNextTransition(), !fragment.mHidden, + fragment.getNextTransitionStyle()); if (anim != null) { anim.setTarget(fragment.mView); if (fragment.mHidden) { @@ -1212,7 +1217,7 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate * * @param f The fragment to change. */ - void moveFragmentToExpectedState(Fragment f) { + void moveFragmentToExpectedState(final Fragment f) { if (f == null) { return; } @@ -1224,9 +1229,11 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate nextState = Fragment.INITIALIZING; } } - moveToState(f, nextState, f.mNextTransition, f.mNextTransitionStyle, false); + + moveToState(f, nextState, f.getNextTransition(), f.getNextTransitionStyle(), false); if (f.mView != null) { + // Move the view if it is out of order Fragment underFragment = findFragmentUnder(f); if (underFragment != null) { final View underView = underFragment.mView; @@ -1239,6 +1246,18 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate container.addView(f.mView, underIndex); } } + if (f.mIsNewlyAdded && f.mContainer != null) { + // Make it visible and run the animations + f.mView.setVisibility(View.VISIBLE); + f.mIsNewlyAdded = false; + // run animations: + Animator anim = loadAnimator(f, f.getNextTransition(), true, f.getNextTransitionStyle()); + if (anim != null) { + anim.setTarget(f.mView); + setHWLayerAnimListenerIfAlpha(f.mView, anim); + anim.start(); + } + } } if (f.mHiddenChanged) { completeShowHideFragment(f); @@ -1270,7 +1289,7 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate final int numActive = mActive.size(); for (int i = 0; i < numActive; i++) { Fragment f = mActive.get(i); - if (f != null && (f.mRemoving || f.mDetached)) { + if (f != null && (f.mRemoving || f.mDetached) && !f.mIsNewlyAdded) { moveFragmentToExpectedState(f); if (f.mLoaderManager != null) { loadersRunning |= f.mLoaderManager.hasRunningLoaders(); @@ -1288,7 +1307,7 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate } } } - + void startPendingDeferredFragments() { if (mActive == null) return; @@ -1539,7 +1558,22 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate mPendingActions = new ArrayList<>(); } mPendingActions.add(action); - if (mPendingActions.size() == 1) { + scheduleCommit(); + } + } + + /** + * Schedules the execution when one hasn't been scheduled already. This should happen + * the first time {@link #enqueueAction(OpGenerator, boolean)} is called or when + * a postponed transaction has been started with + * {@link Fragment#startPostponedEnterTransition()} + */ + private void scheduleCommit() { + synchronized (this) { + boolean postponeReady = + mPostponedTransactions != null && !mPostponedTransactions.isEmpty(); + boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1; + if (postponeReady || pendingReady) { mHost.getHandler().removeCallbacks(mExecCommit); mHost.getHandler().post(mExecCommit); } @@ -1625,6 +1659,7 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate mTmpRecords = new ArrayList<>(); mTmpIsPop = new ArrayList<>(); } + executePostponedTransaction(null, null); } public void execSingleAction(OpGenerator action, boolean allowStateLoss) { @@ -1674,6 +1709,40 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate } /** + * Complete the execution of transactions that have previously been postponed, but are + * now ready. + */ + private void executePostponedTransaction(ArrayList<BackStackRecord> records, + ArrayList<Boolean> isRecordPop) { + int numPostponed = mPostponedTransactions == null ? 0 : mPostponedTransactions.size(); + for (int i = 0; i < numPostponed; i++) { + StartEnterTransitionListener listener = mPostponedTransactions.get(i); + if (records != null && !listener.mIsBack) { + int index = records.indexOf(listener.mRecord); + if (index != -1 && isRecordPop.get(index)) { + listener.cancelTransaction(); + continue; + } + } + if (listener.isReady() || (records != null && + listener.mRecord.interactsWith(records, 0, records.size()))) { + mPostponedTransactions.remove(i); + i--; + numPostponed--; + int index; + if (records != null && !listener.mIsBack && + (index = records.indexOf(listener.mRecord)) != -1 && + isRecordPop.get(index)) { + // This is popping a postponed transaction + listener.cancelTransaction(); + } else { + listener.completeTransaction(); + } + } + } + } + + /** * Optimizes BackStackRecord operations. This method merges operations of proximate records * that allow optimization. See {@link FragmentTransaction#setAllowOptimization(boolean)}. * <p> @@ -1696,6 +1765,9 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate throw new IllegalStateException("Internal error with the back stack records"); } + // Force start of any postponed transactions that interact with scheduled transactions: + executePostponedTransaction(records, isRecordPop); + final int numRecords = records.size(); int startIndex = 0; for (int recordNum = 0; recordNum < numRecords; recordNum++) { @@ -1736,10 +1808,8 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate boolean addToBackStack = false; if (mTmpAddedFragments == null) { mTmpAddedFragments = new ArrayList<>(); - mTmpFragmentsContainerTransitions = new SparseArray<>(); } else { mTmpAddedFragments.clear(); - mTmpFragmentsContainerTransitions.clear(); } if (mAdded != null) { mTmpAddedFragments.addAll(mAdded); @@ -1753,25 +1823,26 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate final int bumpAmount = isPop ? -1 : 1; record.bumpBackStackNesting(bumpAmount); addToBackStack = addToBackStack || record.mAddToBackStack; - - if (mCurState >= Fragment.CREATED) { - if (isPop) { - record.calculatePopFragments(mTmpFragmentsContainerTransitions); - } else { - record.calculateFragments(mTmpFragmentsContainerTransitions); - } - } } mTmpAddedFragments.clear(); if (!allowOptimization) { - startTransitions(records, isRecordPop, startIndex, endIndex); + FragmentTransition.startTransitions(this, records, isRecordPop, startIndex, endIndex, + false); } executeOps(records, isRecordPop, startIndex, endIndex); + int postponeIndex = endIndex; if (allowOptimization) { - moveFragmentsToAtLeastCreated(); - startTransitions(records, isRecordPop, startIndex, endIndex); + moveFragmentsToInvisible(); + postponeIndex = postponePostponableTransactions(records, isRecordPop, + startIndex, endIndex); + } + + if (postponeIndex != startIndex && allowOptimization) { + // need to run something now + FragmentTransition.startTransitions(this, records, isRecordPop, startIndex, + postponeIndex, true); moveToState(mCurState); } @@ -1783,12 +1854,104 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate record.mIndex = -1; } } + if (addToBackStack) { reportBackStackChanged(); } } /** + * Examine all transactions and determine which ones are marked as postponed. Those will + * have their operations rolled back and moved to the end of the record list (up to endIndex). + * It will also add the postponed transaction to the queue. + * + * @param records A list of BackStackRecords that should be checked. + * @param isRecordPop The direction that these records are being run. + * @param startIndex The index of the first record in <code>records</code> to be checked + * @param endIndex One more than the final record index in <code>records</code> to be checked. + * @return The index of the first postponed transaction or endIndex if no transaction was + * postponed. + */ + private int postponePostponableTransactions(ArrayList<BackStackRecord> records, + ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) { + int postponeIndex = endIndex; + for (int i = endIndex - 1; i >= startIndex; i--) { + final BackStackRecord record = records.get(i); + final boolean isPop = isRecordPop.get(i); + boolean isPostponed = record.isPostponed() && + !record.interactsWith(records, i + 1, endIndex); + if (isPostponed) { + if (mPostponedTransactions == null) { + mPostponedTransactions = new ArrayList<>(); + } + StartEnterTransitionListener listener = + new StartEnterTransitionListener(record, isPop); + mPostponedTransactions.add(listener); + record.setOnStartPostponedListener(listener); + + // roll back the transaction + if (isPop) { + record.executeOps(); + } else { + record.executePopOps(); + } + + // move to the end + postponeIndex--; + if (i != postponeIndex) { + records.remove(i); + records.add(postponeIndex, record); + } + + // different views may be visible now + moveFragmentsToInvisible(); + } + } + return postponeIndex; + } + + /** + * When a postponed transaction is ready to be started, this completes the transaction, + * removing, hiding, or showing views as well as starting the animations and transitions. + * <p> + * {@code runtransitions} is set to false when the transaction postponement was interrupted + * abnormally -- normally by a new transaction being started that affects the postponed + * transaction. + * + * @param record The transaction to run + * @param isPop true if record is popping or false if it is adding + * @param runTransitions true if the fragment transition should be run or false otherwise. + * @param moveToState true if the state should be changed after executing the operations. + * This is false when the transaction is canceled when a postponed + * transaction is popped. + */ + private void completeExecute(BackStackRecord record, boolean isPop, boolean runTransitions, + boolean moveToState) { + ArrayList<BackStackRecord> records = new ArrayList<>(1); + ArrayList<Boolean> isRecordPop = new ArrayList<>(1); + records.add(record); + isRecordPop.add(isPop); + executeOps(records, isRecordPop, 0, 1); + if (runTransitions) { + FragmentTransition.startTransitions(this, records, isRecordPop, 0, 1, true); + } + if (moveToState) { + moveToState(mCurState); + } else if (mActive != null) { + final int numActive = mActive.size(); + for (int i = 0; i < numActive; i++) { + // Allow added fragments to be removed during the pop since we aren't going + // to move them to the final state with moveToState(mCurState). + Fragment fragment = mActive.get(i); + if (fragment.mView != null && fragment.mIsNewlyAdded && + record.interactsWith(fragment.mContainerId)) { + fragment.mIsNewlyAdded = false; + } + } + } + } + + /** * Find a fragment within the fragment's container whose View should be below the passed * fragment. {@code null} is returned when the fragment has no View or if there should be * no fragment with a View below the given fragment. @@ -1845,61 +2008,50 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate } /** - * Prepares the fragments for Transitions and starts it. If the FragmentManager is not - * at Fragment.CREATED, no transition will be run. - * - * This is explicitly for {@link Fragment#setEnterTransition(Transition)} and its siblings, - * not for {@link FragmentTransaction#setTransition(int)}. - * - * @param records The entries to examine for transitions. - * @param isRecordPop The direction that these records are being run. - * @param startIndex The index of the first entry in records to run transitions for. - * @param endIndex One past the index of the final entry in records to run transitions for. + * Ensure that fragments that are added are moved to at least the CREATED state. + * Any newly-added Views are made INVISIBLE so that the Transaction can be postponed + * with {@link Fragment#postponeEnterTransition()}. */ - private void startTransitions(ArrayList<BackStackRecord> records, - ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) { - if (mCurState >= Fragment.CREATED) { - if (mTmpFragmentsContainerTransitions.size() != 0) { - BackStackRecord record = records.get(0); - BackStackRecord.TransitionState state = - record.beginTransition(mTmpFragmentsContainerTransitions); - if (state != null) { - for (int i = startIndex + 1; i < endIndex - 1; i++) { - final BackStackRecord nameRecord = records.get(i); - final boolean isPop = isRecordPop.get(i); - ArrayList<String> sourceNames = isPop - ? nameRecord.mSharedElementTargetNames - : record.mSharedElementSourceNames; - ArrayList<String> targetNames = isPop - ? nameRecord.mSharedElementSourceNames - : record.mSharedElementTargetNames; - BackStackRecord.setNameOverrides(state, sourceNames, targetNames); - } + private void moveFragmentsToInvisible() { + if (mCurState < Fragment.CREATED) { + return; + } + // We want to leave the fragment in the started state + final int state = Math.min(mCurState, Fragment.STARTED); + final int numAdded = mAdded == null ? 0 : mAdded.size(); + for (int i = 0; i < numAdded; i++) { + Fragment fragment = mAdded.get(i); + if (fragment.mState < state) { + moveToState(fragment, state, fragment.getNextAnim(), fragment.getNextTransition(), false); + if (fragment.mView != null && !fragment.mHidden && fragment.mIsNewlyAdded) { + fragment.mView.setVisibility(View.INVISIBLE); } - mTmpFragmentsContainerTransitions.clear(); } } } /** - * Ensure that fragments that are added are at least at the CREATED state - * so that they may load Transitions using TransitionInflater. When the transaction - * cannot be optimized, this is executed in - * {@link BackStackRecord#setLastIn(SparseArray, SparseArray, Fragment)} instead. Prior to - * N, this wasn't supported, so no out-of-order creation can be done for compatibility. - * <p> - * This won't change the state of the fragment manager, nor will it change the fragment's - * state if the fragment manager isn't at least at the CREATED state. + * Starts all postponed transactions regardless of whether they are ready or not. */ - private void moveFragmentsToAtLeastCreated() { - if (mCurState < Fragment.CREATED) { - return; + private void forcePostponedTransactions() { + if (mPostponedTransactions != null) { + while (!mPostponedTransactions.isEmpty()) { + mPostponedTransactions.remove(0).completeTransaction(); + } } - final int numAdded = mAdded == null ? 0 : mAdded.size(); - for (int i = 0; i < numAdded; i++) { - Fragment fragment = mAdded.get(i); - if (fragment.mState < Fragment.CREATED) { - moveToState(fragment, Fragment.CREATED, 0, 0, false); + } + + /** + * Ends the animations of fragments so that they immediately reach the end state. + * This is used prior to saving the state so that the correct state is saved. + */ + private void endAnimatingAwayFragments() { + final int numFragments = mActive == null ? 0 : mActive.size(); + for (int i = 0; i < numFragments; i++) { + Fragment fragment = mActive.get(i); + if (fragment != null && fragment.getAnimatingAway() != null) { + // Give up waiting for the animation and just end it. + fragment.getAnimatingAway().end(); } } } @@ -2114,6 +2266,8 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate Parcelable saveAllState() { // Make sure all pending operations have now been executed to get // our state update-to-date. + forcePostponedTransactions(); + endAnimatingAwayFragments(); execPendingActions(); mStateSaved = true; @@ -2723,4 +2877,80 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate return popBackStackState(records, isRecordPop, mName, mId, mFlags); } } + + /** + * A listener for a postponed transaction. This waits until + * {@link Fragment#startPostponedEnterTransition()} is called or a transaction is started + * that interacts with this one, based on interactions with the fragment container. + */ + static class StartEnterTransitionListener + implements Fragment.OnStartEnterTransitionListener { + private final boolean mIsBack; + private final BackStackRecord mRecord; + private int mNumPostponed; + + public StartEnterTransitionListener(BackStackRecord record, boolean isBack) { + mIsBack = isBack; + mRecord = record; + } + + /** + * Called from {@link Fragment#startPostponedEnterTransition()}, this decreases the + * number of Fragments that are postponed. This may cause the transaction to schedule + * to finish running and run transitions and animations. + */ + @Override + public void onStartEnterTransition() { + mNumPostponed--; + if (mNumPostponed != 0) { + return; + } + mRecord.mManager.scheduleCommit(); + } + + /** + * Called from {@link Fragment# + * setOnStartEnterTransitionListener(Fragment.OnStartEnterTransitionListener)}, this + * increases the number of fragments that are postponed as part of this transaction. + */ + @Override + public void startListening() { + mNumPostponed++; + } + + /** + * @return true if there are no more postponed fragments as part of the transaction. + */ + public boolean isReady() { + return mNumPostponed == 0; + } + + /** + * Completes the transaction and start the animations and transitions. This may skip + * the transitions if this is called before all fragments have called + * {@link Fragment#startPostponedEnterTransition()}. + */ + public void completeTransaction() { + final boolean canceled; + canceled = mNumPostponed > 0; + FragmentManagerImpl manager = mRecord.mManager; + final int numAdded = manager.mAdded.size(); + for (int i = 0; i < numAdded; i++) { + final Fragment fragment = manager.mAdded.get(i); + fragment.setOnStartEnterTransitionListener(null); + if (canceled && fragment.isPostponed()) { + fragment.startPostponedEnterTransition(); + } + } + mRecord.mManager.completeExecute(mRecord, mIsBack, !canceled, true); + } + + /** + * Cancels this transaction instead of completing it. That means that the state isn't + * changed, so the pop results in no change to the state. + */ + public void cancelTransaction() { + mRecord.mManager.completeExecute(mRecord, mIsBack, false, false); + } + } } diff --git a/core/java/android/app/FragmentTransition.java b/core/java/android/app/FragmentTransition.java new file mode 100644 index 000000000000..6f5211468b13 --- /dev/null +++ b/core/java/android/app/FragmentTransition.java @@ -0,0 +1,1330 @@ +/* + * 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.TransitionManager; +import android.transition.TransitionSet; +import android.util.ArrayMap; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Contains the Fragment Transition functionality for both optimized and unoptimized + * Fragment Transactions. With optimized fragment transactions, all Views have been + * added to the View hierarchy prior to calling startTransitions. With + */ +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 + }; + + /** + * 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 optimized 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 unoptimized 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 isOptimized true if this is an optimized 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 isOptimized) { + 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, isOptimized); + } else { + calculateFragments(record, transitioningFragments, isOptimized); + } + } + + 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 (isOptimized) { + configureTransitionsOptimized(fragmentManager, containerId, + containerTransition, nonExistentView, nameOverrides); + } else { + configureTransitionsUnoptimized(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 + * optimized. 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 configureTransitionsOptimized(FragmentManagerImpl fragmentManager, + int containerId, FragmentContainerTransition fragments, + View nonExistentView, ArrayMap<String, String> nameOverrides) { + ViewGroup sceneRoot = (ViewGroup) 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 = configureSharedElementsOptimized(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) { + 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 + * not optimized. 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 configureTransitionsUnoptimized(FragmentManagerImpl fragmentManager, + int containerId, FragmentContainerTransition fragments, + View nonExistentView, ArrayMap<String, String> nameOverrides) { + ViewGroup sceneRoot = (ViewGroup) 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 = configureSharedElementsUnoptimized(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); + } + } + + /** + * This method is used for fragment transitions for unoptimized 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) { + + sceneRoot.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); + + if (enterTransition != null) { + enterTransition.removeTarget(nonExistentView); + ArrayList<View> views = configureEnteringExitingViews( + enterTransition, inFragment, sharedElementsIn, nonExistentView); + enteringViews.addAll(views); + } + + if (exitingViews != null) { + ArrayList<View> tempExiting = new ArrayList<>(); + tempExiting.add(nonExistentView); + replaceTargets(exitTransition, exitingViews, tempExiting); + exitingViews.clear(); + exitingViews.add(nonExistentView); + } + + return true; + } + }); + } + + /** + * 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 optimized 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 configureSharedElementsOptimized(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; + } else { + sharedElementsOut.addAll(outSharedElements.values()); + sharedElementsIn.addAll(inSharedElements.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; + } + + sceneRoot.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); + callSharedElementStartEnd(inFragment, outFragment, inIsPop, + inSharedElements, false); + if (epicenterView != null) { + epicenterView.getBoundsOnScreen(epicenter); + } + return true; + } + }); + return sharedElementTransition; + } + + /** + * Configures the shared elements of an unoptimized 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 configureSharedElementsUnoptimized(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; + + sceneRoot.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); + 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 true; + } + }); + 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; + } + + inSharedElements.retainAll(names); + if (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 && 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 Transition.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); + } + } + }); + } + + /** + * 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(); + 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 isOptimized) { + 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, isOptimized); + } + } + + /** + * 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 isOptimized) { + 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, isOptimized); + } + } + + /** + * 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 isOptimizedTransaction 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. + */ + private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op, + SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop, + boolean isOptimizedTransaction) { + final Fragment fragment = op.fragment; + 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 (isOptimizedTransaction) { + setLastIn = fragment.mHiddenChanged && !fragment.mHidden && + fragment.mAdded; + } else { + setLastIn = fragment.mHidden; + } + wasAdded = true; + break; + case BackStackRecord.OP_ADD: + case BackStackRecord.OP_ATTACH: + if (isOptimizedTransaction) { + setLastIn = fragment.mIsNewlyAdded; + } else { + setLastIn = !fragment.mAdded && !fragment.mHidden; + } + wasAdded = true; + break; + case BackStackRecord.OP_HIDE: + if (isOptimizedTransaction) { + 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 (isOptimizedTransaction) { + setFirstOut = !fragment.mAdded && fragment.mView != null && + fragment.mView.getVisibility() == View.VISIBLE; + } 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 (!isOptimizedTransaction && 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.mAllowOptimization) { + 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 (!isOptimizedTransaction && 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; + } +} |