Primary navigation fragments

Port of I82cd22cda6e9ee22fe293c36859e99f49d36a129 from frameworks/support

Add API to track the "primary navigation fragment" for a
FragmentManager. This allows designating one active fragment in a
FragmentManager as the intended nested recipient of navigation actions
such as popping the back stack.

The FragmentManager popBackStack methods will now delegate to the
child FragmentManager of the current primary navigation fragment first
before trying to pop its own back stack. This can only happen if a
back stack transaction id or back stack entry name is not provided to
the pop operation, as these names and ids are namespaced to each
individual FragmentManager.

Primary navigation fragments are set as part of a FragmentTransaction
and follow the associated expectations. Transactions added to the back
stack will be fully reversed when popped, restoring the previous
primary navigation fragment for the FragmentManager. If a primary
navigation fragment is removed from the FragmentManager, the primary
navigation fragment will be set to null.

Bug: 30596279
Test: CTS
Change-Id: Ied7c1515f27142d6f4cd523f16eb35d38a2a2380
This commit is contained in:
Adam Powell
2017-01-27 16:51:03 -08:00
parent 39cb8895a5
commit 5f3a05c15f
7 changed files with 203 additions and 58 deletions

View File

@@ -4684,6 +4684,7 @@ package android.app {
method public abstract android.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
method public abstract int getBackStackEntryCount();
method public abstract android.app.Fragment getFragment(android.os.Bundle, java.lang.String);
method public abstract android.app.Fragment getPrimaryNavigationFragment();
method public void invalidateOptionsMenu();
method public abstract boolean isDestroyed();
method public abstract void popBackStack();
@@ -4760,6 +4761,7 @@ package android.app {
method public abstract android.app.FragmentTransaction setBreadCrumbTitle(java.lang.CharSequence);
method public abstract android.app.FragmentTransaction setCustomAnimations(int, int);
method public abstract android.app.FragmentTransaction setCustomAnimations(int, int, int, int);
method public abstract android.app.FragmentTransaction setPrimaryNavigationFragment(android.app.Fragment);
method public abstract android.app.FragmentTransaction setTransition(int);
method public abstract android.app.FragmentTransaction setTransitionStyle(int);
method public abstract android.app.FragmentTransaction show(android.app.Fragment);

View File

@@ -4844,6 +4844,7 @@ package android.app {
method public abstract android.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
method public abstract int getBackStackEntryCount();
method public abstract android.app.Fragment getFragment(android.os.Bundle, java.lang.String);
method public abstract android.app.Fragment getPrimaryNavigationFragment();
method public void invalidateOptionsMenu();
method public abstract boolean isDestroyed();
method public abstract void popBackStack();
@@ -4920,6 +4921,7 @@ package android.app {
method public abstract android.app.FragmentTransaction setBreadCrumbTitle(java.lang.CharSequence);
method public abstract android.app.FragmentTransaction setCustomAnimations(int, int);
method public abstract android.app.FragmentTransaction setCustomAnimations(int, int, int, int);
method public abstract android.app.FragmentTransaction setPrimaryNavigationFragment(android.app.Fragment);
method public abstract android.app.FragmentTransaction setTransition(int);
method public abstract android.app.FragmentTransaction setTransitionStyle(int);
method public abstract android.app.FragmentTransaction show(android.app.Fragment);

View File

@@ -4694,6 +4694,7 @@ package android.app {
method public abstract android.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
method public abstract int getBackStackEntryCount();
method public abstract android.app.Fragment getFragment(android.os.Bundle, java.lang.String);
method public abstract android.app.Fragment getPrimaryNavigationFragment();
method public void invalidateOptionsMenu();
method public abstract boolean isDestroyed();
method public abstract void popBackStack();
@@ -4770,6 +4771,7 @@ package android.app {
method public abstract android.app.FragmentTransaction setBreadCrumbTitle(java.lang.CharSequence);
method public abstract android.app.FragmentTransaction setCustomAnimations(int, int);
method public abstract android.app.FragmentTransaction setCustomAnimations(int, int, int, int);
method public abstract android.app.FragmentTransaction setPrimaryNavigationFragment(android.app.Fragment);
method public abstract android.app.FragmentTransaction setTransition(int);
method public abstract android.app.FragmentTransaction setTransitionStyle(int);
method public abstract android.app.FragmentTransaction show(android.app.Fragment);

View File

@@ -186,6 +186,8 @@ final class BackStackRecord extends FragmentTransaction implements
static final int OP_SHOW = 5;
static final int OP_DETACH = 6;
static final int OP_ATTACH = 7;
static final int OP_SET_PRIMARY_NAV = 8;
static final int OP_UNSET_PRIMARY_NAV = 9;
static final class Op {
int cmd;
@@ -194,6 +196,14 @@ final class BackStackRecord extends FragmentTransaction implements
int exitAnim;
int popEnterAnim;
int popExitAnim;
Op() {
}
Op(int cmd, Fragment fragment) {
this.cmd = cmd;
this.fragment = fragment;
}
}
ArrayList<Op> mOps = new ArrayList<>();
@@ -318,6 +328,13 @@ final class BackStackRecord extends FragmentTransaction implements
case OP_ATTACH:
cmdStr = "ATTACH";
break;
case OP_SET_PRIMARY_NAV:
cmdStr="SET_PRIMARY_NAV";
break;
case OP_UNSET_PRIMARY_NAV:
cmdStr="UNSET_PRIMARY_NAV";
break;
default:
cmdStr = "cmd=" + op.cmd;
break;
@@ -438,10 +455,7 @@ final class BackStackRecord extends FragmentTransaction implements
fragment.mContainerId = fragment.mFragmentId = containerViewId;
}
Op op = new Op();
op.cmd = opcmd;
op.fragment = fragment;
addOp(op);
addOp(new Op(opcmd, fragment));
}
public FragmentTransaction replace(int containerViewId, Fragment fragment) {
@@ -458,46 +472,37 @@ final class BackStackRecord extends FragmentTransaction implements
}
public FragmentTransaction remove(Fragment fragment) {
Op op = new Op();
op.cmd = OP_REMOVE;
op.fragment = fragment;
addOp(op);
addOp(new Op(OP_REMOVE, fragment));
return this;
}
public FragmentTransaction hide(Fragment fragment) {
Op op = new Op();
op.cmd = OP_HIDE;
op.fragment = fragment;
addOp(op);
addOp(new Op(OP_HIDE, fragment));
return this;
}
public FragmentTransaction show(Fragment fragment) {
Op op = new Op();
op.cmd = OP_SHOW;
op.fragment = fragment;
addOp(op);
addOp(new Op(OP_SHOW, fragment));
return this;
}
public FragmentTransaction detach(Fragment fragment) {
Op op = new Op();
op.cmd = OP_DETACH;
op.fragment = fragment;
addOp(op);
addOp(new Op(OP_DETACH, fragment));
return this;
}
public FragmentTransaction attach(Fragment fragment) {
Op op = new Op();
op.cmd = OP_ATTACH;
op.fragment = fragment;
addOp(op);
addOp(new Op(OP_ATTACH, fragment));
return this;
}
public FragmentTransaction setPrimaryNavigationFragment(Fragment fragment) {
addOp(new Op(OP_SET_PRIMARY_NAV, fragment));
return this;
}
@@ -689,7 +694,8 @@ final class BackStackRecord extends FragmentTransaction implements
final int numOps = mOps.size();
for (int opNum = 0; opNum < numOps; opNum++) {
final Op op = mOps.get(opNum);
if (op.fragment.mContainerId == containerId) {
final int fragContainer = op.fragment != null ? op.fragment.mContainerId : 0;
if (fragContainer != 0 && fragContainer == containerId) {
return true;
}
}
@@ -704,7 +710,7 @@ final class BackStackRecord extends FragmentTransaction implements
int lastContainer = -1;
for (int opNum = 0; opNum < numOps; opNum++) {
final Op op = mOps.get(opNum);
final int container = op.fragment.mContainerId;
final int container = op.fragment != null ? op.fragment.mContainerId : 0;
if (container != 0 && container != lastContainer) {
lastContainer = container;
for (int i = startIndex; i < endIndex; i++) {
@@ -712,7 +718,9 @@ final class BackStackRecord extends FragmentTransaction implements
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) {
final int thatContainer = thatOp.fragment != null
? thatOp.fragment.mContainerId : 0;
if (thatContainer == container) {
return true;
}
}
@@ -731,7 +739,9 @@ 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.setNextTransition(mTransition, mTransitionStyle);
if (f != null) {
f.setNextTransition(mTransition, mTransitionStyle);
}
switch (op.cmd) {
case OP_ADD:
f.setNextAnim(op.enterAnim);
@@ -757,10 +767,16 @@ final class BackStackRecord extends FragmentTransaction implements
f.setNextAnim(op.enterAnim);
mManager.attachFragment(f);
break;
case OP_SET_PRIMARY_NAV:
mManager.setPrimaryNavigationFragment(f);
break;
case OP_UNSET_PRIMARY_NAV:
mManager.setPrimaryNavigationFragment(null);
break;
default:
throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
}
if (!mAllowOptimization && op.cmd != OP_ADD) {
if (!mAllowOptimization && op.cmd != OP_ADD && f != null) {
mManager.moveFragmentToExpectedState(f);
}
}
@@ -778,7 +794,10 @@ 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.setNextTransition(FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle);
if (f != null) {
f.setNextTransition(FragmentManagerImpl.reverseTransit(mTransition),
mTransitionStyle);
}
switch (op.cmd) {
case OP_ADD:
f.setNextAnim(op.popExitAnim);
@@ -804,10 +823,16 @@ final class BackStackRecord extends FragmentTransaction implements
f.setNextAnim(op.popExitAnim);
mManager.detachFragment(f);
break;
case OP_SET_PRIMARY_NAV:
mManager.setPrimaryNavigationFragment(null);
break;
case OP_UNSET_PRIMARY_NAV:
mManager.setPrimaryNavigationFragment(f);
break;
default:
throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
}
if (!mAllowOptimization && op.cmd != OP_REMOVE) {
if (!mAllowOptimization && op.cmd != OP_REMOVE && f != null) {
mManager.moveFragmentToExpectedState(f);
}
}
@@ -817,15 +842,28 @@ final class BackStackRecord extends FragmentTransaction implements
}
/**
* 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.
* Expands all meta-ops into their more primitive equivalents. This must be called prior to
* {@link #executeOps()} or any other call that operations on mOps for forward navigation.
* It should not be called for pop/reverse navigation operations.
*
* <p>Removes all OP_REPLACE ops and replaces them with the proper add and remove
* operations that are equivalent to the replace.</p>
*
* <p>Adds OP_UNSET_PRIMARY_NAV ops to match OP_SET_PRIMARY_NAV, OP_REMOVE and OP_DETACH
* ops so that we can restore the old primary nav fragment later. Since callers call this
* method in a loop before running ops from several transactions at once, the caller should
* pass the return value from this method as the oldPrimaryNav parameter for the next call.
* The first call in such a loop should pass the value of
* {@link FragmentManager#getPrimaryNavigationFragment()}.</p>
*
* @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()}.
* @param oldPrimaryNav The tracked primary navigation fragment as of the beginning of
* this set of ops
* @return the new oldPrimaryNav fragment after this record's ops would be run
*/
void expandReplaceOps(ArrayList<Fragment> added) {
Fragment expandOps(ArrayList<Fragment> added, Fragment oldPrimaryNav) {
for (int opNum = 0; opNum < mOps.size(); opNum++) {
final Op op = mOps.get(opNum);
switch (op.cmd) {
@@ -834,22 +872,33 @@ final class BackStackRecord extends FragmentTransaction implements
added.add(op.fragment);
break;
case OP_REMOVE:
case OP_DETACH:
case OP_DETACH: {
added.remove(op.fragment);
break;
if (op.fragment == oldPrimaryNav) {
mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, op.fragment));
opNum++;
oldPrimaryNav = null;
}
}
break;
case OP_REPLACE: {
Fragment f = op.fragment;
int containerId = f.mContainerId;
final Fragment f = op.fragment;
final int containerId = f.mContainerId;
boolean alreadyAdded = false;
for (int i = added.size() - 1; i >= 0; i--) {
Fragment old = added.get(i);
final Fragment old = added.get(i);
if (old.mContainerId == containerId) {
if (old == f) {
alreadyAdded = true;
} else {
Op removeOp = new Op();
removeOp.cmd = OP_REMOVE;
removeOp.fragment = old;
// This is duplicated from above since we only make
// a single pass for expanding ops. Unset any outgoing primary nav.
if (old == oldPrimaryNav) {
mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, old));
opNum++;
oldPrimaryNav = null;
}
final Op removeOp = new Op(OP_REMOVE, old);
removeOp.enterAnim = op.enterAnim;
removeOp.popEnterAnim = op.popEnterAnim;
removeOp.exitAnim = op.exitAnim;
@@ -869,8 +918,18 @@ final class BackStackRecord extends FragmentTransaction implements
}
}
break;
case OP_SET_PRIMARY_NAV: {
// It's ok if this is null, that means we will restore to no active
// primary navigation fragment on a pop.
mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, oldPrimaryNav));
opNum++;
// Will be set by the OP_SET_PRIMARY_NAV we inserted before when run
oldPrimaryNav = op.fragment;
}
break;
}
}
return oldPrimaryNav;
}
/**
@@ -917,8 +976,8 @@ final class BackStackRecord extends FragmentTransaction implements
private static boolean isFragmentPostponed(Op op) {
final Fragment fragment = op.fragment;
return (fragment.mAdded && fragment.mView != null && !fragment.mDetached &&
!fragment.mHidden && fragment.isPostponed());
return fragment != null && fragment.mAdded && fragment.mView != null && !fragment.mDetached
&& !fragment.mHidden && fragment.isPostponed();
}
public String getName() {

View File

@@ -359,6 +359,18 @@ public abstract class FragmentManager {
*/
public abstract void unregisterFragmentLifecycleCallbacks(FragmentLifecycleCallbacks cb);
/**
* Return the currently active primary navigation fragment for this FragmentManager.
*
* <p>The primary navigation fragment's
* {@link Fragment#getChildFragmentManager() child FragmentManager} will be called first
* to process delegated navigation actions such as {@link #popBackStack()} if no ID
* or transaction name is provided to pop to.</p>
*
* @return the fragment designated as the primary navigation fragment
*/
public abstract Fragment getPrimaryNavigationFragment();
/**
* Print the FragmentManager's state into the given stream.
*
@@ -524,6 +536,7 @@ final class FragmentManagerState implements Parcelable {
FragmentState[] mActive;
int[] mAdded;
BackStackState[] mBackStack;
int mPrimaryNavActiveIndex = -1;
public FragmentManagerState() {
}
@@ -532,6 +545,7 @@ final class FragmentManagerState implements Parcelable {
mActive = in.createTypedArray(FragmentState.CREATOR);
mAdded = in.createIntArray();
mBackStack = in.createTypedArray(BackStackState.CREATOR);
mPrimaryNavActiveIndex = in.readInt();
}
public int describeContents() {
@@ -542,6 +556,7 @@ final class FragmentManagerState implements Parcelable {
dest.writeTypedArray(mActive, flags);
dest.writeIntArray(mAdded);
dest.writeTypedArray(mBackStack, flags);
dest.writeInt(mPrimaryNavActiveIndex);
}
public static final Parcelable.Creator<FragmentManagerState> CREATOR
@@ -626,6 +641,7 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate
FragmentHostCallback<?> mHost;
FragmentContainer mContainer;
Fragment mParent;
Fragment mPrimaryNav;
boolean mNeedMenuInvalidate;
boolean mStateSaved;
@@ -783,6 +799,16 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate
execPendingActions();
ensureExecReady(true);
if (mPrimaryNav != null // We have a primary nav fragment
&& id < 0 // No valid id (since they're local)
&& name == null) { // no name to pop to (since they're local)
final FragmentManager childManager = mPrimaryNav.mChildFragmentManager;
if (childManager != null && childManager.popBackStackImmediate()) {
// We did something, just not to this specific FragmentManager. Return true.
return true;
}
}
boolean executePop = popBackStackState(mTmpRecords, mTmpIsPop, name, id, flags);
if (executePop) {
mExecutingActions = true;
@@ -2038,11 +2064,12 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate
if (mAdded != null) {
mTmpAddedFragments.addAll(mAdded);
}
Fragment oldPrimaryNav = getPrimaryNavigationFragment();
for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
final BackStackRecord record = records.get(recordNum);
final boolean isPop = isRecordPop.get(recordNum);
if (!isPop) {
record.expandReplaceOps(mTmpAddedFragments);
oldPrimaryNav = record.expandOps(mTmpAddedFragments, oldPrimaryNav);
} else {
record.trackAddedFragmentsInPop(mTmpAddedFragments);
}
@@ -2318,20 +2345,20 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate
*/
private boolean generateOpsForPendingActions(ArrayList<BackStackRecord> records,
ArrayList<Boolean> isPop) {
int numActions;
boolean didSomething = false;
synchronized (this) {
if (mPendingActions == null || mPendingActions.size() == 0) {
return false;
}
numActions = mPendingActions.size();
final int numActions = mPendingActions.size();
for (int i = 0; i < numActions; i++) {
mPendingActions.get(i).generateOps(records, isPop);
didSomething |= mPendingActions.get(i).generateOps(records, isPop);
}
mPendingActions.clear();
mHost.getHandler().removeCallbacks(mExecCommit);
}
return numActions > 0;
return didSomething;
}
void doPendingDeferredStart() {
@@ -2618,6 +2645,9 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate
fms.mActive = active;
fms.mAdded = added;
fms.mBackStack = backStack;
if (mPrimaryNav != null) {
fms.mPrimaryNavActiveIndex = mPrimaryNav.mIndex;
}
return fms;
}
@@ -2744,6 +2774,10 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate
} else {
mBackStack = null;
}
if (fms.mPrimaryNavActiveIndex >= 0) {
mPrimaryNav = mActive.get(fms.mPrimaryNavActiveIndex);
}
}
public void attachController(FragmentHostCallback<?> host, FragmentContainer container,
@@ -2942,6 +2976,19 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate
}
}
public void setPrimaryNavigationFragment(Fragment f) {
if (f != null && (f.getFragmentManager() != this || f.mIndex >= mActive.size()
|| mActive.get(f.mIndex) != f)) {
throw new IllegalArgumentException("Fragment " + f
+ " is not an active fragment of FragmentManager " + this);
}
mPrimaryNav = f;
}
public Fragment getPrimaryNavigationFragment() {
return mPrimaryNav;
}
public void registerFragmentLifecycleCallbacks(FragmentLifecycleCallbacks cb,
boolean recursive) {
if (mLifecycleCallbacks == null) {
@@ -3386,6 +3433,16 @@ final class FragmentManagerImpl extends FragmentManager implements LayoutInflate
@Override
public boolean generateOps(ArrayList<BackStackRecord> records,
ArrayList<Boolean> isRecordPop) {
if (mPrimaryNav != null // We have a primary nav fragment
&& mId < 0 // No valid id (since they're local)
&& mName == null) { // no name to pop to (since they're local)
final FragmentManager childManager = mPrimaryNav.mChildFragmentManager;
if (childManager != null && childManager.popBackStackImmediate()) {
// We didn't add any operations for this FragmentManager even though
// a child did do work.
return false;
}
}
return popBackStackState(records, isRecordPop, mName, mId, mFlags);
}
}

View File

@@ -131,6 +131,24 @@ public abstract class FragmentTransaction {
*/
public abstract FragmentTransaction attach(Fragment fragment);
/**
* Set a currently active fragment in this FragmentManager as the primary navigation fragment.
*
* <p>The primary navigation fragment's
* {@link Fragment#getChildFragmentManager() child FragmentManager} will be called first
* to process delegated navigation actions such as {@link FragmentManager#popBackStack()}
* if no ID or transaction name is provided to pop to. Navigation operations outside of the
* fragment system may choose to delegate those actions to the primary navigation fragment
* as returned by {@link FragmentManager#getPrimaryNavigationFragment()}.</p>
*
* <p>The fragment provided must currently be added to the FragmentManager to be set as
* a primary navigation fragment, or previously added as part of this transaction.</p>
*
* @param fragment the fragment to set as the primary navigation fragment
* @return the same FragmentTransaction instance
*/
public abstract FragmentTransaction setPrimaryNavigationFragment(Fragment fragment);
/**
* @return <code>true</code> if this transaction contains no operations,
* <code>false</code> otherwise.

View File

@@ -43,14 +43,16 @@ class FragmentTransition {
* REPLACE operations have already been replaced by add/remove operations.
*/
private static final int[] INVERSE_OPS = {
BackStackRecord.OP_NULL, // inverse of OP_NULL (error)
BackStackRecord.OP_REMOVE, // inverse of OP_ADD
BackStackRecord.OP_NULL, // inverse of OP_REPLACE (error)
BackStackRecord.OP_ADD, // inverse of OP_REMOVE
BackStackRecord.OP_SHOW, // inverse of OP_HIDE
BackStackRecord.OP_HIDE, // inverse of OP_SHOW
BackStackRecord.OP_ATTACH, // inverse of OP_DETACH
BackStackRecord.OP_DETACH, // inverse of OP_ATTACH
BackStackRecord.OP_NULL, // inverse of OP_NULL (error)
BackStackRecord.OP_REMOVE, // inverse of OP_ADD
BackStackRecord.OP_NULL, // inverse of OP_REPLACE (error)
BackStackRecord.OP_ADD, // inverse of OP_REMOVE
BackStackRecord.OP_SHOW, // inverse of OP_HIDE
BackStackRecord.OP_HIDE, // inverse of OP_SHOW
BackStackRecord.OP_ATTACH, // inverse of OP_DETACH
BackStackRecord.OP_DETACH, // inverse of OP_ATTACH
BackStackRecord.OP_UNSET_PRIMARY_NAV, // inverse of OP_SET_PRIMARY_NAV
BackStackRecord.OP_SET_PRIMARY_NAV, // inverse of OP_UNSET_PRIMARY_NAV
};
/**
@@ -1232,6 +1234,9 @@ class FragmentTransition {
SparseArray<FragmentContainerTransition> transitioningFragments, boolean isPop,
boolean isOptimizedTransaction) {
final Fragment fragment = op.fragment;
if (fragment == null) {
return; // no fragment, no transition
}
final int containerId = fragment.mContainerId;
if (containerId == 0) {
return; // no container, no transition