diff --git a/api/current.txt b/api/current.txt index 3e5a695db49f3..cd34a7e991ab3 100644 --- a/api/current.txt +++ b/api/current.txt @@ -4203,6 +4203,9 @@ package android.app { method public abstract android.app.FragmentTransaction setBreadCrumbTitle(java.lang.CharSequence); method public abstract android.app.FragmentTransaction setCustomAnimations(int, int); method public abstract android.app.FragmentTransaction setCustomAnimations(int, int, int, int); + method public abstract android.app.FragmentTransaction setCustomTransition(int, int); + method public abstract android.app.FragmentTransaction setSharedElement(android.view.View, java.lang.String); + method public abstract android.app.FragmentTransaction setSharedElements(android.util.Pair...); method public abstract android.app.FragmentTransaction setTransition(int); method public abstract android.app.FragmentTransaction setTransitionStyle(int); method public abstract android.app.FragmentTransaction show(android.app.Fragment); diff --git a/core/java/android/app/ActivityTransitionCoordinator.java b/core/java/android/app/ActivityTransitionCoordinator.java index c910aca5d46bf..c7030b0a4b0af 100644 --- a/core/java/android/app/ActivityTransitionCoordinator.java +++ b/core/java/android/app/ActivityTransitionCoordinator.java @@ -293,13 +293,17 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { if (transition == null || views == null || views.isEmpty()) { return null; } + // Add the targets to a set containing transition so that transition + // remains unaffected. We don't want to modify the targets of transition itself. TransitionSet set = new TransitionSet(); - set.addTransition(transition); if (views != null) { - for (View view: views) { + for (View view : views) { set.addTarget(view); } } + // By adding the transition after addTarget, we prevent addTarget from + // affecting transition. + set.addTransition(transition); return set; } diff --git a/core/java/android/app/BackStackRecord.java b/core/java/android/app/BackStackRecord.java index 89ee145cbaf3b..01a388f469f2c 100644 --- a/core/java/android/app/BackStackRecord.java +++ b/core/java/android/app/BackStackRecord.java @@ -16,36 +16,53 @@ 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.TransitionInflater; +import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.util.ArrayMap; import android.util.Log; import android.util.LogWriter; -import com.android.internal.util.FastPrintWriter; +import android.util.Pair; +import android.view.View; +import android.view.ViewGroup; 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 int mCustomTransition; + final int mSceneRoot; 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(); + if (op.removed != null) { + numRemoved += op.removed.size(); + } op = op.next; } - mOps = new int[bse.mNumOp*7 + numRemoved]; + mOps = new int[bse.mNumOp * 7 + numRemoved]; if (!bse.mAddToBackStack) { throw new IllegalStateException("Not on back stack"); @@ -63,7 +80,7 @@ final class BackStackState implements Parcelable { if (op.removed != null) { final int N = op.removed.size(); mOps[pos++] = N; - for (int i=0; i= 0) { Fragment f = fm.mActive.get(findex); @@ -116,9 +143,11 @@ final class BackStackState implements Parcelable { final int N = mOps[pos++]; if (N > 0) { op.removed = new ArrayList(N); - for (int i=0; i CREATOR @@ -217,6 +254,11 @@ final class BackStackRecord extends FragmentTransaction implements int mBreadCrumbShortTitleRes; CharSequence mBreadCrumbShortTitleText; + int mCustomTransition; + int mSceneRoot; + ArrayList mSharedElementSourceNames; + ArrayList mSharedElementTargetNames; + @Override public String toString() { StringBuilder sb = new StringBuilder(128); @@ -240,78 +282,112 @@ final class BackStackRecord extends FragmentTransaction implements 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); + 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)); + 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 (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 (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); + 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); + 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:"); + 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; + 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); + 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)); + 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)); + 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(1); + mSharedElementSourceNames.add(viewName); + + mSharedElementTargetNames = new ArrayList(1); + mSharedElementTargetNames.add(name); + return 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 viewName = sharedElements[i].first.getViewName(); + if (viewName == null) { + throw new IllegalArgumentException("Unique viewNames are required for all" + + " sharedElements"); + } + sourceNames.add(viewName); + targetNames.add(sharedElements[i].second); + } + mSharedElementSourceNames = sourceNames; + mSharedElementTargetNames = targetNames; + } + return this; + } + public FragmentTransaction setTransitionStyle(int styleRes) { mTransitionStyle = styleRes; return this; @@ -550,21 +673,27 @@ final class BackStackRecord extends FragmentTransaction implements if (!mAddToBackStack) { return; } - if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting in " + this - + " by " + amt); + 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 (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--) { + 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); + if (FragmentManagerImpl.DEBUG) { + Log.v(TAG, "Bump nesting of " + + r + " to " + r.mBackStackNesting); + } } } op = op.next; @@ -578,9 +707,11 @@ final class BackStackRecord extends FragmentTransaction implements public int commitAllowingStateLoss() { return commitInternal(true); } - + int commitInternal(boolean allowStateLoss) { - if (mCommitted) throw new IllegalStateException("commit already called"); + if (mCommitted) { + throw new IllegalStateException("commit already called"); + } if (FragmentManagerImpl.DEBUG) { Log.v(TAG, "Commit: " + this); LogWriter logw = new LogWriter(Log.VERBOSE, TAG); @@ -597,9 +728,11 @@ final class BackStackRecord extends FragmentTransaction implements mManager.enqueueAction(this, allowStateLoss); return mIndex; } - + public void run() { - if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Run: " + this); + if (FragmentManagerImpl.DEBUG) { + Log.v(TAG, "Run: " + this); + } if (mAddToBackStack) { if (mIndex < 0) { @@ -609,6 +742,9 @@ final class BackStackRecord extends FragmentTransaction implements bumpBackStackNesting(1); + TransitionState state = beginTransition(mSharedElementSourceNames, + mSharedElementTargetNames); + Op op = mHead; while (op != null) { switch (op.cmd) { @@ -616,14 +752,17 @@ final class BackStackRecord extends FragmentTransaction implements Fragment f = op.fragment; f.mNextAnim = op.enterAnim; mManager.addFragment(f, false); - } break; + } + break; case OP_REPLACE: { Fragment f = op.fragment; if (mManager.mAdded != null) { - for (int i=0; i sourceNames, + ArrayList targetNames) { + if (mCustomTransition <= 0 || mSceneRoot <= 0) { + return null; + } + View rootView = mManager.mContainer.findViewById(mSceneRoot); + if (!(rootView instanceof ViewGroup)) { + throw new IllegalArgumentException("SceneRoot is not a ViewGroup"); + } + TransitionState state = new TransitionState(); + // get Transition scene root and create Transitions + state.sceneRoot = (ViewGroup) rootView; + state.sceneRoot.captureTransitioningViews(state.transitioningViews); + + state.exitTransition = TransitionInflater.from(mManager.mActivity) + .inflateTransition(mCustomTransition); + state.sharedElementTransition = TransitionInflater.from(mManager.mActivity) + .inflateTransition(mCustomTransition); + state.enterTransition = TransitionInflater.from(mManager.mActivity) + .inflateTransition(mCustomTransition); + // 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. + View nonExistentView = new View(mManager.mActivity); + state.enterTransition.addTarget(nonExistentView); + state.exitTransition.addTarget(nonExistentView); + state.sharedElementTransition.addTarget(nonExistentView); + + setSharedElementEpicenter(state.enterTransition, state); + + state.excludingTransition = new TransitionSet() + .addTransition(state.exitTransition) + .addTransition(state.enterTransition); + + if (sourceNames != null) { + // Map shared elements. + state.sceneRoot.findNamedViews(state.namedViews); + state.namedViews.retainAll(sourceNames); + View epicenterView = state.namedViews.get(sourceNames.get(0)); + if (epicenterView != null) { + // The epicenter is only the first shared element. + setEpicenter(state.exitTransition, epicenterView); + setEpicenter(state.sharedElementTransition, epicenterView); + } + state.transitioningViews.removeAll(state.namedViews.values()); + state.excludingTransition.addTransition(state.sharedElementTransition); + addTransitioningViews(state.sharedElementTransition, state.namedViews.values()); + } + + // Adds the (maybe) exiting views, not including the shared element. + // If some stay, that's ok. + addTransitioningViews(state.exitTransition, state.transitioningViews); + + // Prepare for shared element name mapping. This could be chained in the case + // of popping several back stack states. + state.excludingTransition.setNameOverrides(new ArrayMap()); + setNameOverrides(state, sourceNames, targetNames); + + // Don't include any subtree in the views that are hidden when capturing the + // view hierarchy transitions. They should be as if not there. + excludeHiddenFragments(state, true); + + TransitionManager.beginDelayedTransition(state.sceneRoot, state.excludingTransition); + return state; + } + + private void updateTransitionEndState(TransitionState state, ArrayList names) { + // Find all views that are entering. + ArrayList enteringViews = new ArrayList(); + state.sceneRoot.captureTransitioningViews(enteringViews); + enteringViews.removeAll(state.transitioningViews); + + if (names != null) { + // find all shared elements. + state.namedViews.clear(); + state.sceneRoot.findNamedViews(state.namedViews); + state.namedViews.retainAll(names); + if (!state.namedViews.isEmpty()) { + enteringViews.removeAll(state.namedViews.values()); + addTransitioningViews(state.sharedElementTransition, state.namedViews.values()); + // now we know the epicenter of the entering transition. + state.mEnteringEpicenterView = state.namedViews.get(names.get(0)); + } + } + + // Add all entering views to the enter transition. + addTransitioningViews(state.enterTransition, enteringViews); + + // Don't allow capturing state for the newly-hidden fragments. + excludeHiddenFragments(state, false); + + // Allow capturing state for the newly-shown fragments + includeVisibleFragments(state.excludingTransition); + } + + private void addTransitioningViews(Transition transition, Collection views) { + if (views.isEmpty()) { + // Add a view so that we can modify the valid views at the end of the + // fragment transaction. + transition.addTarget(new View(mManager.mActivity)); + } else { + for (View view : views) { + transition.addTarget(view); + } + } + } + + private void excludeHiddenFragments(TransitionState state, boolean forceExclude) { + if (mManager.mAdded != null) { + for (int i = 0; i < mManager.mAdded.size(); i++) { + Fragment fragment = mManager.mAdded.get(i); + if (fragment.mView != null && fragment.mHidden + && (forceExclude || !state.hiddenViews.contains(fragment.mView))) { + state.excludingTransition.excludeTarget(fragment.mView, true); + state.hiddenViews.add(fragment.mView); + } + } + } + if (forceExclude && state.hiddenViews.isEmpty()) { + state.excludingTransition.excludeTarget(new View(mManager.mActivity), true); + } + } + + private void includeVisibleFragments(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.mHidden) { + transition.excludeTarget(fragment.mView, false); + } + } + } + } + + 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.mEnteringEpicenterView != null) { + mEpicenter = new Rect(); + state.mEnteringEpicenterView.getBoundsOnScreen(mEpicenter); + } + return mEpicenter; + } + }); + } + + public TransitionState popFromBackStack(boolean doStateMove, TransitionState state) { if (FragmentManagerImpl.DEBUG) { Log.v(TAG, "popFromBackStack: " + this); LogWriter logw = new LogWriter(Log.VERBOSE, TAG); @@ -698,6 +1010,12 @@ final class BackStackRecord extends FragmentTransaction implements pw.flush(); } + if (state == null) { + state = beginTransition(mSharedElementTargetNames, mSharedElementSourceNames); + } else { + setNameOverrides(state, mSharedElementTargetNames, mSharedElementSourceNames); + } + bumpBackStackNesting(-1); Op op = mTail; @@ -709,7 +1027,8 @@ final class BackStackRecord extends FragmentTransaction implements mManager.removeFragment(f, FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); - } break; + } + break; case OP_REPLACE: { Fragment f = op.fragment; if (f != null) { @@ -719,42 +1038,48 @@ final class BackStackRecord extends FragmentTransaction implements mTransitionStyle); } if (op.removed != null) { - for (int i=0; i= 0) { mManager.freeBackStackIndex(mIndex); mIndex = -1; } + return state; + } + + private static void setNameOverride(Transition transition, String source, String target) { + ArrayMap overrides = transition.getNameOverrides(); + 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.excludingTransition, source, target); + } + } } public String getName() { @@ -789,4 +1141,16 @@ final class BackStackRecord extends FragmentTransaction implements public boolean isEmpty() { return mNumOp == 0; } + + public class TransitionState { + public ArrayList hiddenViews = new ArrayList(); + public ArrayList transitioningViews = new ArrayList(); + public ArrayMap namedViews = new ArrayMap(); + public Transition exitTransition; + public Transition sharedElementTransition; + public Transition enterTransition; + public TransitionSet excludingTransition; + public ViewGroup sceneRoot; + public View mEnteringEpicenterView; + } } diff --git a/core/java/android/app/ExitTransitionCoordinator.java b/core/java/android/app/ExitTransitionCoordinator.java index 6176c076ec797..9f3dbdcf21cfe 100644 --- a/core/java/android/app/ExitTransitionCoordinator.java +++ b/core/java/android/app/ExitTransitionCoordinator.java @@ -133,8 +133,10 @@ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { public void onTransitionEnd(Transition transition) { setViewVisibility(mSharedElements, View.INVISIBLE); ViewGroupOverlay overlay = getDecor().getOverlay(); - for (int i = 0; i < mSharedElementSnapshots.size(); i++) { - overlay.add(mSharedElementSnapshots.get(i)); + if (mSharedElementSnapshots != null) { + for (int i = 0; i < mSharedElementSnapshots.size(); i++) { + overlay.add(mSharedElementSnapshots.get(i)); + } } } }); diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java index 76f9d97455cf1..b8f1962d0b800 100644 --- a/core/java/android/app/FragmentManager.java +++ b/core/java/android/app/FragmentManager.java @@ -1494,7 +1494,7 @@ final class FragmentManagerImpl extends FragmentManager { return false; } final BackStackRecord bss = mBackStack.remove(last); - bss.popFromBackStack(true); + bss.popFromBackStack(true, null); reportBackStackChanged(); } else { int index = -1; @@ -1538,9 +1538,10 @@ final class FragmentManagerImpl extends FragmentManager { states.add(mBackStack.remove(i)); } final int LAST = states.size()-1; + BackStackRecord.TransitionState state = null; for (int i=0; i<=LAST; i++) { if (DEBUG) Log.v(TAG, "Popping back stack state: " + states.get(i)); - states.get(i).popFromBackStack(i == LAST); + state = states.get(i).popFromBackStack(i == LAST, state); } reportBackStackChanged(); } diff --git a/core/java/android/app/FragmentTransaction.java b/core/java/android/app/FragmentTransaction.java index 6e99899688963..7479ecdafde31 100644 --- a/core/java/android/app/FragmentTransaction.java +++ b/core/java/android/app/FragmentTransaction.java @@ -1,5 +1,8 @@ package android.app; +import android.util.Pair; +import android.view.View; + /** * API for performing a set of Fragment operations. * @@ -168,6 +171,36 @@ public abstract class FragmentTransaction { */ public abstract FragmentTransaction setTransition(int transit); + /** + * Set a {@link android.transition.Transition} resource id to use with this transaction. + * transitionId will be played for fragments when going forward and when popping + * the back stack. + * @param sceneRootId The ID of the element acting as the scene root for the transition. + * This should be a ViewGroup containing all Fragments in the transaction. + * @param transitionId The resource ID for the Transition used during the Fragment transaction. + */ + public abstract FragmentTransaction setCustomTransition(int sceneRootId, int transitionId); + + /** + * Used with {@link #setCustomTransition(int, int)} to map a View from a removed or hidden + * Fragment to a View from a shown or added Fragment. + * sharedElement must have a unique viewName in the View hierarchy. + * @param sharedElement A View in a disappearing Fragment to match with a View in an + * appearing Fragment. + * @param name The viewName for a View in an appearing Fragment to match to the shared + * element. + */ + public abstract FragmentTransaction setSharedElement(View sharedElement, String name); + + /** + * Used with {@link #setCustomTransition(int, int)} to map multiple Views from removed or hidden + * Fragments to a Views from a shown or added Fragments. Views in + * sharedElements must have unique viewNames in the View hierarchy. + * @param sharedElements Pairs of Views in disappearing Fragments to viewNames in + * appearing Fragments. + */ + public abstract FragmentTransaction setSharedElements(Pair... sharedElements); + /** * Set a custom style resource that will be used for resolving transit * animations. diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java index 0a4f641006f9a..508769d0ff29b 100644 --- a/core/java/android/transition/Transition.java +++ b/core/java/android/transition/Transition.java @@ -206,6 +206,10 @@ public abstract class Transition implements Cloneable { // like CircularPropagation EpicenterCallback mEpicenterCallback; + // For Fragment shared element transitions, linking views explicitly by mismatching + // viewNames. + ArrayMap mNameOverrides; + /** * Constructs a Transition object with no target objects. A transition with * no targets defaults to running on all target objects in the scene hierarchy @@ -1347,6 +1351,21 @@ public abstract class Transition implements Cloneable { } else { captureHierarchy(sceneRoot, start); } + if (!start && mNameOverrides != null) { + int numOverrides = mNameOverrides.size(); + ArrayList overriddenViews = new ArrayList(numOverrides); + for (int i = 0; i < numOverrides; i++) { + String fromName = mNameOverrides.keyAt(i); + overriddenViews.add(mStartValues.nameValues.remove(fromName)); + } + for (int i = 0; i < numOverrides; i++) { + View view = overriddenViews.get(i); + if (view != null) { + String toName = mNameOverrides.valueAt(i); + mStartValues.nameValues.put(toName, view); + } + } + } } static void addViewValues(TransitionValuesMaps transitionValuesMaps, @@ -1400,10 +1419,12 @@ public abstract class Transition implements Cloneable { mStartValues.viewValues.clear(); mStartValues.idValues.clear(); mStartValues.itemIdValues.clear(); + mStartValues.nameValues.clear(); } else { mEndValues.viewValues.clear(); mEndValues.idValues.clear(); mEndValues.itemIdValues.clear(); + mEndValues.nameValues.clear(); } } @@ -1866,6 +1887,20 @@ public abstract class Transition implements Cloneable { return mCanRemoveViews; } + /** + * Sets the shared element names -- a mapping from a name at the start state to + * a different name at the end state. + * @hide + */ + public void setNameOverrides(ArrayMap overrides) { + mNameOverrides = overrides; + } + + /** @hide */ + public ArrayMap getNameOverrides() { + return mNameOverrides; + } + @Override public String toString() { return toString(""); diff --git a/core/java/android/transition/TransitionSet.java b/core/java/android/transition/TransitionSet.java index 698b5638795aa..c04be4f57aac1 100644 --- a/core/java/android/transition/TransitionSet.java +++ b/core/java/android/transition/TransitionSet.java @@ -168,14 +168,36 @@ public class TransitionSet extends Transition { @Override public TransitionSet addTarget(View target) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).addTarget(target); + } return (TransitionSet) super.addTarget(target); } @Override public TransitionSet addTarget(int targetId) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).addTarget(targetId); + } return (TransitionSet) super.addTarget(targetId); } + @Override + public TransitionSet addTarget(String targetName) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).addTarget(targetName); + } + return (TransitionSet) super.addTarget(targetName); + } + + @Override + public TransitionSet addTarget(Class targetType) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).addTarget(targetType); + } + return (TransitionSet) super.addTarget(targetType); + } + @Override public TransitionSet addListener(TransitionListener listener) { return (TransitionSet) super.addListener(listener); @@ -183,14 +205,68 @@ public class TransitionSet extends Transition { @Override public TransitionSet removeTarget(int targetId) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).removeTarget(targetId); + } return (TransitionSet) super.removeTarget(targetId); } @Override public TransitionSet removeTarget(View target) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).removeTarget(target); + } return (TransitionSet) super.removeTarget(target); } + @Override + public TransitionSet removeTarget(Class target) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).removeTarget(target); + } + return (TransitionSet) super.removeTarget(target); + } + + @Override + public TransitionSet removeTarget(String target) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).removeTarget(target); + } + return (TransitionSet) super.removeTarget(target); + } + + @Override + public Transition excludeTarget(View target, boolean exclude) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).excludeTarget(target, exclude); + } + return super.excludeTarget(target, exclude); + } + + @Override + public Transition excludeTarget(String targetViewName, boolean exclude) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).excludeTarget(targetViewName, exclude); + } + return super.excludeTarget(targetViewName, exclude); + } + + @Override + public Transition excludeTarget(int targetId, boolean exclude) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).excludeTarget(targetId, exclude); + } + return super.excludeTarget(targetId, exclude); + } + + @Override + public Transition excludeTarget(Class type, boolean exclude) { + for (int i = 0; i < mTransitions.size(); i++) { + mTransitions.get(i).excludeTarget(type, exclude); + } + return super.excludeTarget(type, exclude); + } + @Override public TransitionSet removeListener(TransitionListener listener) { return (TransitionSet) super.removeListener(listener); diff --git a/core/java/android/transition/Visibility.java b/core/java/android/transition/Visibility.java index 0f7638b2e3373..c6c833772784c 100644 --- a/core/java/android/transition/Visibility.java +++ b/core/java/android/transition/Visibility.java @@ -18,6 +18,9 @@ package android.transition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.BitmapDrawable; import android.view.View; import android.view.ViewGroup; @@ -272,15 +275,23 @@ public abstract class Visibility extends Transition { if (startView.getParent() == null) { // no parent - safe to use overlayView = startView; - } else if (startView.getParent() instanceof View && - startView.getParent().getParent() == null) { + } else if (startView.getParent() instanceof View) { View startParent = (View) startView.getParent(); - int id = startParent.getId(); - if (id != View.NO_ID && sceneRoot.findViewById(id) != null && mCanRemoveViews) { - // no parent, but its parent is unparented but the parent - // hierarchy has been replaced by a new hierarchy with the same id - // and it is safe to un-parent startView - overlayView = startView; + if (!isValidTarget(startParent)) { + if (startView.isAttachedToWindow()) { + overlayView = copyViewImage(startView); + } else { + overlayView = startView; + } + } else if (startParent.getParent() == null) { + int id = startParent.getId(); + if (id != View.NO_ID && sceneRoot.findViewById(id) != null + && mCanRemoveViews) { + // no parent, but its parent is unparented but the parent + // hierarchy has been replaced by a new hierarchy with the same id + // and it is safe to un-parent startView + overlayView = startView; + } } } } @@ -378,6 +389,26 @@ public abstract class Visibility extends Transition { return null; } + private View copyViewImage(View view) { + int width = view.getWidth(); + int height = view.getHeight(); + if (width <= 0 || height <= 0) { + return null; + } + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + view.draw(canvas); + final BitmapDrawable drawable = new BitmapDrawable(bitmap); + + View overlayView = new View(view.getContext()); + overlayView.setBackground(drawable); + int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); + int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); + overlayView.measure(widthSpec, heightSpec); + overlayView.layout(0, 0, width, height); + return overlayView; + } + /** * The default implementation of this method returns a null Animator. Subclasses should * override this method to make targets disappear with the desired transition. The