/* * 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.Collection; 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); TransitionState state = null; if (firstOutFragments.size() != 0 || lastInFragments.size() != 0) { state = 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); } if (state != null) { updateTransitionEndState(state, firstOutFragments, lastInFragments, false); } } 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) { 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; } if (!haveTransitions(firstOutFragments, lastInFragments, false)) { firstOutFragments.clear(); lastInFragments.clear(); } } /** * @return true if custom transitions exist on any fragment in firstOutFragments or * lastInFragments or false otherwise. */ private static boolean haveTransitions(SparseArray firstOutFragments, SparseArray lastInFragments, boolean isBack) { for (int i = firstOutFragments.size() - 1; i >= 0; i--) { Fragment f = firstOutFragments.valueAt(i); if (isBack) { if (f.getReturnTransition() != null || f.getSharedElementReturnTransition() != null) { return true; } } else if (f.getExitTransition() != null) { return true; } } for (int i = lastInFragments.size() - 1; i >= 0; i--) { Fragment f = lastInFragments.valueAt(i); if (isBack) { if (f.getReenterTransition() != null) { return true; } } else if (f.getEnterTransition() != null || f.getSharedElementEnterTransition() != null) { return true; } } return false; } /** * 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) { 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; } if (!haveTransitions(firstOutFragments, lastInFragments, true)) { firstOutFragments.clear(); lastInFragments.clear(); } } /** * 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 #updateTransitionEndState(android.app.BackStackRecord.TransitionState, * android.util.SparseArray, android.util.SparseArray, boolean)}. */ 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); ArrayMap tempViews1 = new ArrayMap(); ArrayMap tempViews2 = new ArrayMap(); ArrayList tempNames = new ArrayList(); ArrayList tempViewList = new ArrayList(); // Go over all leaving fragments. for (int i = 0; i < firstOutFragments.size(); i++) { int containerId = firstOutFragments.keyAt(i); configureTransitions(containerId, state, isBack, firstOutFragments, lastInFragments, tempViews1, tempViews2, tempNames, tempViewList); } // 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, tempViews1, tempViews2, tempNames, tempViewList); } } if (state.overallTransitions.size() == 0) { state = null; } return state; } private static Transition getEnterTransition(Fragment inFragment, boolean isBack) { if (inFragment == null) { return null; } return isBack ? inFragment.getReenterTransition() : inFragment.getEnterTransition(); } private static Transition getExitTransition(Fragment outFragment, boolean isBack) { if (outFragment == null) { return null; } return isBack ? outFragment.getReturnTransition() : outFragment.getExitTransition(); } private static Transition getSharedElementTransition(Fragment inFragment, Fragment outFragment, boolean isBack) { if (inFragment == null || outFragment == null) { return null; } return isBack ? outFragment.getSharedElementReturnTransition() : inFragment.getSharedElementEnterTransition(); } private static Transition captureExitingViews(Transition exitTransition, Fragment outFragment, ArrayList viewList) { if (exitTransition != null) { View root = outFragment.getView(); viewList.clear(); root.captureTransitioningViews(viewList); if (viewList.isEmpty()) { exitTransition = null; } else { addTransitioningViews(exitTransition, viewList); } } return exitTransition; } private ArrayMap remapSharedElements(TransitionState state, Fragment outFragment, ArrayMap namedViews, ArrayMap tempViews2, boolean isBack) { if (mSharedElementSourceNames != null) { outFragment.getView().findNamedViews(namedViews); if (isBack) { namedViews.retainAll(mSharedElementTargetNames); } else { namedViews = remapNames(mSharedElementSourceNames, mSharedElementTargetNames, namedViews, tempViews2); } } 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. */ private void prepareEnterTransition(TransitionState state, final Transition enterTransition, final View container, final Fragment inFragment) { if (enterTransition != null) { final ArrayList enteringViews = new ArrayList(); final View nonExistentView = state.nonExistentView; enterTransition.addTarget(state.nonExistentView); enterTransition.addListener(new Transition.TransitionListenerAdapter() { @Override public void onTransitionStart(Transition transition) { transition.removeListener(this); transition.removeTarget(nonExistentView); int numViews = enteringViews.size(); for (int i = 0; i < numViews; i++) { transition.removeTarget(enteringViews.get(i)); } } }); container.getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { container.getViewTreeObserver().removeOnPreDrawListener(this); View view = inFragment.getView(); if (view != null) { view.captureTransitioningViews(enteringViews); addTransitioningViews(enterTransition, enteringViews); } return true; } }); setSharedElementEpicenter(enterTransition, state); } } 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(); } Transition transition; if (overlap) { transition = TransitionUtils.mergeTransitions(enterTransition, exitTransition, sharedElementTransition); } else { TransitionSet staggered = new TransitionSet() .addTransition(exitTransition) .addTransition(enterTransition) .setOrdering(TransitionSet.ORDERING_SEQUENTIAL); transition = TransitionUtils.mergeTransitions(staggered, sharedElementTransition); } 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 to be shared with {@link #updateTransitionEndState( * android.app.BackStackRecord.TransitionState, android.util.SparseArray, * android.util.SparseArray, boolean)} later. * @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. * @param tempViews1 A temporary mapping of names to Views, used to avoid allocation * inside a loop. * @param tempViews2 A temporary mapping of names to Views, used to avoid allocation * inside a loop. * @param tempNames A temporary list of Strings, used to avoid allocation inside a loop. * @param tempViewList A temporary list of Views, used to avoid allocation inside a loop. */ private void configureTransitions(int containerId, TransitionState state, boolean isBack, SparseArray firstOutFragments, SparseArray lastInFragments, ArrayMap tempViews1, ArrayMap tempViews2, ArrayList tempNames, ArrayList tempViewList) { 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); exitTransition = captureExitingViews(exitTransition, outFragment, tempViewList); ArrayMap namedViews = tempViews1; namedViews.clear(); if (sharedElementTransition != null) { namedViews = remapSharedElements(state, outFragment, namedViews, tempViews2, isBack); } // Notify the start of the transition. SharedElementCallback callback = isBack ? outFragment.mEnterTransitionCallback : inFragment.mEnterTransitionCallback; tempNames.clear(); tempNames.addAll(namedViews.keySet()); tempViewList.clear(); tempViewList.addAll(namedViews.values()); callback.onSharedElementStart(tempNames, tempViewList, null); // Set the epicenter of the exit transition if (mSharedElementTargetNames != null && exitTransition != null) { View epicenterView = namedViews.get(mSharedElementTargetNames.get(0)); if (epicenterView != null) { setEpicenter(exitTransition, epicenterView); } } prepareEnterTransition(state, enterTransition, sceneRoot, inFragment); Transition transition = mergeTransitions(enterTransition, exitTransition, sharedElementTransition, inFragment, isBack); if (transition != null) { state.overallTransitions.put(containerId, transition); 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(state, containerId, transition); cleanupHiddenFragments(transition, state); TransitionManager.beginDelayedTransition(sceneRoot, transition); } } } /** * 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 * @param tempMap A temporary mapping that will be filled with the new values. * @return tempMap after it has been mapped with the new names as keys. */ private static ArrayMap remapNames(ArrayList inMap, ArrayList toGoInMap, ArrayMap namedViews, ArrayMap tempMap) { tempMap.clear(); if (!namedViews.isEmpty()) { int numKeys = inMap.size(); for (int i = 0; i < numKeys; i++) { View view = namedViews.get(inMap.get(i)); if (view != null) { tempMap.put(toGoInMap.get(i), view); } } } return tempMap; } /** * After making all fragment changes, this updates the custom transitions to take into * account the entering views and any remapping. * * @param state The transition State as returned from {@link #beginTransition( * android.util.SparseArray, android.util.SparseArray, boolean)}. * @param outFragments The list of first fragments to be removed, keyed on the * container ID. * @param inFragments 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 updateTransitionEndState(TransitionState state, SparseArray outFragments, SparseArray inFragments, boolean isBack) { ArrayMap tempViews1 = new ArrayMap(); ArrayMap tempViews2 = new ArrayMap(); ArrayList tempNames = new ArrayList(); ArrayList tempViews = new ArrayList(); int numInFragments = inFragments.size(); for (int i = 0; i < numInFragments; i++) { Fragment inFragment = inFragments.valueAt(i); tempViews1.clear(); ArrayMap namedViews = mapEnteringSharedElements(inFragment, tempViews1, tempViews2, 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); } 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; } } int containerId = inFragments.keyAt(i); SharedElementCallback sharedElementCallback = isBack ? outFragments.get(containerId).mEnterTransitionCallback : inFragment.mEnterTransitionCallback; tempNames.clear(); tempNames.addAll(namedViews.keySet()); tempViews.clear(); tempViews.addAll(namedViews.values()); sharedElementCallback.onSharedElementEnd(tempNames, tempViews, null); } // Don't include any newly-hidden fragments in the transition. excludeHiddenFragments(state); } private ArrayMap mapEnteringSharedElements(Fragment inFragment, ArrayMap namedViews, ArrayMap tempViews2, boolean isBack) { View root = inFragment.getView(); if (root != null) { if (mSharedElementSourceNames != null) { root.findNamedViews(namedViews); if (isBack) { namedViews = remapNames(mSharedElementSourceNames, mSharedElementTargetNames, namedViews, tempViews2); } else { namedViews.retainAll(mSharedElementTargetNames); } } } return namedViews; } private static void cleanupHiddenFragments(Transition transition, TransitionState state) { final ArrayList hiddenViews = state.hiddenFragmentViews; transition.addListener(new Transition.TransitionListenerAdapter() { @Override public void onTransitionStart(Transition transition) { transition.removeListener(this); int numViews = hiddenViews.size(); for (int i = 0; i < numViews; i++) { transition.excludeTarget(hiddenViews.get(i), false); } } }); } private void excludeHiddenFragments(TransitionState state, 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 (!state.hiddenFragmentViews.contains(fragment.mView)) { transition.excludeTarget(fragment.mView, true); state.hiddenFragmentViews.add(fragment.mView); } } else { transition.excludeTarget(fragment.mView, false); state.hiddenFragmentViews.remove(fragment.mView); } } } } } private void excludeHiddenFragments(TransitionState state) { int numTransitions = state.overallTransitions.size(); for (int i = 0; i < numTransitions; i++) { Transition transition = state.overallTransitions.valueAt(i); int containerId = state.overallTransitions.keyAt(i); excludeHiddenFragments(state, containerId, transition); } } private static void addTransitioningViews(Transition transition, final Collection views) { for (View view : views) { transition.addTarget(view); } transition.addListener(new Transition.TransitionListenerAdapter() { @Override public void onTransitionStart(Transition transition) { transition.removeListener(this); for (View view : views) { transition.removeTarget(view); } } }); } 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); if (state != null) { updateTransitionEndState(state, firstOutFragments, lastInFragments, 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); String target = namedViews.get(originalTarget).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 SparseArray overallTransitions = new SparseArray(); public ArrayMap nameOverrides = new ArrayMap(); public ArrayList hiddenFragmentViews = new ArrayList(); public View enteringEpicenterView; public View nonExistentView; } }