Optimize Fragment operations so that minimal work is done.
Bug 29631389
When multiple operations are executed at once, they sometimes
cancel each other. For example, if the following transactions
are queued:
Transaction 1: add A
Transaction 2: replace with B
This can be trimmed down to add B.
This CL optimizes fragments in both add and pop directions.
Developers can choose not to allow optimization by
using FragmentTransaction#setAllowOptimization
Test: If6637e9f1c2a414bebaff6404efc45dd828378ad
Change-Id: Iab75be3e0aa388fc79b794e647ac6893165bebd7
diff --git a/api/current.txt b/api/current.txt
index 0b56c1d..dbf39c4 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -4668,6 +4668,7 @@
method public abstract android.app.FragmentTransaction remove(android.app.Fragment);
method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment);
method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment, java.lang.String);
+ method public abstract android.app.FragmentTransaction setAllowOptimization(boolean);
method public abstract android.app.FragmentTransaction setBreadCrumbShortTitle(int);
method public abstract android.app.FragmentTransaction setBreadCrumbShortTitle(java.lang.CharSequence);
method public abstract android.app.FragmentTransaction setBreadCrumbTitle(int);
diff --git a/api/system-current.txt b/api/system-current.txt
index 8866f1c..cf112c5 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4813,6 +4813,7 @@
method public abstract android.app.FragmentTransaction remove(android.app.Fragment);
method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment);
method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment, java.lang.String);
+ method public abstract android.app.FragmentTransaction setAllowOptimization(boolean);
method public abstract android.app.FragmentTransaction setBreadCrumbShortTitle(int);
method public abstract android.app.FragmentTransaction setBreadCrumbShortTitle(java.lang.CharSequence);
method public abstract android.app.FragmentTransaction setBreadCrumbTitle(int);
diff --git a/api/test-current.txt b/api/test-current.txt
index 1bd9b7b..8923781 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -4671,6 +4671,7 @@
method public abstract android.app.FragmentTransaction remove(android.app.Fragment);
method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment);
method public abstract android.app.FragmentTransaction replace(int, android.app.Fragment, java.lang.String);
+ method public abstract android.app.FragmentTransaction setAllowOptimization(boolean);
method public abstract android.app.FragmentTransaction setBreadCrumbShortTitle(int);
method public abstract android.app.FragmentTransaction setBreadCrumbShortTitle(java.lang.CharSequence);
method public abstract android.app.FragmentTransaction setBreadCrumbTitle(int);
diff --git a/core/java/android/app/BackStackRecord.java b/core/java/android/app/BackStackRecord.java
index a4b1a1f..c589466 100644
--- a/core/java/android/app/BackStackRecord.java
+++ b/core/java/android/app/BackStackRecord.java
@@ -16,6 +16,7 @@
package android.app;
+import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.os.Parcel;
@@ -52,6 +53,7 @@
final CharSequence mBreadCrumbShortTitleText;
final ArrayList<String> mSharedElementSourceNames;
final ArrayList<String> mSharedElementTargetNames;
+ final boolean mAllowOptimization;
public BackStackState(FragmentManagerImpl fm, BackStackRecord bse) {
final int numOps = bse.mOps.size();
@@ -81,6 +83,7 @@
mBreadCrumbShortTitleText = bse.mBreadCrumbShortTitleText;
mSharedElementSourceNames = bse.mSharedElementSourceNames;
mSharedElementTargetNames = bse.mSharedElementTargetNames;
+ mAllowOptimization = bse.mAllowOptimization;
}
public BackStackState(Parcel in) {
@@ -95,6 +98,7 @@
mBreadCrumbShortTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
mSharedElementSourceNames = in.createStringArrayList();
mSharedElementTargetNames = in.createStringArrayList();
+ mAllowOptimization = in.readInt() != 0;
}
public BackStackRecord instantiate(FragmentManagerImpl fm) {
@@ -137,6 +141,7 @@
bse.mBreadCrumbShortTitleText = mBreadCrumbShortTitleText;
bse.mSharedElementSourceNames = mSharedElementSourceNames;
bse.mSharedElementTargetNames = mSharedElementTargetNames;
+ bse.mAllowOptimization = mAllowOptimization;
bse.bumpBackStackNesting(1);
return bse;
}
@@ -157,6 +162,7 @@
TextUtils.writeToParcel(mBreadCrumbShortTitleText, dest, 0);
dest.writeStringList(mSharedElementSourceNames);
dest.writeStringList(mSharedElementTargetNames);
+ dest.writeInt(mAllowOptimization ? 1 : 0);
}
public static final Parcelable.Creator<BackStackState> CREATOR
@@ -175,7 +181,7 @@
* @hide Entry of an operation on the fragment back stack.
*/
final class BackStackRecord extends FragmentTransaction implements
- FragmentManager.BackStackEntry, Runnable {
+ FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator {
static final String TAG = FragmentManagerImpl.TAG;
final FragmentManagerImpl mManager;
@@ -210,6 +216,7 @@
String mName;
boolean mCommitted;
int mIndex = -1;
+ boolean mAllowOptimization;
int mBreadCrumbTitleRes;
CharSequence mBreadCrumbTitleText;
@@ -352,6 +359,7 @@
public BackStackRecord(FragmentManagerImpl manager) {
mManager = manager;
+ mAllowOptimization = Build.isAtLeastO();
}
public int getId() {
@@ -633,6 +641,12 @@
mManager.execSingleAction(this, true);
}
+ @Override
+ public FragmentTransaction setAllowOptimization(boolean allowOptimization) {
+ mAllowOptimization = allowOptimization;
+ return this;
+ }
+
int commitInternal(boolean allowStateLoss) {
if (mCommitted) {
throw new IllegalStateException("commit already called");
@@ -654,30 +668,40 @@
return mIndex;
}
- public void run() {
+ /**
+ * Implementation of {@link FragmentManagerImpl.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.
+ *
+ * @param records Modified to add this BackStackRecord
+ * @param isRecordPop Modified to add a false (this isn't a pop)
+ * @return true always because the records and isRecordPop will always be changed
+ */
+ @Override
+ public boolean generateOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop) {
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "Run: " + this);
}
+ records.add(this);
+ isRecordPop.add(false);
if (mAddToBackStack) {
- if (mIndex < 0) {
- throw new IllegalStateException("addToBackStack() called after commit()");
- }
+ mManager.addBackStackState(this);
}
+ return true;
+ }
- expandReplaceOps();
- bumpBackStackNesting(1);
-
- if (mManager.mCurState >= Fragment.CREATED) {
- SparseArray<FragmentContainerTransition> transitioningFragments = new SparseArray<>();
- calculateFragments(transitioningFragments);
- beginTransition(transitioningFragments);
- }
-
+ /**
+ * Executes the operations contained within this transaction. The Fragment states will only
+ * be modified if optimizations are not allowed.
+ */
+ void executeOps() {
final int numOps = mOps.size();
for (int opNum = 0; opNum < numOps; opNum++) {
final Op op = mOps.get(opNum);
- Fragment f = op.fragment;
+ final Fragment f = op.fragment;
+ f.mNextTransition = mTransition;
+ f.mNextTransitionStyle = mTransitionStyle;
switch (op.cmd) {
case OP_ADD:
f.mNextAnim = op.enterAnim;
@@ -685,56 +709,94 @@
break;
case OP_REMOVE:
f.mNextAnim = op.exitAnim;
- mManager.removeFragment(f, mTransition, mTransitionStyle);
+ mManager.removeFragment(f);
break;
case OP_HIDE:
f.mNextAnim = op.exitAnim;
- mManager.hideFragment(f, mTransition, mTransitionStyle);
+ mManager.hideFragment(f);
break;
case OP_SHOW:
f.mNextAnim = op.enterAnim;
- mManager.showFragment(f, mTransition, mTransitionStyle);
+ mManager.showFragment(f);
break;
case OP_DETACH:
f.mNextAnim = op.exitAnim;
- mManager.detachFragment(f, mTransition, mTransitionStyle);
+ mManager.detachFragment(f);
break;
case OP_ATTACH:
f.mNextAnim = op.enterAnim;
- mManager.attachFragment(f, mTransition, mTransitionStyle);
+ mManager.attachFragment(f);
break;
default:
throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
}
+ if (!mAllowOptimization && op.cmd != OP_ADD) {
+ mManager.moveFragmentToExpectedState(f);
+ }
}
-
- mManager.moveToState(mManager.mCurState, mTransition,
- mTransitionStyle, true);
-
- if (mAddToBackStack) {
- mManager.addBackStackState(this);
+ if (!mAllowOptimization) {
+ // Added fragments are added at the end to comply with prior behavior.
+ mManager.moveToState(mManager.mCurState);
}
}
- private void expandReplaceOps() {
- final int numOps = mOps.size();
-
- boolean hasReplace = false;
- // Before we do anything, check to see if any replace operations exist:
- for (int opNum = 0; opNum < numOps; opNum++) {
+ /**
+ * Reverses the execution of the operations within this transaction. The Fragment states will
+ * only be modified if optimizations are not allowed.
+ */
+ void executePopOps() {
+ for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {
final Op op = mOps.get(opNum);
- if (op.cmd == OP_REPLACE) {
- hasReplace = true;
- break;
+ Fragment f = op.fragment;
+ f.mNextTransition = FragmentManagerImpl.reverseTransit(mTransition);
+ f.mNextTransitionStyle = mTransitionStyle;
+ switch (op.cmd) {
+ case OP_ADD:
+ f.mNextAnim = op.popExitAnim;
+ mManager.removeFragment(f);
+ break;
+ case OP_REMOVE:
+ f.mNextAnim = op.popEnterAnim;
+ mManager.addFragment(f, false);
+ break;
+ case OP_HIDE:
+ f.mNextAnim = op.popEnterAnim;
+ mManager.showFragment(f);
+ break;
+ case OP_SHOW:
+ f.mNextAnim = op.popExitAnim;
+ mManager.hideFragment(f);
+ break;
+ case OP_DETACH:
+ f.mNextAnim = op.popEnterAnim;
+ mManager.attachFragment(f);
+ break;
+ case OP_ATTACH:
+ f.mNextAnim = op.popExitAnim;
+ mManager.detachFragment(f);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
+ }
+ if (!mAllowOptimization && op.cmd != OP_ADD) {
+ mManager.moveFragmentToExpectedState(f);
}
}
-
- if (!hasReplace) {
- return; // nothing to expand
+ if (!mAllowOptimization) {
+ mManager.moveToState(mManager.mCurState);
}
+ }
- ArrayList<Fragment> added = (mManager.mAdded == null) ? new ArrayList<Fragment>() :
- new ArrayList<>(mManager.mAdded);
+ /**
+ * Removes all OP_REPLACE ops and replaces them with the proper add and remove
+ * operations that are equivalent to the replace. This must be called prior to
+ * {@link #executeOps()} or any other call that operations on mOps.
+ *
+ * @param added Initialized to the fragments that are in the mManager.mAdded, this
+ * will be modified to contain the fragments that will be in mAdded
+ * after the execution ({@link #executeOps()}.
+ */
+ void expandReplaceOps(ArrayList<Fragment> added) {
for (int opNum = 0; opNum < mOps.size(); opNum++) {
final Op op = mOps.get(opNum);
switch (op.cmd) {
@@ -822,13 +884,14 @@
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) {
+ Build.VERSION_CODES.N && !mAllowOptimization) {
mManager.makeActive(fragment);
mManager.moveToState(fragment, Fragment.CREATED, 0, 0, false);
}
@@ -843,7 +906,7 @@
* and last fragments to be added. This will be modified by
* this method.
*/
- private void calculateFragments(
+ public void calculateFragments(
SparseArray<FragmentContainerTransition> transitioningFragments) {
if (!mManager.mContainer.onHasView()) {
return; // nothing to see, so no transitions
@@ -874,7 +937,7 @@
* and last fragments to be added. This will be modified by
* this method.
*/
- public void calculateBackFragments(
+ public void calculatePopFragments(
SparseArray<FragmentContainerTransition> transitioningFragments) {
if (!mManager.mContainer.onHasView()) {
return; // nothing to see, so no transitions
@@ -923,7 +986,7 @@
* in {@link #setNameOverrides(android.app.BackStackRecord.TransitionState, java.util.ArrayList,
* java.util.ArrayList)}.
*/
- private TransitionState beginTransition(SparseArray<FragmentContainerTransition> containers) {
+ TransitionState beginTransition(SparseArray<FragmentContainerTransition> containers) {
TransitionState state = new TransitionState();
// Adding a non-existent target view makes sure that the transitions don't target
@@ -947,28 +1010,28 @@
return transition;
}
- private static Transition getEnterTransition(Fragment inFragment, boolean isBack) {
+ private static Transition getEnterTransition(Fragment inFragment, boolean isPop) {
if (inFragment == null) {
return null;
}
- return cloneTransition(isBack ? inFragment.getReenterTransition() :
+ return cloneTransition(isPop ? inFragment.getReenterTransition() :
inFragment.getEnterTransition());
}
- private static Transition getExitTransition(Fragment outFragment, boolean isBack) {
+ private static Transition getExitTransition(Fragment outFragment, boolean isPop) {
if (outFragment == null) {
return null;
}
- return cloneTransition(isBack ? outFragment.getReturnTransition() :
+ return cloneTransition(isPop ? outFragment.getReturnTransition() :
outFragment.getExitTransition());
}
private static TransitionSet getSharedElementTransition(Fragment inFragment,
- Fragment outFragment, boolean isBack) {
+ Fragment outFragment, boolean isPop) {
if (inFragment == null || outFragment == null) {
return null;
}
- Transition transition = cloneTransition(isBack
+ Transition transition = cloneTransition(isPop
? outFragment.getSharedElementReturnTransition()
: inFragment.getSharedElementEnterTransition());
if (transition == null) {
@@ -998,11 +1061,11 @@
}
private ArrayMap<String, View> remapSharedElements(TransitionState state, Fragment outFragment,
- boolean isBack) {
+ boolean isPop) {
ArrayMap<String, View> namedViews = new ArrayMap<String, View>();
if (mSharedElementSourceNames != null) {
outFragment.getView().findNamedViews(namedViews);
- if (isBack) {
+ if (isPop) {
namedViews.retainAll(mSharedElementTargetNames);
} else {
namedViews = remapNames(mSharedElementSourceNames, mSharedElementTargetNames,
@@ -1010,7 +1073,7 @@
}
}
- if (isBack) {
+ if (isPop) {
outFragment.mEnterTransitionCallback.onMapSharedElements(
mSharedElementTargetNames, namedViews);
setBackNameOverrides(state, namedViews, false);
@@ -1038,7 +1101,7 @@
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 isBack,
+ final ArrayList<View> hiddenFragmentViews, final boolean isPop,
final ArrayList<View> sharedElementTargets) {
if (enterTransition == null && sharedElementTransition == null &&
overallTransition == null) {
@@ -1059,7 +1122,7 @@
ArrayMap<String, View> namedViews = null;
if (sharedElementTransition != null) {
- namedViews = mapSharedElementsIn(state, isBack, inFragment);
+ namedViews = mapSharedElementsIn(state, isPop, inFragment);
removeTargets(sharedElementTransition, sharedElementTargets);
// keep the nonExistentView as excluded so the list doesn't get emptied
sharedElementTargets.remove(state.nonExistentView);
@@ -1073,7 +1136,7 @@
setEpicenterIn(namedViews, state);
- callSharedElementEnd(state, inFragment, outFragment, isBack,
+ callSharedElementEnd(state, inFragment, outFragment, isPop,
namedViews);
}
@@ -1104,8 +1167,8 @@
}
private void callSharedElementEnd(TransitionState state, Fragment inFragment,
- Fragment outFragment, boolean isBack, ArrayMap<String, View> namedViews) {
- SharedElementCallback sharedElementCallback = isBack ?
+ Fragment outFragment, boolean isPop, ArrayMap<String, View> namedViews) {
+ SharedElementCallback sharedElementCallback = isPop ?
outFragment.mEnterTransitionCallback :
inFragment.mEnterTransitionCallback;
ArrayList<String> names = new ArrayList<String>(namedViews.keySet());
@@ -1125,13 +1188,13 @@
}
private ArrayMap<String, View> mapSharedElementsIn(TransitionState state,
- boolean isBack, Fragment inFragment) {
+ boolean isPop, Fragment inFragment) {
// Now map the shared elements in the incoming fragment
- ArrayMap<String, View> namedViews = mapEnteringSharedElements(state, inFragment, isBack);
+ ArrayMap<String, View> namedViews = mapEnteringSharedElements(state, inFragment, isPop);
// remap shared elements and set the name mapping used
// in the shared element transition.
- if (isBack) {
+ if (isPop) {
inFragment.mExitTransitionCallback.onMapSharedElements(
mSharedElementTargetNames, namedViews);
setBackNameOverrides(state, namedViews, true);
@@ -1145,10 +1208,10 @@
private static Transition mergeTransitions(Transition enterTransition,
Transition exitTransition, Transition sharedElementTransition, Fragment inFragment,
- boolean isBack) {
+ boolean isPop) {
boolean overlap = true;
if (enterTransition != null && exitTransition != null && inFragment != null) {
- overlap = isBack ? inFragment.getAllowReturnTransitionOverlap() :
+ overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() :
inFragment.getAllowEnterTransitionOverlap();
}
@@ -1496,17 +1559,17 @@
* @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 isBack true if this is popping the back stack or false if this is a
+ * @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 isBack) {
+ 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 (isBack) {
+ if (isPop) {
namedViews = remapNames(mSharedElementSourceNames,
mSharedElementTargetNames, namedViews);
} else {
@@ -1565,81 +1628,6 @@
});
}
- public TransitionState popFromBackStack(boolean doStateMove, TransitionState state,
- SparseArray<FragmentContainerTransition> transitioningFragments) {
- if (FragmentManagerImpl.DEBUG) {
- Log.v(TAG, "popFromBackStack: " + this);
- LogWriter logw = new LogWriter(Log.VERBOSE, TAG);
- PrintWriter pw = new FastPrintWriter(logw, false, 1024);
- dump(" ", null, pw, null);
- pw.flush();
- }
-
- if (mManager.mCurState >= Fragment.CREATED) {
- if (state == null) {
- if (transitioningFragments.size() != 0) {
- state = beginTransition(transitioningFragments);
- }
- } else if (!doStateMove) {
- setNameOverrides(state, mSharedElementTargetNames, mSharedElementSourceNames);
- }
- }
-
- bumpBackStackNesting(-1);
-
- final int numOps = mOps.size();
- for (int opNum = numOps - 1; opNum >= 0; opNum--) {
- final Op op = mOps.get(opNum);
- Fragment f = op.fragment;
- switch (op.cmd) {
- case OP_ADD:
- f.mNextAnim = op.popExitAnim;
- mManager.removeFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition),
- mTransitionStyle);
- break;
- case OP_REMOVE:
- f.mNextAnim = op.popEnterAnim;
- mManager.addFragment(f, false);
- break;
- case OP_HIDE:
- f.mNextAnim = op.popEnterAnim;
- mManager.showFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle);
- break;
- case OP_SHOW:
- f.mNextAnim = op.popExitAnim;
- mManager.hideFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle);
- break;
- case OP_DETACH:
- f.mNextAnim = op.popEnterAnim;
- mManager.attachFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle);
- break;
- case OP_ATTACH:
- f.mNextAnim = op.popExitAnim;
- mManager.detachFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle);
- break;
- default:
- throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
- }
- }
-
- if (doStateMove) {
- mManager.moveToState(mManager.mCurState,
- FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle, true);
- state = null;
- }
-
- if (mIndex >= 0) {
- mManager.freeBackStackIndex(mIndex);
- mIndex = -1;
- }
- return state;
- }
-
private static void setNameOverride(ArrayMap<String, String> overrides,
String source, String target) {
if (source != null && target != null && !source.equals(target)) {
@@ -1653,7 +1641,7 @@
}
}
- private static void setNameOverrides(TransitionState state, ArrayList<String> sourceNames,
+ static void setNameOverrides(TransitionState state, ArrayList<String> sourceNames,
ArrayList<String> targetNames) {
if (sourceNames != null && targetNames != null) {
for (int i = 0; i < sourceNames.size(); i++) {
diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java
index 6ea170e..b72b960 100644
--- a/core/java/android/app/Fragment.java
+++ b/core/java/android/app/Fragment.java
@@ -481,6 +481,12 @@
// 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;
@@ -510,6 +516,9 @@
SharedElementCallback mEnterTransitionCallback = SharedElementCallback.NULL_CALLBACK;
SharedElementCallback mExitTransitionCallback = SharedElementCallback.NULL_CALLBACK;
+ // True if mHidden has been changed and the animation should be scheduled.
+ boolean mHiddenChanged;
+
/**
* State information that has been retrieved from a fragment instance
* through {@link FragmentManager#saveFragmentInstanceState(Fragment)
diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java
index 674c3f7..b2df1ac 100644
--- a/core/java/android/app/FragmentManager.java
+++ b/core/java/android/app/FragmentManager.java
@@ -28,10 +28,10 @@
import android.content.res.TypedArray;
import android.os.Bundle;
import android.os.Debug;
-import android.os.Handler;
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;
@@ -445,8 +445,7 @@
}
}
- ArrayList<Runnable> mPendingActions;
- Runnable[] mTmpActions;
+ ArrayList<OpGenerator> mPendingActions;
boolean mExecutingActions;
ArrayList<Fragment> mActive;
@@ -463,7 +462,6 @@
int mCurState = Fragment.INITIALIZING;
FragmentHostCallback<?> mHost;
- FragmentController mController;
FragmentContainer mContainer;
Fragment mParent;
@@ -473,6 +471,12 @@
String mNoTransactionsBecause;
boolean mHavePendingDeferredStart;
+ // 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;
@@ -565,56 +569,66 @@
@Override
public void popBackStack() {
- enqueueAction(new Runnable() {
- @Override public void run() {
- popBackStackState(mHost.getHandler(), null, -1, 0);
- }
- }, false);
+ enqueueAction(new PopBackStackState(null, -1, 0), false);
}
@Override
public boolean popBackStackImmediate() {
checkStateLoss();
- executePendingTransactions();
- return popBackStackState(mHost.getHandler(), null, -1, 0);
+ return popBackStackImmediate(null, -1, 0);
}
@Override
- public void popBackStack(final String name, final int flags) {
- enqueueAction(new Runnable() {
- @Override public void run() {
- popBackStackState(mHost.getHandler(), name, -1, flags);
- }
- }, false);
+ public void popBackStack(String name, int flags) {
+ enqueueAction(new PopBackStackState(name, -1, flags), false);
}
@Override
public boolean popBackStackImmediate(String name, int flags) {
checkStateLoss();
- executePendingTransactions();
- return popBackStackState(mHost.getHandler(), name, -1, flags);
+ return popBackStackImmediate(name, -1, flags);
}
@Override
- public void popBackStack(final int id, final int flags) {
+ public void popBackStack(int id, int flags) {
if (id < 0) {
throw new IllegalArgumentException("Bad id: " + id);
}
- enqueueAction(new Runnable() {
- @Override public void run() {
- popBackStackState(mHost.getHandler(), null, id, flags);
- }
- }, false);
+ enqueueAction(new PopBackStackState(null, id, flags), false);
}
@Override
public boolean popBackStackImmediate(int id, int flags) {
checkStateLoss();
- executePendingTransactions();
if (id < 0) {
throw new IllegalArgumentException("Bad id: " + id);
}
- return popBackStackState(mHost.getHandler(), null, id, flags);
+ return popBackStackImmediate(null, id, flags);
+ }
+
+ /**
+ * Used by all public popBackStackImmediate methods, this executes pending transactions and
+ * returns true if the pop action did anything, regardless of what other pending
+ * transactions did.
+ *
+ * @return true if the pop operation did anything or false otherwise.
+ */
+ private boolean popBackStackImmediate(String name, int id, int flags) {
+ executePendingTransactions();
+ ensureExecReady(true);
+
+ boolean executePop = popBackStackState(mTmpRecords, mTmpIsPop, name, id, flags);
+ if (executePop) {
+ mExecutingActions = true;
+ try {
+ optimizeAndExecuteOps(mTmpRecords, mTmpIsPop);
+ } finally {
+ cleanupExec();
+ }
+ }
+
+ doPendingDeferredStart();
+ return executePop;
}
@Override
@@ -785,7 +799,7 @@
if (N > 0) {
writer.print(prefix); writer.println("Pending Actions:");
for (int i=0; i<N; i++) {
- Runnable r = mPendingActions.get(i);
+ OpGenerator r = mPendingActions.get(i);
writer.print(prefix); writer.print(" #"); writer.print(i);
writer.print(": "); writer.println(r);
}
@@ -1149,26 +1163,115 @@
moveToState(f, mCurState, 0, 0, false);
}
- void moveToState(int newState, boolean always) {
- moveToState(newState, 0, 0, always);
+ /**
+ * Fragments that have been shown or hidden don't have their visibility changed or
+ * animations run during the {@link #showFragment(Fragment)} or {@link #hideFragment(Fragment)}
+ * calls. After fragments are brought to their final state in
+ * {@link #moveFragmentToExpectedState(Fragment)} the fragments that have been shown or
+ * hidden must have their visibility changed and their animations started here.
+ *
+ * @param fragment The fragment with mHiddenChanged = true that should change its View's
+ * visibility and start the show or hide animation.
+ */
+ void completeShowHideFragment(final Fragment fragment) {
+ if (fragment.mView != null) {
+ Animator anim = loadAnimator(fragment, fragment.mNextTransition, !fragment.mHidden,
+ fragment.mNextTransitionStyle);
+ if (anim != null) {
+ anim.setTarget(fragment.mView);
+ if (fragment.mHidden) {
+ // Delay the actual hide operation until the animation finishes, otherwise
+ // the fragment will just immediately disappear
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ animation.removeListener(this);
+ if (fragment.mView != null) {
+ fragment.mView.setVisibility(View.GONE);
+ }
+ }
+ });
+ }
+ setHWLayerAnimListenerIfAlpha(fragment.mView, anim);
+ anim.start();
+ } else {
+ final int visibility = fragment.mHidden ? View.GONE : View.VISIBLE;
+ fragment.mView.setVisibility(visibility);
+ }
+ }
+ if (fragment.mAdded && fragment.mHasMenu && fragment.mMenuVisible) {
+ mNeedMenuInvalidate = true;
+ }
+ fragment.mHiddenChanged = false;
+ fragment.onHiddenChanged(fragment.mHidden);
}
-
- void moveToState(int newState, int transit, int transitStyle, boolean always) {
+
+ /**
+ * Moves a fragment to its expected final state or the fragment manager's state, depending
+ * on whether the fragment manager's state is raised properly.
+ *
+ * @param f The fragment to change.
+ */
+ void moveFragmentToExpectedState(Fragment f) {
+ if (f == null) {
+ return;
+ }
+ int nextState = mCurState;
+ if (f.mRemoving) {
+ if (f.isInBackStack()) {
+ nextState = Fragment.CREATED;
+ } else {
+ nextState = Fragment.INITIALIZING;
+ }
+ }
+ moveToState(f, nextState, f.mNextTransition, f.mNextTransitionStyle, false);
+
+ if (f.mView != null) {
+ Fragment underFragment = findFragmentUnder(f);
+ if (underFragment != null) {
+ final View underView = underFragment.mView;
+ // make sure this fragment is in the right order.
+ final ViewGroup container = f.mContainer;
+ int underIndex = container.indexOfChild(underView);
+ int viewIndex = container.indexOfChild(f.mView);
+ if (viewIndex < underIndex) {
+ container.removeViewAt(viewIndex);
+ container.addView(f.mView, underIndex);
+ }
+ }
+ }
+ if (f.mHiddenChanged) {
+ completeShowHideFragment(f);
+ }
+ }
+
+ void moveToState(int newState) {
if (mHost == null && newState != Fragment.INITIALIZING) {
throw new IllegalStateException("No activity");
}
- if (!always && mCurState == newState) {
- return;
- }
-
mCurState = newState;
+
if (mActive != null) {
boolean loadersRunning = false;
- for (int i=0; i<mActive.size(); i++) {
+
+ // Must add them in the proper order. mActive fragments may be out of order
+ final int numAdded = mAdded.size();
+ for (int i = 0; i < numAdded; i++) {
+ Fragment f = mAdded.get(i);
+ moveFragmentToExpectedState(f);
+ if (f.mLoaderManager != null) {
+ loadersRunning |= f.mLoaderManager.hasRunningLoaders();
+ }
+ }
+
+ // Now iterate through all active fragments. These will include those that are removed
+ // and detached.
+ final int numActive = mActive.size();
+ for (int i = 0; i < numActive; i++) {
Fragment f = mActive.get(i);
- if (f != null) {
- moveToState(f, newState, transit, transitStyle, false);
+ if (f != null && (f.mRemoving || f.mDetached)) {
+ moveFragmentToExpectedState(f);
if (f.mLoaderManager != null) {
loadersRunning |= f.mLoaderManager.hasRunningLoaders();
}
@@ -1244,6 +1347,9 @@
mAdded.add(fragment);
fragment.mAdded = true;
fragment.mRemoving = false;
+ if (fragment.mView == null) {
+ fragment.mHiddenChanged = false;
+ }
if (fragment.mHasMenu && fragment.mMenuVisible) {
mNeedMenuInvalidate = true;
}
@@ -1252,8 +1358,8 @@
}
}
}
-
- public void removeFragment(Fragment fragment, int transition, int transitionStyle) {
+
+ public void removeFragment(Fragment fragment) {
if (DEBUG) Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting);
final boolean inactive = !fragment.isInBackStack();
if (!fragment.mDetached || inactive) {
@@ -1273,66 +1379,42 @@
}
fragment.mAdded = false;
fragment.mRemoving = true;
- moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED,
- transition, transitionStyle, false);
}
}
-
- public void hideFragment(Fragment fragment, int transition, int transitionStyle) {
+
+ /**
+ * Marks a fragment as hidden to be later animated in with
+ * {@link #completeShowHideFragment(Fragment)}.
+ *
+ * @param fragment The fragment to be shown.
+ */
+ public void hideFragment(Fragment fragment) {
if (DEBUG) Log.v(TAG, "hide: " + fragment);
if (!fragment.mHidden) {
fragment.mHidden = true;
- if (fragment.mView != null) {
- Animator anim = loadAnimator(fragment, transition, false,
- transitionStyle);
- if (anim != null) {
- anim.setTarget(fragment.mView);
- // Delay the actual hide operation until the animation finishes, otherwise
- // the fragment will just immediately disappear
- final Fragment finalFragment = fragment;
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- if (finalFragment.mView != null) {
- finalFragment.mView.setVisibility(View.GONE);
- }
- }
- });
- setHWLayerAnimListenerIfAlpha(finalFragment.mView, anim);
- anim.start();
- } else {
- fragment.mView.setVisibility(View.GONE);
- }
- }
- if (fragment.mAdded && fragment.mHasMenu && fragment.mMenuVisible) {
- mNeedMenuInvalidate = true;
- }
- fragment.onHiddenChanged(true);
+ // Toggle hidden changed so that if a fragment goes through show/hide/show
+ // it doesn't go through the animation.
+ fragment.mHiddenChanged = !fragment.mHiddenChanged;
}
}
-
- public void showFragment(Fragment fragment, int transition, int transitionStyle) {
+
+ /**
+ * Marks a fragment as shown to be later animated in with
+ * {@link #completeShowHideFragment(Fragment)}.
+ *
+ * @param fragment The fragment to be shown.
+ */
+ public void showFragment(Fragment fragment) {
if (DEBUG) Log.v(TAG, "show: " + fragment);
if (fragment.mHidden) {
fragment.mHidden = false;
- if (fragment.mView != null) {
- Animator anim = loadAnimator(fragment, transition, true,
- transitionStyle);
- if (anim != null) {
- anim.setTarget(fragment.mView);
- setHWLayerAnimListenerIfAlpha(fragment.mView, anim);
- anim.start();
- }
- fragment.mView.setVisibility(View.VISIBLE);
- }
- if (fragment.mAdded && fragment.mHasMenu && fragment.mMenuVisible) {
- mNeedMenuInvalidate = true;
- }
- fragment.onHiddenChanged(false);
+ // Toggle hidden changed so that if a fragment goes through show/hide/show
+ // it doesn't go through the animation.
+ fragment.mHiddenChanged = !fragment.mHiddenChanged;
}
}
-
- public void detachFragment(Fragment fragment, int transition, int transitionStyle) {
+
+ public void detachFragment(Fragment fragment) {
if (DEBUG) Log.v(TAG, "detach: " + fragment);
if (!fragment.mDetached) {
fragment.mDetached = true;
@@ -1346,12 +1428,11 @@
mNeedMenuInvalidate = true;
}
fragment.mAdded = false;
- moveToState(fragment, Fragment.CREATED, transition, transitionStyle, false);
}
}
}
- public void attachFragment(Fragment fragment, int transition, int transitionStyle) {
+ public void attachFragment(Fragment fragment) {
if (DEBUG) Log.v(TAG, "attach: " + fragment);
if (fragment.mDetached) {
fragment.mDetached = false;
@@ -1368,7 +1449,6 @@
if (fragment.mHasMenu && fragment.mMenuVisible) {
mNeedMenuInvalidate = true;
}
- moveToState(fragment, mCurState, transition, transitionStyle, false);
}
}
}
@@ -1447,7 +1527,7 @@
* @param allowStateLoss whether to allow loss of state information
* @throws IllegalStateException if the activity has been destroyed
*/
- public void enqueueAction(Runnable action, boolean allowStateLoss) {
+ public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
@@ -1456,7 +1536,7 @@
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
- mPendingActions = new ArrayList<Runnable>();
+ mPendingActions = new ArrayList<>();
}
mPendingActions.add(action);
if (mPendingActions.size() == 1) {
@@ -1522,7 +1602,13 @@
}
}
- public void execSingleAction(Runnable action, boolean allowStateLoss) {
+ /**
+ * Broken out from exec*, this prepares for gathering and executing operations.
+ *
+ * @param allowStateLoss true if state loss should be ignored or false if it should be
+ * checked.
+ */
+ private void ensureExecReady(boolean allowStateLoss) {
if (mExecutingActions) {
throw new IllegalStateException("FragmentManager is already executing transactions");
}
@@ -1535,55 +1621,49 @@
checkStateLoss();
}
- mExecutingActions = true;
- try {
- action.run();
- } finally {
- mExecutingActions = false;
+ if (mTmpRecords == null) {
+ mTmpRecords = new ArrayList<>();
+ mTmpIsPop = new ArrayList<>();
+ }
+ }
+
+ public void execSingleAction(OpGenerator action, boolean allowStateLoss) {
+ ensureExecReady(allowStateLoss);
+ if (action.generateOps(mTmpRecords, mTmpIsPop)) {
+ mExecutingActions = true;
+ try {
+ optimizeAndExecuteOps(mTmpRecords, mTmpIsPop);
+ } finally {
+ cleanupExec();
+ }
}
doPendingDeferredStart();
}
/**
+ * Broken out of exec*, this cleans up the mExecutingActions and the temporary structures
+ * used in executing operations.
+ */
+ private void cleanupExec() {
+ mExecutingActions = false;
+ mTmpIsPop.clear();
+ mTmpRecords.clear();
+ }
+
+ /**
* Only call from main thread!
*/
public boolean execPendingActions() {
- if (mExecutingActions) {
- throw new IllegalStateException("Recursive entry to executePendingTransactions");
- }
-
- if (Looper.myLooper() != mHost.getHandler().getLooper()) {
- throw new IllegalStateException("Must be called from main thread of process");
- }
+ ensureExecReady(true);
boolean didSomething = false;
-
- while (true) {
- int numActions;
-
- synchronized (this) {
- if (mPendingActions == null || mPendingActions.size() == 0) {
- break;
- }
-
- numActions = mPendingActions.size();
- if (mTmpActions == null || mTmpActions.length < numActions) {
- mTmpActions = new Runnable[numActions];
- }
- mPendingActions.toArray(mTmpActions);
- mPendingActions.clear();
- mHost.getHandler().removeCallbacks(mExecCommit);
- }
-
+ while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {
mExecutingActions = true;
try {
- for (int i = 0; i < numActions; i++) {
- mTmpActions[i].run();
- mTmpActions[i] = null;
- }
+ optimizeAndExecuteOps(mTmpRecords, mTmpIsPop);
} finally {
- mExecutingActions = false;
+ cleanupExec();
}
didSomething = true;
}
@@ -1593,6 +1673,265 @@
return didSomething;
}
+ /**
+ * Optimizes BackStackRecord operations. This method merges operations of proximate records
+ * that allow optimization. See {@link FragmentTransaction#setAllowOptimization(boolean)}.
+ * <p>
+ * For example, a transaction that adds to the back stack and then another that pops that
+ * back stack record will be optimized.
+ * <p>
+ * Likewise, two transactions committed that are executed at the same time will be optimized
+ * as well as two pop operations executed together.
+ *
+ * @param records The records pending execution
+ * @param isRecordPop The direction that these records are being run.
+ */
+ private void optimizeAndExecuteOps(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isRecordPop) {
+ if (records == null || records.isEmpty()) {
+ return;
+ }
+
+ if (isRecordPop == null || records.size() != isRecordPop.size()) {
+ throw new IllegalStateException("Internal error with the back stack records");
+ }
+
+ final int numRecords = records.size();
+ int startIndex = 0;
+ for (int recordNum = 0; recordNum < numRecords; recordNum++) {
+ final boolean canOptimize = records.get(recordNum).mAllowOptimization;
+ if (!canOptimize) {
+ // execute all previous transactions
+ if (startIndex != recordNum) {
+ executeOpsTogether(records, isRecordPop, startIndex, recordNum);
+ }
+ // execute all unoptimized together
+ int optimizeEnd;
+ for (optimizeEnd = recordNum + 1; optimizeEnd < numRecords; optimizeEnd++) {
+ if (records.get(optimizeEnd).mAllowOptimization) {
+ break;
+ }
+ }
+ executeOpsTogether(records, isRecordPop, recordNum, optimizeEnd);
+ startIndex = optimizeEnd;
+ recordNum = optimizeEnd - 1;
+ }
+ }
+ if (startIndex != numRecords) {
+ executeOpsTogether(records, isRecordPop, startIndex, numRecords);
+ }
+ }
+
+ /**
+ * Optimizes a subset of a list of BackStackRecords, all of which either allow optimization or
+ * do not allow optimization.
+ * @param records A list of BackStackRecords that are to be optimized
+ * @param isRecordPop The direction that these records are being run.
+ * @param startIndex The index of the first record in <code>records</code> to be optimized
+ * @param endIndex One more than the final record index in <code>records</code> to optimize.
+ */
+ private void executeOpsTogether(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
+ final boolean allowOptimization = records.get(startIndex).mAllowOptimization;
+ boolean addToBackStack = false;
+ if (mTmpAddedFragments == null) {
+ mTmpAddedFragments = new ArrayList<>();
+ mTmpFragmentsContainerTransitions = new SparseArray<>();
+ } else {
+ mTmpAddedFragments.clear();
+ mTmpFragmentsContainerTransitions.clear();
+ }
+ if (mAdded != null) {
+ mTmpAddedFragments.addAll(mAdded);
+ }
+ for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
+ final BackStackRecord record = records.get(recordNum);
+ final boolean isPop = isRecordPop.get(recordNum);
+ if (!isPop) {
+ record.expandReplaceOps(mTmpAddedFragments);
+ }
+ 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);
+ }
+ executeOps(records, isRecordPop, startIndex, endIndex);
+
+ if (allowOptimization) {
+ moveFragmentsToAtLeastCreated();
+ startTransitions(records, isRecordPop, startIndex, endIndex);
+ moveToState(mCurState);
+ }
+
+ for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
+ final BackStackRecord record = records.get(recordNum);
+ final boolean isPop = isRecordPop.get(recordNum);
+ if (isPop && record.mIndex >= 0) {
+ freeBackStackIndex(record.mIndex);
+ record.mIndex = -1;
+ }
+ }
+ if (addToBackStack) {
+ reportBackStackChanged();
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * As an example, if mAdded has two Fragments with Views sharing the same container:
+ * FragmentA
+ * FragmentB
+ *
+ * Then, when processing FragmentB, FragmentA will be returned. If, however, FragmentA
+ * had no View, null would be returned.
+ *
+ * @param f The fragment that may be on top of another fragment.
+ * @return The fragment with a View under f, if one exists or null if f has no View or
+ * there are no fragments with Views in the same container.
+ */
+ private Fragment findFragmentUnder(Fragment f) {
+ final ViewGroup container = f.mContainer;
+ final View view = f.mView;
+
+ if (container == null || view == null) {
+ return null;
+ }
+
+ final int fragmentIndex = mAdded.indexOf(f);
+ for (int i = fragmentIndex - 1; i >= 0; i--) {
+ Fragment underFragment = mAdded.get(i);
+ if (underFragment.mContainer == container && underFragment.mView != null) {
+ // Found the fragment under this one
+ return underFragment;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Run the operations in the BackStackRecords, either to push or pop.
+ *
+ * @param records The list of records whose operations should be run.
+ * @param isRecordPop The direction that these records are being run.
+ * @param startIndex The index of the first entry in records to run.
+ * @param endIndex One past the index of the final entry in records to run.
+ */
+ private static void executeOps(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
+ for (int i = startIndex; i < endIndex; i++) {
+ final BackStackRecord record = records.get(i);
+ final boolean isPop = isRecordPop.get(i);
+ if (isPop) {
+ record.executePopOps();
+ } else {
+ record.executeOps();
+ }
+ }
+ }
+
+ /**
+ * 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.
+ */
+ 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);
+ }
+ }
+ 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.
+ */
+ private void moveFragmentsToAtLeastCreated() {
+ if (mCurState < Fragment.CREATED) {
+ return;
+ }
+ 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);
+ }
+ }
+ }
+
+ /**
+ * Adds all records in the pending actions to records and whether they are add or pop
+ * operations to isPop. After executing, the pending actions will be empty.
+ *
+ * @param records All pending actions will generate BackStackRecords added to this.
+ * This contains the transactions, in order, to execute.
+ * @param isPop All pending actions will generate booleans to add to this. This contains
+ * an entry for each entry in records to indicate whether or not it is a
+ * pop action.
+ */
+ private boolean generateOpsForPendingActions(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isPop) {
+ int numActions;
+ synchronized (this) {
+ if (mPendingActions == null || mPendingActions.size() == 0) {
+ return false;
+ }
+
+ numActions = mPendingActions.size();
+ for (int i = 0; i < numActions; i++) {
+ mPendingActions.get(i).generateOps(records, isPop);
+ }
+ mPendingActions.clear();
+ mHost.getHandler().removeCallbacks(mExecCommit);
+ }
+ return numActions > 0;
+ }
+
void doPendingDeferredStart() {
if (mHavePendingDeferredStart) {
boolean loadersRunning = false;
@@ -1624,24 +1963,19 @@
mBackStack.add(state);
reportBackStackChanged();
}
-
- boolean popBackStackState(Handler handler, String name, int id, int flags) {
+
+ boolean popBackStackState(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
+ String name, int id, int flags) {
if (mBackStack == null) {
return false;
}
- if (name == null && id < 0 && (flags&POP_BACK_STACK_INCLUSIVE) == 0) {
- int last = mBackStack.size()-1;
+ if (name == null && id < 0 && (flags & POP_BACK_STACK_INCLUSIVE) == 0) {
+ int last = mBackStack.size() - 1;
if (last < 0) {
return false;
}
- final BackStackRecord bss = mBackStack.remove(last);
- SparseArray<BackStackRecord.FragmentContainerTransition> transitioningFragments =
- new SparseArray<>();
- if (mCurState >= Fragment.CREATED) {
- bss.calculateBackFragments(transitioningFragments);
- }
- bss.popFromBackStack(true, null, transitioningFragments);
- reportBackStackChanged();
+ records.add(mBackStack.remove(last));
+ isRecordPop.add(true);
} else {
int index = -1;
if (name != null || id >= 0) {
@@ -1678,25 +2012,10 @@
if (index == mBackStack.size()-1) {
return false;
}
- final ArrayList<BackStackRecord> states
- = new ArrayList<BackStackRecord>();
- for (int i=mBackStack.size()-1; i>index; i--) {
- states.add(mBackStack.remove(i));
+ for (int i = mBackStack.size() - 1; i > index; i--) {
+ records.add(mBackStack.remove(i));
+ isRecordPop.add(true);
}
- final int LAST = states.size()-1;
- SparseArray<BackStackRecord.FragmentContainerTransition> transitioningFragments =
- new SparseArray<>();
- if (mCurState >= Fragment.CREATED) {
- for (int i = 0; i <= LAST; i++) {
- states.get(i).calculateBackFragments(transitioningFragments);
- }
- }
- BackStackRecord.TransitionState state = null;
- for (int i=0; i<=LAST; i++) {
- if (DEBUG) Log.v(TAG, "Popping back stack state: " + states.get(i));
- state = states.get(i).popFromBackStack(i == LAST, state, transitioningFragments);
- }
- reportBackStackChanged();
}
return true;
}
@@ -2036,40 +2355,40 @@
public void dispatchCreate() {
mStateSaved = false;
- moveToState(Fragment.CREATED, false);
+ moveToState(Fragment.CREATED);
}
public void dispatchActivityCreated() {
mStateSaved = false;
- moveToState(Fragment.ACTIVITY_CREATED, false);
+ moveToState(Fragment.ACTIVITY_CREATED);
}
public void dispatchStart() {
mStateSaved = false;
- moveToState(Fragment.STARTED, false);
+ moveToState(Fragment.STARTED);
}
public void dispatchResume() {
mStateSaved = false;
- moveToState(Fragment.RESUMED, false);
+ moveToState(Fragment.RESUMED);
}
public void dispatchPause() {
- moveToState(Fragment.STARTED, false);
+ moveToState(Fragment.STARTED);
}
public void dispatchStop() {
- moveToState(Fragment.STOPPED, false);
+ moveToState(Fragment.STOPPED);
}
public void dispatchDestroyView() {
- moveToState(Fragment.CREATED, false);
+ moveToState(Fragment.CREATED);
}
public void dispatchDestroy() {
mDestroyed = true;
execPendingActions();
- moveToState(Fragment.INITIALIZING, false);
+ moveToState(Fragment.INITIALIZING);
mHost = null;
mContainer = null;
mParent = null;
@@ -2363,4 +2682,45 @@
LayoutInflater.Factory2 getLayoutInflaterFactory() {
return this;
}
+
+ /**
+ * An add or pop transaction to be scheduled for the UI thread.
+ */
+ interface OpGenerator {
+ /**
+ * Generate transactions to add to {@code records} and whether or not the transaction is
+ * an add or pop to {@code isRecordPop}.
+ *
+ * records and isRecordPop must be added equally so that each transaction in records
+ * matches the boolean for whether or not it is a pop in isRecordPop.
+ *
+ * @param records A list to add transactions to.
+ * @param isRecordPop A list to add whether or not the transactions added to records is
+ * a pop transaction.
+ * @return true if something was added or false otherwise.
+ */
+ boolean generateOps(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop);
+ }
+
+ /**
+ * A pop operation OpGenerator. This will be run on the UI thread and will generate the
+ * transactions that will be popped if anything can be popped.
+ */
+ private class PopBackStackState implements OpGenerator {
+ final String mName;
+ final int mId;
+ final int mFlags;
+
+ public PopBackStackState(String name, int id, int flags) {
+ mName = name;
+ mId = id;
+ mFlags = flags;
+ }
+
+ @Override
+ public boolean generateOps(ArrayList<BackStackRecord> records,
+ ArrayList<Boolean> isRecordPop) {
+ return popBackStackState(records, isRecordPop, mName, mId, mFlags);
+ }
+ }
}
diff --git a/core/java/android/app/FragmentTransaction.java b/core/java/android/app/FragmentTransaction.java
index 633e85b..25a7839 100644
--- a/core/java/android/app/FragmentTransaction.java
+++ b/core/java/android/app/FragmentTransaction.java
@@ -260,6 +260,32 @@
public abstract FragmentTransaction setBreadCrumbShortTitle(CharSequence text);
/**
+ * Sets whether or not to allow optimizing operations within and across
+ * transactions. Optimizing fragment transaction's operations can eliminate
+ * operations that cancel. For example, if two transactions are executed
+ * together, one that adds a fragment A and the next replaces it with fragment B,
+ * the operations will cancel and only fragment B will be added. That means that
+ * fragment A may not go through the creation/destruction lifecycle.
+ * <p>
+ * The side effect of optimization is that fragments may have state changes
+ * out of the expected order. For example, one transaction adds fragment A,
+ * a second adds fragment B, then a third removes fragment A. Without optimization,
+ * fragment B could expect that while it is being created, fragment A will also
+ * exist because fragment A will be removed after fragment B was added.
+ * With optimization, fragment B cannot expect fragment A to exist when
+ * it has been created because fragment A's add/remove will be optimized out.
+ * <p>
+ * The default is {@code false} for applications targeting version
+ * versions prior to O and {@code true} for applications targeting O and
+ * later.
+ *
+ * @param allowOptimization {@code true} to enable optimizing operations
+ * or {@code false} to disable optimizing
+ * operations on this transaction.
+ */
+ public abstract FragmentTransaction setAllowOptimization(boolean allowOptimization);
+
+ /**
* Schedules a commit of this transaction. The commit does
* not happen immediately; it will be scheduled as work on the main thread
* to be done the next time that thread is ready.