From 0a778eda690a66173733a63622886e888d405c45 Mon Sep 17 00:00:00 2001 From: George Mount Date: Fri, 13 Dec 2013 13:35:36 -0800 Subject: Cross-Activity Scene transition API. First pass at API for cross-Activity Scene transitions. Remaining work: Transition back Automatically capture hero element info Transfer of surface texture to synchronize between Activities Possibly use scene names to indicate preferred transition Change-Id: I59d07de1fae694a46b92b1c82525daa301ec1377 --- api/current.txt | 17 +- core/java/android/app/Activity.java | 140 +++++++--- core/java/android/app/ActivityOptions.java | 290 ++++++++++++++------- core/java/android/transition/Transition.java | 15 +- core/java/android/view/View.java | 33 +++ core/java/android/view/ViewGroup.java | 42 +++ core/java/android/view/Window.java | 38 ++- core/res/res/values/attrs.xml | 13 + core/res/res/values/ids.xml | 1 + core/res/res/values/public.xml | 6 + .../android/internal/policy/impl/PhoneWindow.java | 268 ++++++++++++++++--- 11 files changed, 699 insertions(+), 164 deletions(-) diff --git a/api/current.txt b/api/current.txt index 94ffb300c852..7bd1175c59a1 100644 --- a/api/current.txt +++ b/api/current.txt @@ -543,6 +543,7 @@ package android { field public static final int fromAlpha = 16843210; // 0x10101ca field public static final int fromDegrees = 16843187; // 0x10101b3 field public static final int fromScene = 16843741; // 0x10103dd + field public static final int fromSceneName = 16843774; // 0x10103fe field public static final int fromXDelta = 16843206; // 0x10101c6 field public static final int fromXScale = 16843202; // 0x10101c2 field public static final int fromYDelta = 16843208; // 0x10101c8 @@ -960,6 +961,7 @@ package android { field public static final int shadowRadius = 16843108; // 0x1010164 field public static final int shape = 16843162; // 0x101019a field public static final int shareInterpolator = 16843195; // 0x10101bb + field public static final int sharedElementName = 16843776; // 0x1010400 field public static final int sharedUserId = 16842763; // 0x101000b field public static final int sharedUserLabel = 16843361; // 0x1010261 field public static final int shouldDisableView = 16843246; // 0x10101ee @@ -1142,6 +1144,7 @@ package android { field public static final int toAlpha = 16843211; // 0x10101cb field public static final int toDegrees = 16843188; // 0x10101b4 field public static final int toScene = 16843742; // 0x10103de + field public static final int toSceneName = 16843775; // 0x10103ff field public static final int toXDelta = 16843207; // 0x10101c7 field public static final int toXScale = 16843203; // 0x10101c3 field public static final int toYDelta = 16843209; // 0x10101c9 @@ -1157,6 +1160,7 @@ package android { field public static final int transformPivotX = 16843552; // 0x1010320 field public static final int transformPivotY = 16843553; // 0x1010321 field public static final int transition = 16843743; // 0x10103df + field public static final int transitionGroup = 16843777; // 0x1010401 field public static final int transitionOrdering = 16843744; // 0x10103e0 field public static final int translationX = 16843554; // 0x1010322 field public static final int translationY = 16843555; // 0x1010323 @@ -1514,6 +1518,7 @@ package android { field public static final int selectAll = 16908319; // 0x102001f field public static final int selectTextMode = 16908333; // 0x102002d field public static final int selectedIcon = 16908302; // 0x102000e + field public static final int shared_element_name = 16908334; // 0x102002e field public static final int startSelectingText = 16908328; // 0x1020028 field public static final int stopSelectingText = 16908329; // 0x1020029 field public static final int summary = 16908304; // 0x1020010 @@ -3035,6 +3040,7 @@ package android.app { method public int getTaskId(); method public final java.lang.CharSequence getTitle(); method public final int getTitleColor(); + method public android.os.Bundle getTransitionArgs(); method public final int getVolumeControlStream(); method public android.view.Window getWindow(); method public android.view.WindowManager getWindowManager(); @@ -3130,6 +3136,7 @@ package android.app { method public void setContentView(android.view.View); method public void setContentView(android.view.View, android.view.ViewGroup.LayoutParams); method public final void setDefaultKeyMode(int); + method public void setEarlyBackgroundTransition(boolean); method public final void setFeatureDrawable(int, android.graphics.drawable.Drawable); method public final void setFeatureDrawableAlpha(int, int); method public final void setFeatureDrawableResource(int, int); @@ -3170,6 +3177,7 @@ package android.app { method public boolean startNextMatchingActivity(android.content.Intent); method public boolean startNextMatchingActivity(android.content.Intent, android.os.Bundle); method public void startSearch(java.lang.String, boolean, android.os.Bundle, boolean); + method protected void startSharedElementTransition(android.os.Bundle); method public deprecated void stopManagingCursor(android.database.Cursor); method public void takeKeyEvents(boolean); method public void triggerSearch(java.lang.String, android.os.Bundle); @@ -3340,6 +3348,7 @@ package android.app { public class ActivityOptions { method public static android.app.ActivityOptions makeCustomAnimation(android.content.Context, int, int); method public static android.app.ActivityOptions makeScaleUpAnimation(android.view.View, int, int, int, int); + method public static android.app.ActivityOptions makeSceneTransitionAnimation(android.os.Bundle); method public static android.app.ActivityOptions makeThumbnailScaleUpAnimation(android.view.View, android.graphics.Bitmap, int, int); method public android.os.Bundle toBundle(); method public void update(android.app.ActivityOptions); @@ -18375,7 +18384,7 @@ package android.os { public abstract class CountDownTimer { ctor public CountDownTimer(long, long); - method public final void cancel(); + method public final synchronized void cancel(); method public abstract void onFinish(); method public abstract void onTick(long); method public final synchronized android.os.CountDownTimer start(); @@ -26679,6 +26688,7 @@ package android.transition { method public android.transition.Transition addListener(android.transition.Transition.TransitionListener); method public android.transition.Transition addTarget(int); method public android.transition.Transition addTarget(android.view.View); + method public boolean canRemoveViews(); method public abstract void captureEndValues(android.transition.TransitionValues); method public abstract void captureStartValues(android.transition.TransitionValues); method public android.transition.Transition clone(); @@ -28743,6 +28753,7 @@ package android.view { method public int getScrollBarStyle(); method public final int getScrollX(); method public final int getScrollY(); + method public java.lang.String getSharedElementName(); method public int getSolidColor(); method protected int getSuggestedMinimumHeight(); method protected int getSuggestedMinimumWidth(); @@ -28996,6 +29007,7 @@ package android.view { method public void setScrollY(int); method public void setScrollbarFadingEnabled(boolean); method public void setSelected(boolean); + method public void setSharedElementName(java.lang.String); method public void setSoundEffectsEnabled(boolean); method public void setSystemUiVisibility(int); method public void setTag(java.lang.Object); @@ -29382,6 +29394,7 @@ package android.view { method protected boolean isChildrenDrawingOrderEnabled(); method protected boolean isChildrenDrawnWithCacheEnabled(); method public boolean isMotionEventSplittingEnabled(); + method public boolean isTransitionGroup(); method public final void layout(int, int, int, int); method protected void measureChild(android.view.View, int, int); method protected void measureChildWithMargins(android.view.View, int, int, int, int); @@ -29427,6 +29440,7 @@ package android.view { method public void setOnHierarchyChangeListener(android.view.ViewGroup.OnHierarchyChangeListener); method public void setPersistentDrawingCache(int); method protected void setStaticTransformationsEnabled(boolean); + method public void setTransitionGroup(boolean); method public boolean shouldDelayChildPressedState(); method public boolean showContextMenuForChild(android.view.View); method public android.view.ActionMode startActionModeForChild(android.view.View, android.view.ActionMode.Callback); @@ -29721,7 +29735,6 @@ package android.view { method public abstract void setTitle(java.lang.CharSequence); method public abstract deprecated void setTitleColor(int); method public void setTransitionManager(android.transition.TransitionManager); - method public void setTransitionOptions(android.os.Bundle); method public void setType(int); method public void setUiOptions(int); method public void setUiOptions(int, int); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 698f06a16f89..a8716bf96d1c 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -18,7 +18,6 @@ package android.app; import android.annotation.NonNull; import android.transition.Scene; -import android.transition.Transition; import android.transition.TransitionManager; import android.util.ArrayMap; import android.util.SuperNotCalledException; @@ -771,6 +770,7 @@ public class Activity extends ContextThemeWrapper private Thread mUiThread; final Handler mHandler = new Handler(); + private ActivityOptions mTransitionActivityOptions; /** Return the intent that started this activity. */ public Intent getIntent() { @@ -3443,38 +3443,16 @@ public class Activity extends ContextThemeWrapper * * @throws android.content.ActivityNotFoundException * - * @see #startActivity + * @see #startActivity */ public void startActivityForResult(Intent intent, int requestCode) { - final TransitionManager tm = getWindow().getTransitionManager(); - final Scene currScene = getWindow().getContentScene(); - final String[] targetSceneNames = currScene != null && tm != null ? - tm.getTargetSceneNames(currScene) : null; - - if (targetSceneNames == null || targetSceneNames.length == 0) { - startActivityForResult(intent, requestCode, null); - } else { - // TODO Capture the scene transition args and send along - final ActivityOptions opts = ActivityOptions.makeSceneTransitionAnimation( - targetSceneNames, null, - new ActivityOptions.OnSceneTransitionStartedListener() { - @Override public void onSceneTransitionStarted(String destSceneName) { - final Transition t = tm.getNamedTransition(currScene, destSceneName); - // TODO Fill this in to notify the outgoing activity that it should - // treat this as a sync point for the transition - the target - // transition has started. - Log.d(TAG, "Scene transition to scene " + destSceneName + - " transition " + t); - } - }, mHandler); - startActivityForResult(intent, requestCode, opts.toBundle()); - } + startActivityForResult(intent, requestCode, null); } /** * Launch an activity for which you would like a result when it finished. * When this activity exits, your - * onActivityResult() method will be called with the given requestCode. + * onActivityResult() method will be called with the given requestCode. * Using a negative requestCode is the same as calling * {@link #startActivity} (the activity is not launched as a sub-activity). * @@ -3487,9 +3465,9 @@ public class Activity extends ContextThemeWrapper * *

As a special case, if you call startActivityForResult() with a requestCode * >= 0 during the initial onCreate(Bundle savedInstanceState)/onResume() of your - * activity, then your window will not be displayed until a result is - * returned back from the started activity. This is to avoid visible - * flickering when redirecting to another activity. + * activity, then your window will not be displayed until a result is + * returned back from the started activity. This is to avoid visible + * flickering when redirecting to another activity. * *

This method throws {@link android.content.ActivityNotFoundException} * if there was no Activity found to run the given Intent. @@ -3503,9 +3481,17 @@ public class Activity extends ContextThemeWrapper * * @throws android.content.ActivityNotFoundException * - * @see #startActivity + * @see #startActivity */ public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { + TransitionManager tm = getContentTransitionManager(); + if (tm != null && options != null) { + ActivityOptions activityOptions = new ActivityOptions(options); + if (activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) { + getWindow().startExitTransition(activityOptions); + options = activityOptions.toBundle(); + } + } if (mParent == null) { Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( @@ -5313,7 +5299,7 @@ public class Activity extends ContextThemeWrapper mWindow.setUiOptions(info.uiOptions); } mUiThread = Thread.currentThread(); - + mMainThread = aThread; mInstrumentation = instr; mToken = token; @@ -5335,8 +5321,40 @@ public class Activity extends ContextThemeWrapper mWindow.setContainer(mParent.getWindow()); } mWindowManager = mWindow.getWindowManager(); - mWindow.setTransitionOptions(options); mCurrentConfig = config; + mTransitionActivityOptions = null; + Window.SceneTransitionListener sceneTransitionListener = null; + if (options != null) { + ActivityOptions activityOptions = new ActivityOptions(options); + if (activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) { + mTransitionActivityOptions = activityOptions; + sceneTransitionListener = new Window.SceneTransitionListener() { + @Override + public void enterSharedElement(Bundle transitionArgs) { + startSharedElementTransition(transitionArgs); + mTransitionActivityOptions = null; + } + + @Override + public void nullPendingTransition() { + overridePendingTransition(0, 0); + } + + @Override + public void convertFromTranslucent() { + Activity.this.convertFromTranslucent(); + } + + @Override + public void convertToTranslucent() { + Activity.this.convertToTranslucent(null); + } + }; + + } + } + + mWindow.setTransitionOptions(mTransitionActivityOptions, sceneTransitionListener); } /** @hide */ @@ -5350,7 +5368,7 @@ public class Activity extends ContextThemeWrapper com.android.internal.R.styleable.Window_windowNoDisplay, false); mFragments.dispatchActivityCreated(); } - + final void performStart() { mFragments.noteStateNotSaved(); mCalled = false; @@ -5507,7 +5525,7 @@ public class Activity extends ContextThemeWrapper } } } - + mStopped = true; } mResumed = false; @@ -5522,7 +5540,57 @@ public class Activity extends ContextThemeWrapper mLoaderManager.doDestroy(); } } - + + /** + * Gets the entering Activity transition args. Will be null if + * {@link android.app.ActivityOptions#makeSceneTransitionAnimation(android.os.Bundle)} was + * not used to pass a Bundle to startActivity. The Bundle passed to that method in the + * calling Activity is returned here. + *

After startSharedElementTransition is called, this method will return null.

+ * + * @return The Bundle passed into Bundle parameter of + * {@link android.app.ActivityOptions#makeSceneTransitionAnimation(android.os.Bundle)} + * in the calling Activity. + */ + public Bundle getTransitionArgs() { + if (mTransitionActivityOptions == null) { + return null; + } + return mTransitionActivityOptions.getSceneTransitionArgs(); + } + + /** + * Override to transfer a shared element from a calling Activity to this Activity. + * Shared elements will be made VISIBLE before this call. The Activity is responsible + * for transitioning the shared elements from their location to the eventual destination. + * The shared element will be laid out a the destination when this method is called. + * + * @param transitionArgs The same as returned from {@link #getTransitionArgs()}, this should + * contain information from the calling Activity to tell where the + * shared element should be placed. + */ + protected void startSharedElementTransition(Bundle transitionArgs) { + } + + /** + * Controls how the background fade is triggered when there is an entering Activity transition. + * If fadeEarly is true, the Window background will fade in as soon as the shared elements are + * ready to switch. If fadeEarly is false, the background will fade only after the calling + * Activity's exit transition completes. By default, the Window will fade in when the calling + * Activity's exit transition completes. + * + * @param fadeEarly Set to true to fade out the exiting Activity as soon as the shared elements + * are transferred. Set to false to fade out the exiting Activity as soon as + * the shared element is transferred. + * @see android.app.ActivityOptions#makeSceneTransitionAnimation(android.os.Bundle) + */ + public void setEarlyBackgroundTransition(boolean fadeEarly) { + if (mTransitionActivityOptions == null) { + return; + } + mWindow.setEarlyBackgroundTransition(fadeEarly); + } + /** * @hide */ @@ -5530,7 +5598,7 @@ public class Activity extends ContextThemeWrapper return mResumed; } - void dispatchActivityResult(String who, int requestCode, + void dispatchActivityResult(String who, int requestCode, int resultCode, Intent data) { if (false) Log.v( TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 582ce3c1e186..3f97c40149fe 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -16,16 +16,21 @@ package android.app; +import android.animation.Animator; import android.content.Context; import android.graphics.Bitmap; import android.os.Bundle; import android.os.Handler; import android.os.IRemoteCallback; import android.os.RemoteException; -import android.text.TextUtils; +import android.transition.Transition; +import android.util.ArrayMap; import android.util.Log; import android.view.View; +import java.util.ArrayList; +import java.util.Map; + /** * Helper class for building an options Bundle that can be used with * {@link android.content.Context#startActivity(android.content.Intent, android.os.Bundle) @@ -95,33 +100,29 @@ public class ActivityOptions { public static final String KEY_ANIM_START_LISTENER = "android:animStartListener"; /** - * A string array of names for the destination scene. This defines an API in the same - * way that intent action or extra names do and should follow a similar convention: - * "com.example.scene.FOO" - * + * Arguments for the scene transition about to begin. * @hide */ - public static final String KEY_DEST_SCENE_NAMES = "android:destSceneNames"; + public static final String KEY_SCENE_TRANSITION_ARGS = "android:sceneTransitionArgs"; /** - * A string indicating the destination scene name that was chosen by the target. - * Used by {@link OnSceneTransitionStartedListener}. - * @hide + * For Activity transitions, the calling Activity's TransitionListener used to + * notify the called Activity when the shared element and the exit transitions + * complete. */ - public static final String KEY_DEST_SCENE_NAME_CHOSEN = "android:destSceneNameChosen"; + private static final String KEY_TRANSITION_COMPLETE_LISTENER + = "android:transitionCompleteListener"; /** - * Callback for when scene transition is started. - * @hide + * For Activity transitions, the called Activity's listener to receive calls + * when transitions complete. */ - public static final String KEY_SCENE_TRANSITION_START_LISTENER = - "android:sceneTransitionStartListener"; + private static final String KEY_TRANSITION_TARGET_LISTENER = "android:transitionTargetListener"; /** - * Arguments for the scene transition about to begin. - * @hide + * The shared element's texture ID (TODO: not used yet). */ - public static final String KEY_SCENE_TRANSITION_ARGS = "android:sceneTransitionArgs"; + private static final String KEY_SHARED_ELEMENT_TEXTURE_ID = "android:sharedElementTextureId"; /** @hide */ public static final int ANIM_NONE = 0; @@ -145,10 +146,9 @@ public class ActivityOptions { private int mStartY; private int mStartWidth; private int mStartHeight; - private String[] mDestSceneNames; private Bundle mTransitionArgs; private IRemoteCallback mAnimationStartedListener; - private IRemoteCallback mSceneTransitionStartedListener; + private IRemoteCallback mTransitionCompleteListener; /** * Create an ActivityOptions specifying a custom animation to run when @@ -215,24 +215,6 @@ public class ActivityOptions { } } - private void setOnSceneTransitionStartedListener(Handler handler, - OnSceneTransitionStartedListener listener) { - if (listener != null) { - final Handler h = handler; - final OnSceneTransitionStartedListener l = listener; - mSceneTransitionStartedListener = new IRemoteCallback.Stub() { - @Override public void sendResult(final Bundle data) throws RemoteException { - h.post(new Runnable() { - public void run() { - l.onSceneTransitionStarted(data != null ? - data.getString(KEY_DEST_SCENE_NAME_CHOSEN) : null); - } - }); - } - }; - } - } - /** * Callback for use with {@link ActivityOptions#makeThumbnailScaleUpAnimation} * to find out when the given animation has started running. @@ -242,13 +224,10 @@ public class ActivityOptions { void onAnimationStarted(); } - /** - * Callback for use with {@link ActivityOptions#makeSceneTransitionAnimation} - * to find out when a transition is about to begin. - * @hide - */ - public interface OnSceneTransitionStartedListener { - void onSceneTransitionStarted(String destSceneName); + /** @hide */ + public interface ActivityTransitionTarget { + void sharedElementTransitionComplete(); + void exitTransitionComplete(); } /** @@ -369,18 +348,35 @@ public class ActivityOptions { } /** - * Create an ActivityOptions specifying an animation where an activity window is asked - * to perform animations within the window content. + * Create an ActivityOptions to transition between Activities using cross-Activity animation. + * When visual elements are to carry between Activities, args should be used to tell the called + * Activity about the location and size. * - * @hide + * TODO: Provide facility to capture layout and bitmap of shared elements. + * + *

When + * {@link android.app.Activity#startActivities(android.content.Intent[], android.os.Bundle)} + * is used with the {@link #toBundle()} result, the Activity's content scene will automatically + * transition out by setting their visibility to {@link View#INVISIBLE}. Shared elements + * ({@link android.view.View#setSharedElementName(String)}) are unmodified during the + * transition to allow the started Activity to seamlessly take it over. ViewGroups typically + * don't transition out, and instead transition out their children unless they have a + * background. To modify this behavior, use + * {@link android.view.ViewGroup#setTransitionGroup(boolean)}.

+ * + *

This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS} to be + * enabled on the calling Activity to cause an exit transition. The same must be in + * the called Activity to get an entering transition.

+ * + * @param args Contains information for transferring a view between this Activity and the + * target Activity. Will be used by the called Activity to transition the + * view to its eventual destination + * @see android.app.Activity#startSharedElementTransition(android.os.Bundle) */ - public static ActivityOptions makeSceneTransitionAnimation(String[] destSceneNames, - Bundle args, OnSceneTransitionStartedListener listener, Handler handler) { + public static ActivityOptions makeSceneTransitionAnimation(Bundle args) { ActivityOptions opts = new ActivityOptions(); opts.mAnimationType = ANIM_SCENE_TRANSITION; - opts.mDestSceneNames = destSceneNames; opts.mTransitionArgs = args; - opts.setOnSceneTransitionStartedListener(handler, listener); return opts; } @@ -416,10 +412,9 @@ public class ActivityOptions { break; case ANIM_SCENE_TRANSITION: - mDestSceneNames = opts.getStringArray(KEY_DEST_SCENE_NAMES); mTransitionArgs = opts.getBundle(KEY_SCENE_TRANSITION_ARGS); - mSceneTransitionStartedListener = IRemoteCallback.Stub.asInterface( - opts.getBinder(KEY_SCENE_TRANSITION_START_LISTENER)); + mTransitionCompleteListener = IRemoteCallback.Stub.asInterface( + opts.getBinder(KEY_TRANSITION_COMPLETE_LISTENER)); break; } } @@ -469,11 +464,6 @@ public class ActivityOptions { return mStartHeight; } - /** @hide */ - public String[] getDestSceneNames() { - return mDestSceneNames; - } - /** @hide */ public Bundle getSceneTransitionArgs() { return mTransitionArgs; @@ -485,19 +475,33 @@ public class ActivityOptions { } /** @hide */ - public void dispatchSceneTransitionStarted(String destScene) { - if (mSceneTransitionStartedListener != null) { - Bundle data = null; - if (!TextUtils.isEmpty(destScene)) { - data = new Bundle(); - data.putString(KEY_DEST_SCENE_NAME_CHOSEN, destScene); - } + public void dispatchSceneTransitionStarted(final ActivityTransitionTarget target) { + boolean listenerSent = false; + if (mTransitionCompleteListener != null) { + IRemoteCallback callback = new IRemoteCallback.Stub() { + @Override + public void sendResult(Bundle data) throws RemoteException { + if (data == null) { + target.exitTransitionComplete(); + } else { + // TODO: Use texture id + target.sharedElementTransitionComplete(); + } + } + }; + Bundle bundle = new Bundle(); + bundle.putBinder(KEY_TRANSITION_TARGET_LISTENER, callback.asBinder()); try { - mSceneTransitionStartedListener.sendResult(data); + mTransitionCompleteListener.sendResult(bundle); + listenerSent = true; } catch (RemoteException e) { - Log.e(TAG, "Caught exception dispatching scene transition start", e); + Log.w(TAG, "Couldn't retrieve transition notifications", e); } } + if (!listenerSent) { + target.sharedElementTransitionComplete(); + target.exitTransitionComplete(); + } } /** @hide */ @@ -508,12 +512,6 @@ public class ActivityOptions { } catch (RemoteException e) { } } - if (mSceneTransitionStartedListener != null) { - try { - mSceneTransitionStartedListener.sendResult(null); - } catch (RemoteException e) { - } - } } /** @hide */ @@ -545,9 +543,8 @@ public class ActivityOptions { } } mAnimationStartedListener = otherOptions.mAnimationStartedListener; - mSceneTransitionStartedListener = null; + mTransitionCompleteListener = null; mTransitionArgs = null; - mDestSceneNames = null; break; case ANIM_SCALE_UP: mAnimationType = otherOptions.mAnimationType; @@ -562,9 +559,8 @@ public class ActivityOptions { } } mAnimationStartedListener = null; - mSceneTransitionStartedListener = null; + mTransitionCompleteListener = null; mTransitionArgs = null; - mDestSceneNames = null; break; case ANIM_THUMBNAIL_SCALE_UP: case ANIM_THUMBNAIL_SCALE_DOWN: @@ -579,20 +575,12 @@ public class ActivityOptions { } } mAnimationStartedListener = otherOptions.mAnimationStartedListener; - mSceneTransitionStartedListener = null; + mTransitionCompleteListener = null; mTransitionArgs = null; - mDestSceneNames = null; break; case ANIM_SCENE_TRANSITION: mAnimationType = otherOptions.mAnimationType; - if (mSceneTransitionStartedListener != null) { - try { - mSceneTransitionStartedListener.sendResult(null); - } catch (RemoteException e) { - } - } - mSceneTransitionStartedListener = otherOptions.mSceneTransitionStartedListener; - mDestSceneNames = otherOptions.mDestSceneNames; + mTransitionCompleteListener = otherOptions.mTransitionCompleteListener; mTransitionArgs = otherOptions.mTransitionArgs; mThumbnail = null; mAnimationStartedListener = null; @@ -639,10 +627,11 @@ public class ActivityOptions { break; case ANIM_SCENE_TRANSITION: b.putInt(KEY_ANIM_TYPE, mAnimationType); - b.putStringArray(KEY_DEST_SCENE_NAMES, mDestSceneNames); b.putBundle(KEY_SCENE_TRANSITION_ARGS, mTransitionArgs); - b.putBinder(KEY_SCENE_TRANSITION_START_LISTENER, mSceneTransitionStartedListener - != null ? mSceneTransitionStartedListener.asBinder() : null); + if (mTransitionCompleteListener != null) { + b.putBinder(KEY_TRANSITION_COMPLETE_LISTENER, + mTransitionCompleteListener.asBinder()); + } break; } return b; @@ -661,4 +650,123 @@ public class ActivityOptions { return null; } + + /** @hide */ + public interface SharedElementSource { + int getTextureId(); + } + + /** + * In the calling Activity when transitioning out, sets the Transition to listen for + * changes. + * @hide + */ + public void setExitTransition(Transition transition, SharedElementSource sharedElementSource) { + mTransitionCompleteListener = new ExitTransitionListener(transition, sharedElementSource); + } + + private static class ExitTransitionListener extends IRemoteCallback.Stub + implements Transition.TransitionListener, Animator.AnimatorListener { + private ArrayList mSharedElementAnimators = new ArrayList(); + private boolean mSharedElementNotified; + private Transition mExitTransition; + private IRemoteCallback mTransitionCompleteCallback; + private boolean mExitComplete; + private SharedElementSource mSharedElementSource; + + public ExitTransitionListener(Transition transition, SharedElementSource sharedElementSource) { + mSharedElementSource = sharedElementSource; + mExitTransition = transition; + mExitTransition.addListener(this); + } + + @Override + public void sendResult(Bundle data) throws RemoteException { + if (data != null) { + mTransitionCompleteCallback = IRemoteCallback.Stub.asInterface( + data.getBinder(KEY_TRANSITION_TARGET_LISTENER)); + notifySharedElement(); + notifyExit(); + } + } + + @Override + public void onTransitionStart(Transition transition) { + ArrayMap runningAnimators + = Transition.getRunningAnimators(); + for (Map.Entry entry : runningAnimators.entrySet()) { + if (entry.getValue().view.getSharedElementName() != null) { + mSharedElementAnimators.add(entry.getKey()); + entry.getKey().addListener(this); + } + } + notifySharedElement(); + } + + @Override + public void onTransitionEnd(Transition transition) { + mExitComplete = true; + notifyExit(); + mExitTransition.removeListener(this); + } + + @Override + public void onTransitionCancel(Transition transition) { + mExitComplete = true; + notifyExit(); + mExitTransition.removeListener(this); + } + + @Override + public void onTransitionPause(Transition transition) { + } + + @Override + public void onTransitionResume(Transition transition) { + } + + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + mSharedElementAnimators.remove(animation); + notifySharedElement(); + } + + @Override + public void onAnimationCancel(Animator animation) { + mSharedElementAnimators.remove(animation); + notifySharedElement(); + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + private void notifySharedElement() { + if (!mSharedElementNotified && mSharedElementAnimators.isEmpty() + && mTransitionCompleteCallback != null) { + mSharedElementNotified = true; + try { + Bundle bundle = new Bundle(); + bundle.putInt(KEY_SHARED_ELEMENT_TEXTURE_ID, mSharedElementSource.getTextureId()); + mTransitionCompleteCallback.sendResult(bundle); + } catch (RemoteException e) { + Log.w(TAG, "Couldn't notify that the transition ended", e); + } + } + } + + private void notifyExit() { + if (mExitComplete && mTransitionCompleteCallback != null) { + try { + mTransitionCompleteCallback.sendResult(null); + } catch (RemoteException e) { + Log.w(TAG, "Couldn't notify that the transition ended", e); + } + } + } + } } diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java index da9ba5a7fd75..fd3f9b3c86f7 100644 --- a/core/java/android/transition/Transition.java +++ b/core/java/android/transition/Transition.java @@ -552,7 +552,8 @@ public abstract class Transition implements Cloneable { return false; } - private static ArrayMap getRunningAnimators() { + /** @hide */ + public static ArrayMap getRunningAnimators() { ArrayMap runningAnimators = sRunningAnimators.get(); if (runningAnimators == null) { runningAnimators = new ArrayMap(); @@ -1077,6 +1078,9 @@ public abstract class Transition implements Cloneable { if (view == null) { return; } + if (!isValidTarget(view, view.getId())) { + return; + } boolean isListViewItem = false; if (view.getParent() instanceof ListView) { isListViewItem = true; @@ -1467,6 +1471,10 @@ public abstract class Transition implements Cloneable { mCanRemoveViews = canRemoveViews; } + public boolean canRemoveViews() { + return mCanRemoveViews; + } + @Override public String toString() { return toString(""); @@ -1629,9 +1637,10 @@ public abstract class Transition implements Cloneable { * animation should be canceled or a new animation noop'd. The structure holds * information about the state that an animation is going to, to be compared to * end state of a new animation. + * @hide */ - private static class AnimationInfo { - View view; + public static class AnimationInfo { + public View view; String name; TransitionValues values; diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 4b6f2b072f43..518908f8fcb6 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -665,6 +665,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @attr ref android.R.styleable#View_scrollbarTrackVertical * @attr ref android.R.styleable#View_scrollbarAlwaysDrawHorizontalTrack * @attr ref android.R.styleable#View_scrollbarAlwaysDrawVerticalTrack + * @attr ref android.R.styleable#View_sharedElementName * @attr ref android.R.styleable#View_soundEffectsEnabled * @attr ref android.R.styleable#View_tag * @attr ref android.R.styleable#View_textAlignment @@ -4038,6 +4039,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, case R.styleable.View_accessibilityLiveRegion: setAccessibilityLiveRegion(a.getInt(attr, ACCESSIBILITY_LIVE_REGION_DEFAULT)); break; + case R.styleable.View_sharedElementName: + setSharedElementName(a.getString(attr)); + break; } } @@ -18871,6 +18875,35 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return mAnimator; } + /** + * Specifies that the shared name of the View to be shared with another Activity. + * When transitioning between Activities, the name links a UI element in the starting + * Activity to UI element in the called Activity. Names should be unique in the + * View hierarchy. + * + * @param sharedElementName The cross-Activity View identifier. The called Activity will use + * the name to match the location with a View in its layout. + * @see android.app.ActivityOptions#makeSceneTransitionAnimation(android.os.Bundle) + */ + public void setSharedElementName(String sharedElementName) { + setTagInternal(com.android.internal.R.id.shared_element_name, sharedElementName); + } + + /** + * Returns the shared name of the View to be shared with another Activity. + * When transitioning between Activities, the name links a UI element in the starting + * Activity to UI element in the called Activity. Names should be unique in the + * View hierarchy. + * + *

This returns null if the View is not a shared element or the name if it is.

+ * + * @return The name used for this View for cross-Activity transitions or null if + * this View has not been identified as shared. + */ + public String getSharedElementName() { + return (String) getTag(com.android.internal.R.id.shared_element_name); + } + /** * Interface definition for a callback to be invoked when a hardware key event is * dispatched to this view. The callback will be invoked before the key event is diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index b91091fbcca4..73b108f77f11 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -360,6 +360,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ static final int FLAG_ISOLATED_Z_VOLUME = 0x1000000; + static final int FLAG_IS_TRANSITION_GROUP = 0x2000000; + + static final int FLAG_IS_TRANSITION_GROUP_SET = 0x4000000; + /** * Indicates which types of drawing caches are to be kept in memory. * This field should be made private, so it is hidden from the SDK. @@ -556,6 +560,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager case R.styleable.ViewGroup_layoutMode: setLayoutMode(a.getInt(attr, LAYOUT_MODE_UNDEFINED)); break; + case R.styleable.ViewGroup_transitionGroup: + setTransitionGroup(a.getBoolean(attr, false)); + break; } } @@ -2287,6 +2294,41 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) == FLAG_SPLIT_MOTION_EVENTS; } + /** + * Returns true if this ViewGroup should be considered as a single entity for removal + * when executing an Activity transition. If this is false, child elements will move + * individually during the transition. + * @return True if the ViewGroup should be acted on together during an Activity transition. + * The default value is false when the background is null and true when the background + * is not null. + * @see android.app.ActivityOptions#makeSceneTransitionAnimation(android.os.Bundle) + */ + public boolean isTransitionGroup() { + if ((mGroupFlags & FLAG_IS_TRANSITION_GROUP_SET) != 0) { + return ((mGroupFlags & FLAG_IS_TRANSITION_GROUP) != 0); + } else { + return getBackground() != null; + } + } + + /** + * Changes whether or not this ViewGroup should be treated as a single entity during + * ActivityTransitions. + * @param isTransitionGroup Whether or not the ViewGroup should be treated as a unit + * in Activity transitions. If false, the ViewGroup won't transition, + * only its children. If true, the entire ViewGroup will transition + * together. + * @see android.app.ActivityOptions#makeSceneTransitionAnimation(android.os.Bundle) + */ + public void setTransitionGroup(boolean isTransitionGroup) { + mGroupFlags |= FLAG_IS_TRANSITION_GROUP_SET; + if (isTransitionGroup) { + mGroupFlags |= FLAG_IS_TRANSITION_GROUP; + } else { + mGroupFlags &= ~FLAG_IS_TRANSITION_GROUP; + } + } + /** * {@inheritDoc} */ diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 1064a08a379d..11740abc3d3b 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -18,6 +18,7 @@ package android.view; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityOptions; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; @@ -1374,8 +1375,41 @@ public abstract class Window { /** * Set options that can affect the transition behavior within this window. * @param options Options to set or null for none + * @hide */ - public void setTransitionOptions(Bundle options) { - throw new UnsupportedOperationException(); + public void setTransitionOptions(ActivityOptions options, SceneTransitionListener listener) { + } + + /** + * A callback for Activity transitions to be told when the shared element is ready to be shown + * and start the transition to its target location. + * @hide + */ + public interface SceneTransitionListener { + void enterSharedElement(Bundle transitionArgs); + void nullPendingTransition(); + void convertFromTranslucent(); + void convertToTranslucent(); + } + + /** + * Controls how the background fade is triggered. If fadeEarly is true, the Window background + * will fade in as soon as the shared elements are ready to switch. If fadeEarly is false, + * the background will fade only after the calling Activity's exit transition completes. + * By default, the Window will fade in when the calling Activity's exit transition completes. + * + * @param fadeEarly Set to true to fade out the exiting Activity as soon as the shared elements + * are transferred. Set to false to fade out the exiting Activity as soon as + * the shared element is transferred. + * @hide + */ + public void setEarlyBackgroundTransition(boolean fadeEarly) { + } + + /** + * Start the exit transition. + * @hide + */ + public void startExitTransition(ActivityOptions activityOptions) { } } diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index f77d66b19c22..9526e13d5c54 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2250,6 +2250,11 @@ view with a theme override will inherit the themed context. --> + + + + +