/* * Copyright (C) 2010 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 com.android.internal.util.FastPrintWriter; import android.graphics.Rect; 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.transition.TransitionUtils; import android.util.ArrayMap; import android.util.Log; import android.util.LogWriter; import android.util.Pair; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; final class BackStackState implements Parcelable { final int[] mOps; final int mTransition; final int mTransitionStyle; final String mName; final int mIndex; final int mBreadCrumbTitleRes; final CharSequence mBreadCrumbTitleText; final int mBreadCrumbShortTitleRes; final CharSequence mBreadCrumbShortTitleText; final ArrayList mSharedElementSourceNames; final ArrayList mSharedElementTargetNames; public BackStackState(FragmentManagerImpl fm, BackStackRecord bse) { int numRemoved = 0; BackStackRecord.Op op = bse.mHead; while (op != null) { if (op.removed != null) { numRemoved += op.removed.size(); } op = op.next; } mOps = new int[bse.mNumOp * 7 + numRemoved]; if (!bse.mAddToBackStack) { throw new IllegalStateException("Not on back stack"); } op = bse.mHead; int pos = 0; while (op != null) { mOps[pos++] = op.cmd; mOps[pos++] = op.fragment != null ? op.fragment.mIndex : -1; mOps[pos++] = op.enterAnim; mOps[pos++] = op.exitAnim; mOps[pos++] = op.popEnterAnim; mOps[pos++] = op.popExitAnim; if (op.removed != null) { final int N = op.removed.size(); mOps[pos++] = N; for (int i = 0; i < N; i++) { mOps[pos++] = op.removed.get(i).mIndex; } } else { mOps[pos++] = 0; } op = op.next; } mTransition = bse.mTransition; mTransitionStyle = bse.mTransitionStyle; mName = bse.mName; mIndex = bse.mIndex; mBreadCrumbTitleRes = bse.mBreadCrumbTitleRes; mBreadCrumbTitleText = bse.mBreadCrumbTitleText; mBreadCrumbShortTitleRes = bse.mBreadCrumbShortTitleRes; mBreadCrumbShortTitleText = bse.mBreadCrumbShortTitleText; mSharedElementSourceNames = bse.mSharedElementSourceNames; mSharedElementTargetNames = bse.mSharedElementTargetNames; } public BackStackState(Parcel in) { mOps = in.createIntArray(); mTransition = in.readInt(); mTransitionStyle = in.readInt(); mName = in.readString(); mIndex = in.readInt(); mBreadCrumbTitleRes = in.readInt(); mBreadCrumbTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); mBreadCrumbShortTitleRes = in.readInt(); mBreadCrumbShortTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); mSharedElementSourceNames = in.createStringArrayList(); mSharedElementTargetNames = in.createStringArrayList(); } public BackStackRecord instantiate(FragmentManagerImpl fm) { BackStackRecord bse = new BackStackRecord(fm); int pos = 0; int num = 0; while (pos < mOps.length) { BackStackRecord.Op op = new BackStackRecord.Op(); op.cmd = mOps[pos++]; if (FragmentManagerImpl.DEBUG) { Log.v(FragmentManagerImpl.TAG, "Instantiate " + bse + " op #" + num + " base fragment #" + mOps[pos]); } int findex = mOps[pos++]; if (findex >= 0) { Fragment f = fm.mActive.get(findex); op.fragment = f; } else { op.fragment = null; } op.enterAnim = mOps[pos++]; op.exitAnim = mOps[pos++]; op.popEnterAnim = mOps[pos++]; op.popExitAnim = mOps[pos++]; final int N = mOps[pos++]; if (N > 0) { op.removed = new ArrayList(N); for (int i = 0; i < N; i++) { if (FragmentManagerImpl.DEBUG) { Log.v(FragmentManagerImpl.TAG, "Instantiate " + bse + " set remove fragment #" + mOps[pos]); } Fragment r = fm.mActive.get(mOps[pos++]); op.removed.add(r); } } bse.addOp(op); num++; } bse.mTransition = mTransition; bse.mTransitionStyle = mTransitionStyle; bse.mName = mName; bse.mIndex = mIndex; bse.mAddToBackStack = true; bse.mBreadCrumbTitleRes = mBreadCrumbTitleRes; bse.mBreadCrumbTitleText = mBreadCrumbTitleText; bse.mBreadCrumbShortTitleRes = mBreadCrumbShortTitleRes; bse.mBreadCrumbShortTitleText = mBreadCrumbShortTitleText; bse.mSharedElementSourceNames = mSharedElementSourceNames; bse.mSharedElementTargetNames = mSharedElementTargetNames; bse.bumpBackStackNesting(1); return bse; } public int describeContents() { return 0; } public void writeToParcel(Parcel dest, int flags) { dest.writeIntArray(mOps); dest.writeInt(mTransition); dest.writeInt(mTransitionStyle); dest.writeString(mName); dest.writeInt(mIndex); dest.writeInt(mBreadCrumbTitleRes); TextUtils.writeToParcel(mBreadCrumbTitleText, dest, 0); dest.writeInt(mBreadCrumbShortTitleRes); TextUtils.writeToParcel(mBreadCrumbShortTitleText, dest, 0); dest.writeStringList(mSharedElementSourceNames); dest.writeStringList(mSharedElementTargetNames); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public BackStackState createFromParcel(Parcel in) { return new BackStackState(in); } public BackStackState[] newArray(int size) { return new BackStackState[size]; } }; } /** * @hide Entry of an operation on the fragment back stack. */ final class BackStackRecord extends FragmentTransaction implements FragmentManager.BackStackEntry, Runnable { static final String TAG = FragmentManagerImpl.TAG; final FragmentManagerImpl mManager; static final int OP_NULL = 0; static final int OP_ADD = 1; static final int OP_REPLACE = 2; static final int OP_REMOVE = 3; static final int OP_HIDE = 4; static final int OP_SHOW = 5; static final int OP_DETACH = 6; static final int OP_ATTACH = 7; static final class Op { Op next; Op prev; int cmd; Fragment fragment; int enterAnim; int exitAnim; int popEnterAnim; int popExitAnim; ArrayList removed; } Op mHead; Op mTail; int mNumOp; int mEnterAnim; int mExitAnim; int mPopEnterAnim; int mPopExitAnim; int mTransition; int mTransitionStyle; boolean mAddToBackStack; boolean mAllowAddToBackStack = true; String mName; boolean mCommitted; int mIndex = -1; int mBreadCrumbTitleRes; CharSequence mBreadCrumbTitleText; int mBreadCrumbShortTitleRes; CharSequence mBreadCrumbShortTitleText; ArrayList mSharedElementSourceNames; ArrayList mSharedElementTargetNames; @Override public String toString() { StringBuilder sb = new StringBuilder(128); sb.append("BackStackEntry{"); sb.append(Integer.toHexString(System.identityHashCode(this))); if (mIndex >= 0) { sb.append(" #"); sb.append(mIndex); } if (mName != null) { sb.append(" "); sb.append(mName); } sb.append("}"); return sb.toString(); } public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { dump(prefix, writer, true); } void dump(String prefix, PrintWriter writer, boolean full) { if (full) { writer.print(prefix); writer.print("mName="); writer.print(mName); writer.print(" mIndex="); writer.print(mIndex); writer.print(" mCommitted="); writer.println(mCommitted); if (mTransition != FragmentTransaction.TRANSIT_NONE) { writer.print(prefix); writer.print("mTransition=#"); writer.print(Integer.toHexString(mTransition)); writer.print(" mTransitionStyle=#"); writer.println(Integer.toHexString(mTransitionStyle)); } if (mEnterAnim != 0 || mExitAnim != 0) { writer.print(prefix); writer.print("mEnterAnim=#"); writer.print(Integer.toHexString(mEnterAnim)); writer.print(" mExitAnim=#"); writer.println(Integer.toHexString(mExitAnim)); } if (mPopEnterAnim != 0 || mPopExitAnim != 0) { writer.print(prefix); writer.print("mPopEnterAnim=#"); writer.print(Integer.toHexString(mPopEnterAnim)); writer.print(" mPopExitAnim=#"); writer.println(Integer.toHexString(mPopExitAnim)); } if (mBreadCrumbTitleRes != 0 || mBreadCrumbTitleText != null) { writer.print(prefix); writer.print("mBreadCrumbTitleRes=#"); writer.print(Integer.toHexString(mBreadCrumbTitleRes)); writer.print(" mBreadCrumbTitleText="); writer.println(mBreadCrumbTitleText); } if (mBreadCrumbShortTitleRes != 0 || mBreadCrumbShortTitleText != null) { writer.print(prefix); writer.print("mBreadCrumbShortTitleRes=#"); writer.print(Integer.toHexString(mBreadCrumbShortTitleRes)); writer.print(" mBreadCrumbShortTitleText="); writer.println(mBreadCrumbShortTitleText); } } if (mHead != null) { writer.print(prefix); writer.println("Operations:"); String innerPrefix = prefix + " "; Op op = mHead; int num = 0; while (op != null) { String cmdStr; switch (op.cmd) { case OP_NULL: cmdStr = "NULL"; break; case OP_ADD: cmdStr = "ADD"; break; case OP_REPLACE: cmdStr = "REPLACE"; break; case OP_REMOVE: cmdStr = "REMOVE"; break; case OP_HIDE: cmdStr = "HIDE"; break; case OP_SHOW: cmdStr = "SHOW"; break; case OP_DETACH: cmdStr = "DETACH"; break; case OP_ATTACH: cmdStr = "ATTACH"; break; default: cmdStr = "cmd=" + op.cmd; break; } writer.print(prefix); writer.print(" Op #"); writer.print(num); writer.print(": "); writer.print(cmdStr); writer.print(" "); writer.println(op.fragment); if (full) { if (op.enterAnim != 0 || op.exitAnim != 0) { writer.print(innerPrefix); writer.print("enterAnim=#"); writer.print(Integer.toHexString(op.enterAnim)); writer.print(" exitAnim=#"); writer.println(Integer.toHexString(op.exitAnim)); } if (op.popEnterAnim != 0 || op.popExitAnim != 0) { writer.print(innerPrefix); writer.print("popEnterAnim=#"); writer.print(Integer.toHexString(op.popEnterAnim)); writer.print(" popExitAnim=#"); writer.println(Integer.toHexString(op.popExitAnim)); } } if (op.removed != null && op.removed.size() > 0) { for (int i = 0; i < op.removed.size(); i++) { writer.print(innerPrefix); if (op.removed.size() == 1) { writer.print("Removed: "); } else { if (i == 0) { writer.println("Removed:"); } writer.print(innerPrefix); writer.print(" #"); writer.print(i); writer.print(": "); } writer.println(op.removed.get(i)); } } op = op.next; num++; } } } public BackStackRecord(FragmentManagerImpl manager) { mManager = manager; } public int getId() { return mIndex; } public int getBreadCrumbTitleRes() { return mBreadCrumbTitleRes; } public int getBreadCrumbShortTitleRes() { return mBreadCrumbShortTitleRes; } public CharSequence getBreadCrumbTitle() { if (mBreadCrumbTitleRes != 0) { return mManager.mActivity.getText(mBreadCrumbTitleRes); } return mBreadCrumbTitleText; } public CharSequence getBreadCrumbShortTitle() { if (mBreadCrumbShortTitleRes != 0) { return mManager.mActivity.getText(mBreadCrumbShortTitleRes); } return mBreadCrumbShortTitleText; } void addOp(Op op) { if (mHead == null) { mHead = mTail = op; } else { op.prev = mTail; mTail.next = op; mTail = op; } op.enterAnim = mEnterAnim; op.exitAnim = mExitAnim; op.popEnterAnim = mPopEnterAnim; op.popExitAnim = mPopExitAnim; mNumOp++; } public FragmentTransaction add(Fragment fragment, String tag) { doAddOp(0, fragment, tag, OP_ADD); return this; } public FragmentTransaction add(int containerViewId, Fragment fragment) { doAddOp(containerViewId, fragment, null, OP_ADD); return this; } public FragmentTransaction add(int containerViewId, Fragment fragment, String tag) { doAddOp(containerViewId, fragment, tag, OP_ADD); return this; } private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) { fragment.mFragmentManager = mManager; if (tag != null) { if (fragment.mTag != null && !tag.equals(fragment.mTag)) { throw new IllegalStateException("Can't change tag of fragment " + fragment + ": was " + fragment.mTag + " now " + tag); } fragment.mTag = tag; } if (containerViewId != 0) { if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) { throw new IllegalStateException("Can't change container ID of fragment " + fragment + ": was " + fragment.mFragmentId + " now " + containerViewId); } fragment.mContainerId = fragment.mFragmentId = containerViewId; } Op op = new Op(); op.cmd = opcmd; op.fragment = fragment; addOp(op); } public FragmentTransaction replace(int containerViewId, Fragment fragment) { return replace(containerViewId, fragment, null); } public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) { if (containerViewId == 0) { throw new IllegalArgumentException("Must use non-zero containerViewId"); } doAddOp(containerViewId, fragment, tag, OP_REPLACE); return this; } public FragmentTransaction remove(Fragment fragment) { Op op = new Op(); op.cmd = OP_REMOVE; op.fragment = fragment; addOp(op); return this; } public FragmentTransaction hide(Fragment fragment) { Op op = new Op(); op.cmd = OP_HIDE; op.fragment = fragment; addOp(op); return this; } public FragmentTransaction show(Fragment fragment) { Op op = new Op(); op.cmd = OP_SHOW; op.fragment = fragment; addOp(op); return this; } public FragmentTransaction detach(Fragment fragment) { Op op = new Op(); op.cmd = OP_DETACH; op.fragment = fragment; addOp(op); return this; } public FragmentTransaction attach(Fragment fragment) { Op op = new Op(); op.cmd = OP_ATTACH; op.fragment = fragment; addOp(op); return this; } public FragmentTransaction setCustomAnimations(int enter, int exit) { return setCustomAnimations(enter, exit, 0, 0); } public FragmentTransaction setCustomAnimations(int enter, int exit, int popEnter, int popExit) { mEnterAnim = enter; mExitAnim = exit; mPopEnterAnim = popEnter; mPopExitAnim = popExit; return this; } public FragmentTransaction setTransition(int transition) { mTransition = transition; return this; } @Override public FragmentTransaction addSharedElement(View sharedElement, String name) { String transitionName = sharedElement.getTransitionName(); if (transitionName == null) { throw new IllegalArgumentException("Unique transitionNames are required for all" + " sharedElements"); } if (mSharedElementSourceNames == null) { mSharedElementSourceNames = new ArrayList(); mSharedElementTargetNames = new ArrayList(); } mSharedElementSourceNames.add(transitionName); mSharedElementTargetNames.add(name); return this; } /** TODO: remove this */ @Override public FragmentTransaction setSharedElement(View sharedElement, String name) { String transitionName = sharedElement.getTransitionName(); if (transitionName == null) { throw new IllegalArgumentException("Unique transitionNames are required for all" + " sharedElements"); } mSharedElementSourceNames = new ArrayList(1); mSharedElementSourceNames.add(transitionName); mSharedElementTargetNames = new ArrayList(1); mSharedElementTargetNames.add(name); return this; } /** TODO: remove this */ @Override public FragmentTransaction setSharedElements(Pair... sharedElements) { if (sharedElements == null || sharedElements.length == 0) { mSharedElementSourceNames = null; mSharedElementTargetNames = null; } else { ArrayList sourceNames = new ArrayList(sharedElements.length); ArrayList targetNames = new ArrayList(sharedElements.length); for (int i = 0; i < sharedElements.length; i++) { String transitionName = sharedElements[i].first.getTransitionName(); if (transitionName == null) { throw new IllegalArgumentException("Unique transitionNames are required for all" + " sharedElements"); } sourceNames.add(transitionName); targetNames.add(sharedElements[i].second); } mSharedElementSourceNames = sourceNames; mSharedElementTargetNames = targetNames; } return this; } public FragmentTransaction setTransitionStyle(int styleRes) { mTransitionStyle = styleRes; return this; } public FragmentTransaction addToBackStack(String name) { if (!mAllowAddToBackStack) { throw new IllegalStateException( "This FragmentTransaction is not allowed to be added to the back stack."); } mAddToBackStack = true; mName = name; return this; } public boolean isAddToBackStackAllowed() { return mAllowAddToBackStack; } public FragmentTransaction disallowAddToBackStack() { if (mAddToBackStack) { throw new IllegalStateException( "This transaction is already being added to the back stack"); } mAllowAddToBackStack = false; return this; } public FragmentTransaction setBreadCrumbTitle(int res) { mBreadCrumbTitleRes = res; mBreadCrumbTitleText = null; return this; } public FragmentTransaction setBreadCrumbTitle(CharSequence text) { mBreadCrumbTitleRes = 0; mBreadCrumbTitleText = text; return this; } public FragmentTransaction setBreadCrumbShortTitle(int res) { mBreadCrumbShortTitleRes = res; mBreadCrumbShortTitleText = null; return this; } public FragmentTransaction setBreadCrumbShortTitle(CharSequence text) { mBreadCrumbShortTitleRes = 0; mBreadCrumbShortTitleText = text; return this; } void bumpBackStackNesting(int amt) { if (!mAddToBackStack) { return; } if (FragmentManagerImpl.DEBUG) { Log.v(TAG, "Bump nesting in " + this + " by " + amt); } Op op = mHead; while (op != null) { if (op.fragment != null) { op.fragment.mBackStackNesting += amt; if (FragmentManagerImpl.DEBUG) { Log.v(TAG, "Bump nesting of " + op.fragment + " to " + op.fragment.mBackStackNesting); } } if (op.removed != null) { for (int i = op.removed.size() - 1; i >= 0; i--) { Fragment r = op.removed.get(i); r.mBackStackNesting += amt; if (FragmentManagerImpl.DEBUG) { Log.v(TAG, "Bump nesting of " + r + " to " + r.mBackStackNesting); } } } op = op.next; } } public int commit() { return commitInternal(false); } public int commitAllowingStateLoss() { return commitInternal(true); } int commitInternal(boolean allowStateLoss) { if (mCommitted) { throw new IllegalStateException("commit already called"); } if (FragmentManagerImpl.DEBUG) { Log.v(TAG, "Commit: " + this); LogWriter logw = new LogWriter(Log.VERBOSE, TAG); PrintWriter pw = new FastPrintWriter(logw, false, 1024); dump(" ", null, pw, null); pw.flush(); } mCommitted = true; if (mAddToBackStack) { mIndex = mManager.allocBackStackIndex(this); } else { mIndex = -1; } mManager.enqueueAction(this, allowStateLoss); return mIndex; } public void run() { if (FragmentManagerImpl.DEBUG) { Log.v(TAG, "Run: " + this); } if (mAddToBackStack) { if (mIndex < 0) { throw new IllegalStateException("addToBackStack() called after commit()"); } } bumpBackStackNesting(1); SparseArray firstOutFragments = new SparseArray(); SparseArray lastInFragments = new SparseArray(); calculateFragments(firstOutFragments, lastInFragments); beginTransition(firstOutFragments, lastInFragments, false); Op op = mHead; while (op != null) { switch (op.cmd) { case OP_ADD: { Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.addFragment(f, false); } break; case OP_REPLACE: { Fragment f = op.fragment; if (mManager.mAdded != null) { for (int i = 0; i < mManager.mAdded.size(); i++) { Fragment old = mManager.mAdded.get(i); if (FragmentManagerImpl.DEBUG) { Log.v(TAG, "OP_REPLACE: adding=" + f + " old=" + old); } if (f == null || old.mContainerId == f.mContainerId) { if (old == f) { op.fragment = f = null; } else { if (op.removed == null) { op.removed = new ArrayList(); } op.removed.add(old); old.mNextAnim = op.exitAnim; if (mAddToBackStack) { old.mBackStackNesting += 1; if (FragmentManagerImpl.DEBUG) { Log.v(TAG, "Bump nesting of " + old + " to " + old.mBackStackNesting); } } mManager.removeFragment(old, mTransition, mTransitionStyle); } } } } if (f != null) { f.mNextAnim = op.enterAnim; mManager.addFragment(f, false); } } break; case OP_REMOVE: { Fragment f = op.fragment; f.mNextAnim = op.exitAnim; mManager.removeFragment(f, mTransition, mTransitionStyle); } break; case OP_HIDE: { Fragment f = op.fragment; f.mNextAnim = op.exitAnim; mManager.hideFragment(f, mTransition, mTransitionStyle); } break; case OP_SHOW: { Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.showFragment(f, mTransition, mTransitionStyle); } break; case OP_DETACH: { Fragment f = op.fragment; f.mNextAnim = op.exitAnim; mManager.detachFragment(f, mTransition, mTransitionStyle); } break; case OP_ATTACH: { Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.attachFragment(f, mTransition, mTransitionStyle); } break; default: { throw new IllegalArgumentException("Unknown cmd: " + op.cmd); } } op = op.next; } mManager.moveToState(mManager.mCurState, mTransition, mTransitionStyle, true); if (mAddToBackStack) { mManager.addBackStackState(this); } } private static void setFirstOut(SparseArray fragments, Fragment fragment) { if (fragment != null) { int containerId = fragment.mContainerId; if (containerId != 0 && !fragment.isHidden() && fragment.isAdded() && fragment.getView() != null && fragments.get(containerId) == null) { fragments.put(containerId, fragment); } } } private void setLastIn(SparseArray fragments, Fragment fragment) { if (fragment != null) { int containerId = fragment.mContainerId; if (containerId != 0) { fragments.put(containerId, fragment); } } } /** * 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 firstOutFragments The list of first fragments to be removed, keyed on the * container ID. This list will be modified by the method. * @param lastInFragments The list of last fragments to be added, keyed on the * container ID. This list will be modified by the method. */ private void calculateFragments(SparseArray firstOutFragments, SparseArray lastInFragments) { if (!mManager.mContainer.hasView()) { return; // nothing to see, so no transitions } Op op = mHead; while (op != null) { switch (op.cmd) { case OP_ADD: setLastIn(lastInFragments, op.fragment); break; case OP_REPLACE: { Fragment f = op.fragment; if (mManager.mAdded != null) { for (int i = 0; i < mManager.mAdded.size(); i++) { Fragment old = mManager.mAdded.get(i); if (f == null || old.mContainerId == f.mContainerId) { if (old == f) { f = null; } else { setFirstOut(firstOutFragments, old); } } } } setLastIn(lastInFragments, f); break; } case OP_REMOVE: setFirstOut(firstOutFragments, op.fragment); break; case OP_HIDE: setFirstOut(firstOutFragments, op.fragment); break; case OP_SHOW: setLastIn(lastInFragments, op.fragment); break; case OP_DETACH: setFirstOut(firstOutFragments, op.fragment); break; case OP_ATTACH: setLastIn(lastInFragments, op.fragment); break; } op = op.next; } } /** * 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 firstOutFragments The list of first fragments to be removed, keyed on the * container ID. This list will be modified by the method. * @param lastInFragments The list of last fragments to be added, keyed on the * container ID. This list will be modified by the method. */ public void calculateBackFragments(SparseArray firstOutFragments, SparseArray lastInFragments) { if (!mManager.mContainer.hasView()) { return; // nothing to see, so no transitions } Op op = mHead; while (op != null) { switch (op.cmd) { case OP_ADD: setFirstOut(firstOutFragments, op.fragment); break; case OP_REPLACE: if (op.removed != null) { for (int i = op.removed.size() - 1; i >= 0; i--) { setLastIn(lastInFragments, op.removed.get(i)); } } setFirstOut(firstOutFragments, op.fragment); break; case OP_REMOVE: setLastIn(lastInFragments, op.fragment); break; case OP_HIDE: setLastIn(lastInFragments, op.fragment); break; case OP_SHOW: setFirstOut(firstOutFragments, op.fragment); break; case OP_DETACH: setLastIn(lastInFragments, op.fragment); break; case OP_ATTACH: setFirstOut(firstOutFragments, op.fragment); break; } op = op.next; } } /** * 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. * *

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.

* *

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.

* *

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.

* * @param firstOutFragments The list of first fragments to be removed, keyed on the * container ID. * @param lastInFragments The list of last fragments to be added, keyed on the * container ID. * @param isBack true if this is popping the back stack or false if this is a * forward operation. * @return The TransitionState used to complete the operation of the transition * in {@link #setNameOverrides(android.app.BackStackRecord.TransitionState, java.util.ArrayList, * java.util.ArrayList)}. */ private TransitionState beginTransition(SparseArray firstOutFragments, SparseArray lastInFragments, boolean isBack) { 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.mActivity); // Go over all leaving fragments. for (int i = 0; i < firstOutFragments.size(); i++) { int containerId = firstOutFragments.keyAt(i); configureTransitions(containerId, state, isBack, firstOutFragments, lastInFragments); } // Now go over all entering fragments that didn't have a leaving fragment. for (int i = 0; i < lastInFragments.size(); i++) { int containerId = lastInFragments.keyAt(i); if (firstOutFragments.get(containerId) == null) { configureTransitions(containerId, state, isBack, firstOutFragments, lastInFragments); } } return state; } private static Transition cloneTransition(Transition transition) { if (transition != null) { transition = transition.clone(); } return transition; } private static Transition getEnterTransition(Fragment inFragment, boolean isBack) { if (inFragment == null) { return null; } return cloneTransition(isBack ? inFragment.getReenterTransition() : inFragment.getEnterTransition()); } private static Transition getExitTransition(Fragment outFragment, boolean isBack) { if (outFragment == null) { return null; } return cloneTransition(isBack ? outFragment.getReturnTransition() : outFragment.getExitTransition()); } private static Transition getSharedElementTransition(Fragment inFragment, Fragment outFragment, boolean isBack) { if (inFragment == null || outFragment == null) { return null; } return cloneTransition(isBack ? outFragment.getSharedElementReturnTransition() : inFragment.getSharedElementEnterTransition()); } private static ArrayList captureExitingViews(Transition exitTransition, Fragment outFragment, ArrayMap namedViews, View nonExistentView) { ArrayList viewList = null; if (exitTransition != null) { viewList = new ArrayList(); 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 remapSharedElements(TransitionState state, Fragment outFragment, boolean isBack) { ArrayMap namedViews = new ArrayMap(); if (mSharedElementSourceNames != null) { outFragment.getView().findNamedViews(namedViews); if (isBack) { namedViews.retainAll(mSharedElementTargetNames); } else { namedViews = remapNames(mSharedElementSourceNames, mSharedElementTargetNames, namedViews); } } if (isBack) { 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. * *

The shared element transition maps its shared elements immediately prior to * capturing the final state of the Transition.

*/ private ArrayList addTransitionTargets(final TransitionState state, final Transition enterTransition, final Transition sharedElementTransition, final Transition overallTransition, final View container, final Fragment inFragment, final Fragment outFragment, final ArrayList hiddenFragmentViews, final boolean isBack, final ArrayList sharedElementTargets) { if (enterTransition == null && sharedElementTransition == null && overallTransition == null) { return null; } final ArrayList enteringViews = new ArrayList(); container.getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { container.getViewTreeObserver().removeOnPreDrawListener(this); // Don't include any newly-hidden fragments in the transition. excludeHiddenFragments(hiddenFragmentViews, inFragment.mContainerId, overallTransition); ArrayMap namedViews = null; if (sharedElementTransition != null) { namedViews = mapSharedElementsIn(state, isBack, inFragment); removeTargets(sharedElementTransition, sharedElementTargets); sharedElementTargets.clear(); sharedElementTargets.add(state.nonExistentView); sharedElementTargets.addAll(namedViews.values()); addTargets(sharedElementTransition, sharedElementTargets); setEpicenterIn(namedViews, state); callSharedElementEnd(state, inFragment, outFragment, isBack, namedViews); } if (enterTransition != null) { 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. enterTransition.removeTarget(state.nonExistentView); addTargets(enterTransition, enteringViews); } setSharedElementEpicenter(enterTransition, state); } return true; } }); return enteringViews; } private void callSharedElementEnd(TransitionState state, Fragment inFragment, Fragment outFragment, boolean isBack, ArrayMap namedViews) { SharedElementCallback sharedElementCallback = isBack ? outFragment.mEnterTransitionCallback : inFragment.mEnterTransitionCallback; ArrayList names = new ArrayList(namedViews.keySet()); ArrayList views = new ArrayList(namedViews.values()); sharedElementCallback.onSharedElementEnd(names, views, null); } private void setEpicenterIn(ArrayMap 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 mapSharedElementsIn(TransitionState state, boolean isBack, Fragment inFragment) { // Now map the shared elements in the incoming fragment ArrayMap namedViews = mapEnteringSharedElements(state, inFragment, isBack); // remap shared elements and set the name mapping used // in the shared element transition. if (isBack) { 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 isBack) { boolean overlap = true; if (enterTransition != null && exitTransition != null) { overlap = isBack ? 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 firstOutFragments The list of first fragments to be removed, keyed on the * container ID. * @param lastInFragments The list of last fragments to be added, keyed on the * container ID. * @param isBack true if this is popping the back stack or false if this is a * forward operation. */ private void configureTransitions(int containerId, TransitionState state, boolean isBack, SparseArray firstOutFragments, SparseArray lastInFragments) { ViewGroup sceneRoot = (ViewGroup) mManager.mContainer.findViewById(containerId); if (sceneRoot != null) { Fragment inFragment = lastInFragments.get(containerId); Fragment outFragment = firstOutFragments.get(containerId); Transition enterTransition = getEnterTransition(inFragment, isBack); Transition sharedElementTransition = getSharedElementTransition(inFragment, outFragment, isBack); Transition exitTransition = getExitTransition(outFragment, isBack); if (enterTransition == null && sharedElementTransition == null && exitTransition == null) { return; // no transitions! } if (enterTransition != null) { enterTransition.addTarget(state.nonExistentView); } ArrayMap namedViews = null; ArrayList sharedElementTargets = new ArrayList(); if (sharedElementTransition != null) { namedViews = remapSharedElements(state, outFragment, isBack); sharedElementTargets.add(state.nonExistentView); sharedElementTargets.addAll(namedViews.values()); addTargets(sharedElementTransition, sharedElementTargets); // Notify the start of the transition. SharedElementCallback callback = isBack ? outFragment.mEnterTransitionCallback : inFragment.mEnterTransitionCallback; ArrayList names = new ArrayList(namedViews.keySet()); ArrayList views = new ArrayList(namedViews.values()); callback.onSharedElementStart(names, views, null); } ArrayList exitingViews = captureExitingViews(exitTransition, outFragment, namedViews, state.nonExistentView); if (exitingViews == null || exitingViews.isEmpty()) { exitTransition = null; } // 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, isBack); if (transition != null) { ArrayList hiddenFragments = new ArrayList(); ArrayList enteringViews = addTransitionTargets(state, enterTransition, sharedElementTransition, transition, sceneRoot, inFragment, outFragment, hiddenFragments, isBack, 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); } } } /** * 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 enteringViews, final Transition exitTransition, final ArrayList exitingViews, final Transition sharedElementTransition, final ArrayList sharedElementTargets, final Transition overallTransition, final ArrayList hiddenViews) { if (overallTransition != null) { sceneRoot.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); if (enterTransition != null) { enterTransition.removeTarget(nonExistingView); removeTargets(enterTransition, enteringViews); } if (exitTransition != null) { removeTargets(exitTransition, exitingViews); } 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 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 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 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 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 remapNames(ArrayList inMap, ArrayList toGoInMap, ArrayMap namedViews) { ArrayMap remappedViews = new ArrayMap(); 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 isBack true if this is popping the back stack or false if this is a * forward operation. */ private ArrayMap mapEnteringSharedElements(TransitionState state, Fragment inFragment, boolean isBack) { ArrayMap namedViews = new ArrayMap(); View root = inFragment.getView(); if (root != null) { if (mSharedElementSourceNames != null) { root.findNamedViews(namedViews); if (isBack) { namedViews = remapNames(mSharedElementSourceNames, mSharedElementTargetNames, namedViews); } else { namedViews.retainAll(mSharedElementTargetNames); } } } return namedViews; } private void excludeHiddenFragments(final ArrayList 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; } }); } public TransitionState popFromBackStack(boolean doStateMove, TransitionState state, SparseArray firstOutFragments, SparseArray lastInFragments) { 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 (state == null) { if (firstOutFragments.size() != 0 || lastInFragments.size() != 0) { state = beginTransition(firstOutFragments, lastInFragments, true); } } else if (!doStateMove) { setNameOverrides(state, mSharedElementTargetNames, mSharedElementSourceNames); } bumpBackStackNesting(-1); Op op = mTail; while (op != null) { switch (op.cmd) { case OP_ADD: { Fragment f = op.fragment; f.mNextAnim = op.popExitAnim; mManager.removeFragment(f, FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); } break; case OP_REPLACE: { Fragment f = op.fragment; if (f != null) { f.mNextAnim = op.popExitAnim; mManager.removeFragment(f, FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); } if (op.removed != null) { for (int i = 0; i < op.removed.size(); i++) { Fragment old = op.removed.get(i); old.mNextAnim = op.popEnterAnim; mManager.addFragment(old, false); } } } break; case OP_REMOVE: { Fragment f = op.fragment; f.mNextAnim = op.popEnterAnim; mManager.addFragment(f, false); } break; case OP_HIDE: { Fragment f = op.fragment; f.mNextAnim = op.popEnterAnim; mManager.showFragment(f, FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); } break; case OP_SHOW: { Fragment f = op.fragment; f.mNextAnim = op.popExitAnim; mManager.hideFragment(f, FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); } break; case OP_DETACH: { Fragment f = op.fragment; f.mNextAnim = op.popEnterAnim; mManager.attachFragment(f, FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); } break; case OP_ATTACH: { Fragment f = op.fragment; f.mNextAnim = op.popExitAnim; mManager.detachFragment(f, FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); } break; default: { throw new IllegalArgumentException("Unknown cmd: " + op.cmd); } } op = op.prev; } 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 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); } } private static void setNameOverrides(TransitionState state, ArrayList sourceNames, ArrayList targetNames) { if (sourceNames != null) { for (int i = 0; i < sourceNames.size(); i++) { String source = sourceNames.get(i); String target = targetNames.get(i); setNameOverride(state.nameOverrides, source, target); } } } private void setBackNameOverrides(TransitionState state, ArrayMap namedViews, boolean isEnd) { int count = mSharedElementTargetNames.size(); 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 namedViews, boolean isEnd) { int count = 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); } } } public String getName() { return mName; } public int getTransition() { return mTransition; } public int getTransitionStyle() { return mTransitionStyle; } public boolean isEmpty() { return mNumOp == 0; } public class TransitionState { public ArrayMap nameOverrides = new ArrayMap(); public View enteringEpicenterView; public View nonExistentView; } }