diff --git a/api/current.txt b/api/current.txt index 1e356478126fb..2970f2b168d83 100644 --- a/api/current.txt +++ b/api/current.txt @@ -29496,19 +29496,18 @@ package android.view.transition { public abstract class Transition implements java.lang.Cloneable { ctor public Transition(); method public void addListener(android.view.transition.Transition.TransitionListener); - method protected void cancelTransition(); + method protected void cancel(); method protected abstract void captureValues(android.view.transition.TransitionValues, boolean); method public android.view.transition.Transition clone(); method public long getDuration(); method public android.animation.TimeInterpolator getInterpolator(); method public java.util.ArrayList getListeners(); + method public java.lang.String getName(); method public long getStartDelay(); method public int[] getTargetIds(); method public android.view.View[] getTargets(); + method public java.lang.String[] getTransitionProperties(); method protected android.view.transition.TransitionValues getTransitionValues(android.view.View, boolean); - method protected void onTransitionCancel(); - method protected void onTransitionEnd(); - method protected void onTransitionStart(); method protected android.animation.Animator play(android.view.ViewGroup, android.view.transition.TransitionValues, android.view.transition.TransitionValues); method public void removeListener(android.view.transition.Transition.TransitionListener); method public android.view.transition.Transition setDuration(long); @@ -29521,6 +29520,8 @@ package android.view.transition { public static abstract interface Transition.TransitionListener { method public abstract void onTransitionCancel(android.view.transition.Transition); method public abstract void onTransitionEnd(android.view.transition.Transition); + method public abstract void onTransitionPause(android.view.transition.Transition); + method public abstract void onTransitionResume(android.view.transition.Transition); method public abstract void onTransitionStart(android.view.transition.Transition); } @@ -29567,6 +29568,7 @@ package android.view.transition { method protected android.animation.Animator appear(android.view.ViewGroup, android.view.transition.TransitionValues, int, android.view.transition.TransitionValues, int); method protected void captureValues(android.view.transition.TransitionValues, boolean); method protected android.animation.Animator disappear(android.view.ViewGroup, android.view.transition.TransitionValues, int, android.view.transition.TransitionValues, int); + method public boolean isVisible(android.view.transition.TransitionValues); } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index f574efadbc300..c874c82db361b 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -5356,6 +5356,18 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + /** + * Returns whether layout calls on this container are currently being + * suppressed, due to an earlier call to {@link #suppressLayout(boolean)}. + * + * @return true if layout calls are currently suppressed, false otherwise. + * + * @hide + */ + public boolean isLayoutSuppressed() { + return mSuppressLayout; + } + /** * {@inheritDoc} */ diff --git a/core/java/android/view/transition/Fade.java b/core/java/android/view/transition/Fade.java index 4fd60c14f47ec..45c21d888ac58 100644 --- a/core/java/android/view/transition/Fade.java +++ b/core/java/android/view/transition/Fade.java @@ -19,6 +19,7 @@ package android.view.transition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -35,6 +36,7 @@ public class Fade extends Visibility { private static boolean DBG = Transition.DBG && false; private static final String LOG_TAG = "Fade"; + private static final String PROPNAME_ALPHA = "android:fade:alpha"; private static final String PROPNAME_SCREEN_X = "android:fade:screenX"; private static final String PROPNAME_SCREEN_Y = "android:fade:screenY"; @@ -74,26 +76,51 @@ public class Fade extends Visibility { /** * Utility method to handle creating and running the Animator. */ - private Animator runAnimation(View view, float startAlpha, float endAlpha, - Animator.AnimatorListener listener) { + private Animator createAnimation(View view, float startAlpha, float endAlpha, + AnimatorListenerAdapter listener) { + if (startAlpha == endAlpha) { + // run listener if we're noop'ing the animation, to get the end-state results now + if (listener != null) { + listener.onAnimationEnd(null); + } + return null; + } final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", startAlpha, endAlpha); if (listener != null) { anim.addListener(listener); + anim.addPauseListener(listener); } - // TODO: Maybe extract a method into Transition to run an animation that handles the - // duration/startDelay stuff for all subclasses. return anim; } @Override protected void captureValues(TransitionValues values, boolean start) { super.captureValues(values, start); + float alpha = values.view.getAlpha(); + values.values.put(PROPNAME_ALPHA, alpha); int[] loc = new int[2]; values.view.getLocationOnScreen(loc); values.values.put(PROPNAME_SCREEN_X, loc[0]); values.values.put(PROPNAME_SCREEN_Y, loc[1]); } + @Override + protected Animator play(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + Animator animator = super.play(sceneRoot, startValues, endValues); + if (animator == null && startValues != null && endValues != null) { + boolean endVisible = isVisible(endValues); + final View endView = endValues.view; + float endAlpha = endView.getAlpha(); + float startAlpha = (Float) startValues.values.get(PROPNAME_ALPHA); + if ((endVisible && startAlpha < endAlpha && (mFadingMode & Fade.IN) != 0) || + (!endVisible && startAlpha > endAlpha && (mFadingMode & Fade.OUT) != 0)) { + animator = createAnimation(endView, startAlpha, endAlpha, null); + } + } + return animator; + } + @Override protected Animator appear(ViewGroup sceneRoot, TransitionValues startValues, int startVisibility, @@ -102,15 +129,11 @@ public class Fade extends Visibility { return null; } final View endView = endValues.view; - endView.setAlpha(0); - final Animator.AnimatorListener endListener = new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - // Always end animation with full alpha, in case it's canceled mid-stream - endView.setAlpha(1); - } - }; - return runAnimation(endView, 0, 1, endListener); + // if alpha < 1, just fade it in from the current value + if (endView.getAlpha() == 1.0f) { + endView.setAlpha(0); + } + return createAnimation(endView, endView.getAlpha(), 1, null); } @Override @@ -129,7 +152,7 @@ public class Fade extends Visibility { } View overlayView = null; View viewToKeep = null; - if (endView == null) { + if (endView == null || endView.getParent() == null) { // view was removed: add the start view to the Overlay view = startView; overlayView = view; @@ -167,7 +190,7 @@ public class Fade extends Visibility { final View finalOverlayView = overlayView; final View finalViewToKeep = viewToKeep; final ViewGroup finalSceneRoot = sceneRoot; - final Animator.AnimatorListener endListener = new AnimatorListenerAdapter() { + final AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { finalView.setAlpha(startAlpha); @@ -179,8 +202,22 @@ public class Fade extends Visibility { finalSceneRoot.getOverlay().remove(finalOverlayView); } } + + @Override + public void onAnimationPause(Animator animation) { + if (finalOverlayView != null) { + finalSceneRoot.getOverlay().remove(finalOverlayView); + } + } + + @Override + public void onAnimationResume(Animator animation) { + if (finalOverlayView != null) { + finalSceneRoot.getOverlay().add(finalOverlayView); + } + } }; - return runAnimation(view, startAlpha, endAlpha, endListener); + return createAnimation(view, startAlpha, endAlpha, endListener); } if (viewToKeep != null) { // TODO: find a different way to do this, like just changing the view to be @@ -193,12 +230,42 @@ public class Fade extends Visibility { final View finalOverlayView = overlayView; final View finalViewToKeep = viewToKeep; final ViewGroup finalSceneRoot = sceneRoot; - final Animator.AnimatorListener endListener = new AnimatorListenerAdapter() { + final AnimatorListenerAdapter endListener = new AnimatorListenerAdapter() { + boolean mCanceled = false; + float mPausedAlpha = -1; + + @Override + public void onAnimationPause(Animator animation) { + if (finalViewToKeep != null && !mCanceled) { + finalViewToKeep.setVisibility(finalVisibility); + } + mPausedAlpha = finalView.getAlpha(); + finalView.setAlpha(startAlpha); + } + + @Override + public void onAnimationResume(Animator animation) { + if (finalViewToKeep != null && !mCanceled) { + finalViewToKeep.setVisibility(View.VISIBLE); + } + finalView.setAlpha(mPausedAlpha); + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + if (mPausedAlpha >= 0) { + finalView.setAlpha(mPausedAlpha); + } + } + @Override public void onAnimationEnd(Animator animation) { - finalView.setAlpha(startAlpha); + if (!mCanceled) { + finalView.setAlpha(startAlpha); + } // TODO: restore view offset from overlay repositioning - if (finalViewToKeep != null) { + if (finalViewToKeep != null && !mCanceled) { finalViewToKeep.setVisibility(finalVisibility); } if (finalOverlayView != null) { @@ -206,7 +273,7 @@ public class Fade extends Visibility { } } }; - return runAnimation(view, startAlpha, endAlpha, endListener); + return createAnimation(view, startAlpha, endAlpha, endListener); } return null; } diff --git a/core/java/android/view/transition/Move.java b/core/java/android/view/transition/Move.java index ceda5a5f6f6e6..ae7d75938c1d1 100644 --- a/core/java/android/view/transition/Move.java +++ b/core/java/android/view/transition/Move.java @@ -25,8 +25,6 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; -import android.util.ArrayMap; -import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -42,6 +40,13 @@ public class Move extends Transition { private static final String PROPNAME_PARENT = "android:move:parent"; private static final String PROPNAME_WINDOW_X = "android:move:windowX"; private static final String PROPNAME_WINDOW_Y = "android:move:windowY"; + private static String[] sTransitionProperties = { + PROPNAME_BOUNDS, + PROPNAME_PARENT, + PROPNAME_WINDOW_X, + PROPNAME_WINDOW_Y + }; + int[] tempLocation = new int[2]; boolean mResizeClip = false; boolean mReparent = false; @@ -49,6 +54,11 @@ public class Move extends Transition { private static RectEvaluator sRectEvaluator = new RectEvaluator(); + @Override + public String[] getTransitionProperties() { + return sTransitionProperties; + } + public void setResizeClip(boolean resizeClip) { mResizeClip = resizeClip; } @@ -146,12 +156,33 @@ public class Move extends Transition { if (view.getParent() instanceof ViewGroup) { final ViewGroup parent = (ViewGroup) view.getParent(); parent.suppressLayout(true); - anim.addListener(new AnimatorListenerAdapter() { + TransitionListener transitionListener = new TransitionListenerAdapter() { + boolean mCanceled = false; + @Override - public void onAnimationEnd(Animator animation) { + public void onTransitionCancel(Transition transition) { + parent.suppressLayout(false); + mCanceled = true; + } + + @Override + public void onTransitionEnd(Transition transition) { + if (!mCanceled) { + parent.suppressLayout(false); + } + } + + @Override + public void onTransitionPause(Transition transition) { parent.suppressLayout(false); } - }); + + @Override + public void onTransitionResume(Transition transition) { + parent.suppressLayout(true); + } + }; + addListener(transitionListener); } return anim; } else { @@ -191,12 +222,33 @@ public class Move extends Transition { if (view.getParent() instanceof ViewGroup) { final ViewGroup parent = (ViewGroup) view.getParent(); parent.suppressLayout(true); - anim.addListener(new AnimatorListenerAdapter() { + TransitionListener transitionListener = new TransitionListenerAdapter() { + boolean mCanceled = false; + @Override - public void onAnimationEnd(Animator animation) { + public void onTransitionCancel(Transition transition) { + parent.suppressLayout(false); + mCanceled = true; + } + + @Override + public void onTransitionEnd(Transition transition) { + if (!mCanceled) { + parent.suppressLayout(false); + } + } + + @Override + public void onTransitionPause(Transition transition) { parent.suppressLayout(false); } - }); + + @Override + public void onTransitionResume(Transition transition) { + parent.suppressLayout(true); + } + }; + addListener(transitionListener); } anim.addListener(new AnimatorListenerAdapter() { @Override diff --git a/core/java/android/view/transition/Transition.java b/core/java/android/view/transition/Transition.java index f99ddc0f018d3..0444843ef2221 100644 --- a/core/java/android/view/transition/Transition.java +++ b/core/java/android/view/transition/Transition.java @@ -22,7 +22,6 @@ import android.animation.TimeInterpolator; import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; -import android.util.Pair; import android.util.SparseArray; import android.view.SurfaceView; import android.view.TextureView; @@ -60,6 +59,8 @@ public abstract class Transition implements Cloneable { private static final String LOG_TAG = "Transition"; static final boolean DBG = false; + private String mName = getClass().getName(); + long mStartDelay = -1; long mDuration = -1; TimeInterpolator mInterpolator = null; @@ -69,29 +70,29 @@ public abstract class Transition implements Cloneable { private TransitionValuesMaps mEndValues = new TransitionValuesMaps(); TransitionGroup mParent = null; + // Per-animator information used for later canceling when future transitions overlap + private static ThreadLocal> sRunningAnimators = + new ThreadLocal>(); + // Scene Root is set at play() time in the cloned Transition ViewGroup mSceneRoot = null; - // Used to carry data between setup() and play(), cleared before every scene transition - private ArrayList mPlayStartValuesList = new ArrayList(); - private ArrayList mPlayEndValuesList = new ArrayList(); - // Track all animators in use in case the transition gets canceled and needs to // cancel running animators private ArrayList mCurrentAnimators = new ArrayList(); // Number of per-target instances of this Transition currently running. This count is - // determined by calls to startTransition() and endTransition() + // determined by calls to start() and end() int mNumInstances = 0; - + // Whether this transition is currently paused, due to a call to pause() + boolean mPaused = false; // The set of listeners to be sent transition lifecycle events. ArrayList mListeners = null; // The set of animators collected from calls to play(), to be run in runAnimations() - ArrayMap, Animator> mAnimatorMap = - new ArrayMap, Animator>(); + ArrayList mAnimators = new ArrayList(); /** * Constructs a Transition object with no target objects. A transition with @@ -115,6 +116,14 @@ public abstract class Transition implements Cloneable { return this; } + /** + * Returns the duration set on this transition. If no duration has been set, + * the returned value will be negative, indicating that resulting animators will + * retain their own durations. + * + * @return The duration set on this transition, if one has been set, otherwise + * returns a negative number. + */ public long getDuration() { return mDuration; } @@ -131,6 +140,14 @@ public abstract class Transition implements Cloneable { mStartDelay = startDelay; } + /** + * Returns the startDelay set on this transition. If no startDelay has been set, + * the returned value will be negative, indicating that resulting animators will + * retain their own startDelays. + * + * @return The startDealy set on this transition, if one has been set, otherwise + * returns a negative number. + */ public long getStartDelay() { return mStartDelay; } @@ -147,10 +164,43 @@ public abstract class Transition implements Cloneable { mInterpolator = interpolator; } + /** + * Returns the interpolator set on this transition. If no interpolator has been set, + * the returned value will be null, indicating that resulting animators will + * retain their own interpolators. + * + * @return The interpolator set on this transition, if one has been set, otherwise + * returns null. + */ public TimeInterpolator getInterpolator() { return mInterpolator; } + /** + * Returns the set of property names used stored in the {@link TransitionValues} + * object passed into {@link #captureValues(TransitionValues, boolean)} that + * this transition cares about for the purposes of canceling overlapping animations. + * When any transition is started on a given scene root, all transitions + * currently running on that same scene root are checked to see whether the + * properties on which they based their animations agree with the end values of + * the same properties in the new transition. If the end values are not equal, + * then the old animation is canceled since the new transition will start a new + * animation to these new values. If the values are equal, the old animation is + * allowed to continue and no new animation is started for that transition. + * + *

A transition does not need to override this method. However, not doing so + * will mean that the cancellation logic outlined in the previous paragraph + * will be skipped for that transition, possibly leading to artifacts as + * old transitions and new transitions on the same targets run in parallel, + * animating views toward potentially different end values.

+ * + * @return An array of property names as described in the class documentation for + * {@link TransitionValues}. The default implementation returns null. + */ + public String[] getTransitionProperties() { + return null; + } + /** * This method is called by the transition's parent (all the way up to the * topmost Transition in the hierarchy) with the sceneRoot and start/end @@ -210,8 +260,6 @@ public abstract class Transition implements Cloneable { if (DBG) { Log.d(LOG_TAG, "play() for " + this); } - mPlayStartValuesList.clear(); - mPlayEndValuesList.clear(); ArrayMap endCopy = new ArrayMap(endValues.viewValues); SparseArray endIdCopy = @@ -316,6 +364,7 @@ public abstract class Transition implements Cloneable { startValuesList.add(start); endValuesList.add(end); } + ArrayMap runningAnimators = getRunningAnimators(); for (int i = 0; i < startValuesList.size(); ++i) { TransitionValues start = startValuesList.get(i); TransitionValues end = endValuesList.get(i); @@ -345,14 +394,46 @@ public abstract class Transition implements Cloneable { // TODO: what to do about targetIds and itemIds? Animator animator = play(sceneRoot, start, end); if (animator != null) { - mAnimatorMap.put(new Pair(start, end), animator); - // Note: we've already done the check against targetIDs in these lists - mPlayStartValuesList.add(start); - mPlayEndValuesList.add(end); + // Save animation info for future cancellation purposes + View view = null; + TransitionValues infoValues = null; + if (end != null) { + view = end.view; + String[] properties = getTransitionProperties(); + if (view != null && properties != null && properties.length > 0) { + infoValues = new TransitionValues(); + infoValues.view = view; + TransitionValues newValues = endValues.viewValues.get(view); + if (newValues != null) { + for (int j = 0; j < properties.length; ++j) { + infoValues.values.put(properties[j], + newValues.values.get(properties[j])); + } + } + int numExistingAnims = runningAnimators.size(); + for (int j = 0; j < numExistingAnims; ++j) { + Animator anim = runningAnimators.keyAt(j); + AnimationInfo info = runningAnimators.get(anim); + if (info.values != null && info.view == view && + ((info.name == null && getName() == null) || + info.name.equals(getName()))) { + if (info.values.equals(infoValues)) { + // Favor the old animator + animator = null; + break; + } + } + } + } + } else { + view = (start != null) ? start.view : null; + } + if (animator != null) { + AnimationInfo info = new AnimationInfo(view, getName(), infoValues); + runningAnimators.put(animator, info); + mAnimators.add(animator); + } } - } else if (DBG) { - View view = (end != null) ? end.view : start.view; - Log.d(LOG_TAG, " No change for view " + view); } } } @@ -389,6 +470,15 @@ public abstract class Transition implements Cloneable { return false; } + private static ArrayMap getRunningAnimators() { + ArrayMap runningAnimators = sRunningAnimators.get(); + if (runningAnimators == null) { + runningAnimators = new ArrayMap(); + sRunningAnimators.set(runningAnimators); + } + return runningAnimators; + } + /** * This is called internally once all animations have been set up by the * transition hierarchy. \ @@ -396,28 +486,27 @@ public abstract class Transition implements Cloneable { * @hide */ protected void runAnimations() { - if (DBG && mPlayStartValuesList.size() > 0) { - Log.d(LOG_TAG, "runAnimations (" + mPlayStartValuesList.size() + ") on " + this); + if (DBG) { + Log.d(LOG_TAG, "runAnimations() on " + this); } - startTransition(); - // Now walk the list of TransitionValues, calling play for each pair - for (int i = 0; i < mPlayStartValuesList.size(); ++i) { - TransitionValues start = mPlayStartValuesList.get(i); - TransitionValues end = mPlayEndValuesList.get(i); - Animator anim = mAnimatorMap.get(new Pair(start, end)); + start(); + ArrayMap runningAnimators = getRunningAnimators(); + // Now start every Animator that was previously created for this transition in play() + for (Animator anim : mAnimators) { if (DBG) { Log.d(LOG_TAG, " anim: " + anim); } - startTransition(); - runAnimator(anim); + if (runningAnimators.containsKey(anim)) { + start(); + runAnimator(anim, runningAnimators); + } } - mPlayStartValuesList.clear(); - mPlayEndValuesList.clear(); - mAnimatorMap.clear(); - endTransition(); + mAnimators.clear(); + end(); } - private void runAnimator(Animator animator) { + private void runAnimator(Animator animator, + final ArrayMap runningAnimators) { if (animator != null) { // TODO: could be a single listener instance for all of them since it uses the param animator.addListener(new AnimatorListenerAdapter() { @@ -427,6 +516,7 @@ public abstract class Transition implements Cloneable { } @Override public void onAnimationEnd(Animator animation) { + runningAnimators.remove(animation); mCurrentAnimators.remove(animation); } }); @@ -690,12 +780,113 @@ public abstract class Transition implements Cloneable { return values; } + /** + * Pauses this transition, sending out calls to {@link + * TransitionListener#onTransitionPause(Transition)} to all listeners + * and pausing all running animators started by this transition. + * + * @hide + */ + public void pause() { + ArrayMap runningAnimators = getRunningAnimators(); + int numOldAnims = runningAnimators.size(); + for (int i = numOldAnims - 1; i >= 0; i--) { + Animator anim = runningAnimators.keyAt(i); + anim.pause(); + } + if (mListeners != null && mListeners.size() > 0) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onTransitionPause(this); + } + } + mPaused = true; + } + + /** + * Resumes this transition, sending out calls to {@link + * TransitionListener#onTransitionPause(Transition)} to all listeners + * and pausing all running animators started by this transition. + * + * @hide + */ + public void resume() { + if (mPaused) { + ArrayMap runningAnimators = getRunningAnimators(); + int numOldAnims = runningAnimators.size(); + for (int i = numOldAnims - 1; i >= 0; i--) { + Animator anim = runningAnimators.keyAt(i); + anim.resume(); + } + if (mListeners != null && mListeners.size() > 0) { + ArrayList tmpListeners = + (ArrayList) mListeners.clone(); + int numListeners = tmpListeners.size(); + for (int i = 0; i < numListeners; ++i) { + tmpListeners.get(i).onTransitionResume(this); + } + } + mPaused = false; + } + } + /** * Called by TransitionManager to play the transition. This calls * play() to set things up and create all of the animations and then * runAnimations() to actually start the animations. */ void playTransition(ViewGroup sceneRoot) { + ArrayMap runningAnimators = getRunningAnimators(); + int numOldAnims = runningAnimators.size(); + for (int i = numOldAnims - 1; i >= 0; i--) { + Animator anim = runningAnimators.keyAt(i); + if (anim != null) { + anim.resume(); + AnimationInfo oldInfo = runningAnimators.get(anim); + if (oldInfo != null) { + boolean cancel = false; + TransitionValues oldValues = oldInfo.values; + View oldView = oldInfo.view; + TransitionValues newValues = mEndValues.viewValues != null ? + mEndValues.viewValues.get(oldView) : null; + if (oldValues == null || newValues == null) { + if (oldValues != null || newValues != null) { + cancel = true; + } + } else { + for (String key : oldValues.values.keySet()) { + Object oldValue = oldValues.values.get(key); + Object newValue = newValues.values.get(key); + if ((oldValue == null && newValue != null) || + (oldValue != null && !oldValue.equals(newValue))) { + cancel = true; + if (DBG) { + Log.d(LOG_TAG, "Transition.play: oldValue != newValue for " + + key + ": old, new = " + oldValue + ", " + newValue); + } + break; + } + } + } + if (cancel) { + if (anim.isRunning() || anim.isStarted()) { + if (DBG) { + Log.d(LOG_TAG, "Canceling anim " + anim); + } + anim.cancel(); + } else { + if (DBG) { + Log.d(LOG_TAG, "removing anim from info list: " + anim); + } + runningAnimators.remove(anim); + } + } + } + } + } + // setup() must be called on entire transition hierarchy and set of views // before calling play() on anything; every transition needs a chance to set up // target views appropriately before transitions begin running @@ -707,7 +898,7 @@ public abstract class Transition implements Cloneable { * This is a utility method used by subclasses to handle standard parts of * setting up and running an Animator: it sets the {@link #getDuration() * duration} and the {@link #getStartDelay() startDelay}, starts the - * animation, and, when the animator ends, calls {@link #endTransition()}. + * animation, and, when the animator ends, calls {@link #end()}. * * @param animator The Animator to be run during this transition. * @@ -716,7 +907,7 @@ public abstract class Transition implements Cloneable { protected void animate(Animator animator) { // TODO: maybe pass auto-end as a boolean parameter? if (animator == null) { - endTransition(); + end(); } else { if (getDuration() >= 0) { animator.setDuration(getDuration()); @@ -730,7 +921,7 @@ public abstract class Transition implements Cloneable { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - endTransition(); + end(); animation.removeListener(this); } }); @@ -738,30 +929,6 @@ public abstract class Transition implements Cloneable { } } - /** - * Subclasses may override to receive notice of when the transition starts. - * This is equivalent to listening for the - * {@link TransitionListener#onTransitionStart(Transition)} callback. - */ - protected void onTransitionStart() { - } - - /** - * Subclasses may override to receive notice of when the transition is - * canceled. This is equivalent to listening for the - * {@link TransitionListener#onTransitionCancel(Transition)} callback. - */ - protected void onTransitionCancel() { - } - - /** - * Subclasses may override to receive notice of when the transition ends. - * This is equivalent to listening for the - * {@link TransitionListener#onTransitionEnd(Transition)} callback. - */ - protected void onTransitionEnd() { - } - /** * This method is called automatically by the transition and * TransitionGroup classes prior to a Transition subclass starting; @@ -769,9 +936,8 @@ public abstract class Transition implements Cloneable { * * @hide */ - protected void startTransition() { + protected void start() { if (mNumInstances == 0) { - onTransitionStart(); if (mListeners != null && mListeners.size() > 0) { ArrayList tmpListeners = (ArrayList) mListeners.clone(); @@ -790,15 +956,14 @@ public abstract class Transition implements Cloneable { * a transition did nothing (returned a null Animator from * {@link Transition#play(ViewGroup, TransitionValues, * TransitionValues)}) or because the transition returned a valid - * Animator and endTransition() was called in the onAnimationEnd() + * Animator and end() was called in the onAnimationEnd() * callback of the AnimatorListener. * * @hide */ - protected void endTransition() { + protected void end() { --mNumInstances; if (mNumInstances == 0) { - onTransitionEnd(); if (mListeners != null && mListeners.size() > 0) { ArrayList tmpListeners = (ArrayList) mListeners.clone(); @@ -828,7 +993,7 @@ public abstract class Transition implements Cloneable { * This method cancels a transition that is currently running. * Implementation TBD. */ - protected void cancelTransition() { + protected void cancel() { // TODO: how does this work with instances? // TODO: this doesn't actually do *anything* yet int numAnimators = mCurrentAnimators.size(); @@ -836,7 +1001,6 @@ public abstract class Transition implements Cloneable { Animator animator = mCurrentAnimators.get(i); animator.cancel(); } - onTransitionCancel(); if (mListeners != null && mListeners.size() > 0) { ArrayList tmpListeners = (ArrayList) mListeners.clone(); @@ -901,11 +1065,28 @@ public abstract class Transition implements Cloneable { Transition clone = null; try { clone = (Transition) super.clone(); + clone.mAnimators = new ArrayList(); } catch (CloneNotSupportedException e) {} return clone; } + /** + * Returns the name of this Transition. This name is used internally to distinguish + * between different transitions to determine when interrupting transitions overlap. + * For example, a Move running on the same target view as another Move should determine + * whether the old transition is animating to different end values and should be + * canceled in favor of the new transition. + * + *

By default, a Transition's name is simply the value of {@link Class#getName()}, + * but subclasses are free to override and return something different.

+ * + * @return The name of this transition. + */ + public String getName() { + return mName; + } + String toString(String indent) { String result = indent + getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + ": "; @@ -943,8 +1124,7 @@ public abstract class Transition implements Cloneable { /** * A transition listener receives notifications from a transition. - * Notifications indicate transition lifecycle events: when the transition - * begins, ends, or is canceled. + * Notifications indicate transition lifecycle events. */ public static interface TransitionListener { /** @@ -957,7 +1137,7 @@ public abstract class Transition implements Cloneable { /** * Notification about the end of the transition. Canceled transitions * will always notify listeners of both the cancellation and end - * events. That is, {@link #onTransitionEnd()} is always called, + * events. That is, {@link #onTransitionEnd(Transition)} is always called, * regardless of whether the transition was canceled or played * through to completion. * @@ -967,10 +1147,38 @@ public abstract class Transition implements Cloneable { /** * Notification about the cancellation of the transition. + * Note that cancel() may be called by a parent {@link TransitionGroup} on + * a child transition which has not yet started. This allows the child + * transition to restore state on target objects which was set at + * {@link #play(android.view.ViewGroup, TransitionValues, TransitionValues) + * play()} time. * * @param transition The transition which was canceled. */ void onTransitionCancel(Transition transition); + + /** + * Notification when a transition is paused. + * Note that play() may be called by a parent {@link TransitionGroup} on + * a child transition which has not yet started. This allows the child + * transition to restore state on target objects which was set at + * {@link #play(android.view.ViewGroup, TransitionValues, TransitionValues) + * play()} time. + * + * @param transition The transition which was paused. + */ + void onTransitionPause(Transition transition); + + /** + * Notification when a transition is resumed. + * Note that resume() may be called by a parent {@link TransitionGroup} on + * a child transition which has not yet started. This allows the child + * transition to restore state which may have changed in an earlier call + * to {@link #onTransitionPause(Transition)}. + * + * @param transition The transition which was resumed. + */ + void onTransitionResume(Transition transition); } /** @@ -991,6 +1199,32 @@ public abstract class Transition implements Cloneable { @Override public void onTransitionCancel(Transition transition) { } + + @Override + public void onTransitionPause(Transition transition) { + } + + @Override + public void onTransitionResume(Transition transition) { + } } + /** + * Holds information about each animator used when a new transition starts + * while other transitions are still running to determine whether a running + * 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. + */ + private static class AnimationInfo { + View view; + String name; + TransitionValues values; + + AnimationInfo(View view, String name, TransitionValues values) { + this.view = view; + this.name = name; + this.values = values; + } + } } diff --git a/core/java/android/view/transition/TransitionGroup.java b/core/java/android/view/transition/TransitionGroup.java index 313e33e601bdf..b3bacde4987ac 100644 --- a/core/java/android/view/transition/TransitionGroup.java +++ b/core/java/android/view/transition/TransitionGroup.java @@ -164,7 +164,7 @@ public class TransitionGroup extends Transition { @Override public void onTransitionStart(Transition transition) { if (!mTransitionGroup.mStarted) { - mTransitionGroup.startTransition(); + mTransitionGroup.start(); mTransitionGroup.mStarted = true; } } @@ -175,7 +175,7 @@ public class TransitionGroup extends Transition { if (mTransitionGroup.mCurrentListeners == 0) { // All child trans mTransitionGroup.mStarted = false; - mTransitionGroup.endTransition(); + mTransitionGroup.end(); } transition.removeListener(this); } @@ -233,12 +233,32 @@ public class TransitionGroup extends Transition { } } + /** @hide */ @Override - protected void cancelTransition() { - super.cancelTransition(); + public void pause() { + super.pause(); int numTransitions = mTransitions.size(); for (int i = 0; i < numTransitions; ++i) { - mTransitions.get(i).cancelTransition(); + mTransitions.get(i).pause(); + } + } + + /** @hide */ + @Override + public void resume() { + super.resume(); + int numTransitions = mTransitions.size(); + for (int i = 0; i < numTransitions; ++i) { + mTransitions.get(i).resume(); + } + } + + @Override + protected void cancel() { + super.cancel(); + int numTransitions = mTransitions.size(); + for (int i = 0; i < numTransitions; ++i) { + mTransitions.get(i).cancel(); } } diff --git a/core/java/android/view/transition/TransitionManager.java b/core/java/android/view/transition/TransitionManager.java index 7836268b067b0..3cb6f68177831 100644 --- a/core/java/android/view/transition/TransitionManager.java +++ b/core/java/android/view/transition/TransitionManager.java @@ -18,6 +18,7 @@ package android.view.transition; import android.util.ArrayMap; import android.util.Log; +import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; @@ -45,8 +46,8 @@ public class TransitionManager { ArrayMap mSceneTransitions = new ArrayMap(); ArrayMap> mScenePairTransitions = new ArrayMap>(); - static ArrayMap sRunningTransitions = - new ArrayMap(); + private static ThreadLocal>> sRunningTransitions = + new ThreadLocal>>(); private static ArrayList sPendingTransitions = new ArrayList(); @@ -160,6 +161,16 @@ public class TransitionManager { sceneChangeRunTransition(sceneRoot, transitionClone); } + private static ArrayMap> getRunningTransitions() { + ArrayMap> runningTransitions = + sRunningTransitions.get(); + if (runningTransitions == null) { + runningTransitions = new ArrayMap>(); + sRunningTransitions.set(runningTransitions); + } + return runningTransitions; + } + private static void sceneChangeRunTransition(final ViewGroup sceneRoot, final Transition transition) { if (transition != null) { @@ -169,16 +180,31 @@ public class TransitionManager { sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); sPendingTransitions.remove(sceneRoot); // Add to running list, handle end to remove it - sRunningTransitions.put(sceneRoot, transition); + final ArrayMap> runningTransitions = + getRunningTransitions(); + ArrayList currentTransitions = runningTransitions.get(sceneRoot); + if (currentTransitions == null) { + currentTransitions = new ArrayList(); + runningTransitions.put(sceneRoot, currentTransitions); + } + currentTransitions.add(transition); transition.addListener(new Transition.TransitionListenerAdapter() { @Override public void onTransitionEnd(Transition transition) { - sRunningTransitions.remove(sceneRoot); + ArrayList currentTransitions = + runningTransitions.get(sceneRoot); + currentTransitions.remove(transition); } }); transition.captureValues(sceneRoot, false); transition.playTransition(sceneRoot); - return true; + + // Returning false from onPreDraw() skips the current frame. This is + // necessary to avoid artifacts caused by resetting target views + // to their proper end states for capturing. Waiting until the next + // frame to draw allows these views to have their mid-transition + // values set on them again and avoid artifacts. + return false; } }); } @@ -187,16 +213,18 @@ public class TransitionManager { private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) { // Capture current values - Transition runningTransition = sRunningTransitions.get(sceneRoot); + ArrayList runningTransitions = getRunningTransitions().get(sceneRoot); + + if (runningTransitions != null && runningTransitions.size() > 0) { + for (Transition runningTransition : runningTransitions) { + runningTransition.pause(); + } + } if (transition != null) { transition.captureValues(sceneRoot, true); } - if (runningTransition != null) { - runningTransition.cancelTransition(); - } - // Notify previous scene that it is being exited Scene previousScene = sceneRoot.getCurrentScene(); if (previousScene != null) { diff --git a/core/java/android/view/transition/Visibility.java b/core/java/android/view/transition/Visibility.java index 6d39ab650d28c..96ea0444f5353 100644 --- a/core/java/android/view/transition/Visibility.java +++ b/core/java/android/view/transition/Visibility.java @@ -19,6 +19,7 @@ package android.view.transition; import android.animation.Animator; import android.view.View; import android.view.ViewGroup; +import android.view.ViewOverlay; import android.view.ViewParent; /** @@ -38,6 +39,10 @@ public abstract class Visibility extends Transition { private static final String PROPNAME_VISIBILITY = "android:visibility:visibility"; private static final String PROPNAME_PARENT = "android:visibility:parent"; + private static String[] sTransitionProperties = { + PROPNAME_VISIBILITY, + PROPNAME_PARENT, + }; private static class VisibilityInfo { boolean visibilityChange; @@ -51,6 +56,11 @@ public abstract class Visibility extends Transition { // Temporary structure, used in calculating state in setup() and play() private VisibilityInfo mTmpVisibilityInfo = new VisibilityInfo(); + @Override + public String[] getTransitionProperties() { + return sTransitionProperties; + } + @Override protected void captureValues(TransitionValues values, boolean start) { int visibility = values.view.getVisibility(); @@ -58,6 +68,31 @@ public abstract class Visibility extends Transition { values.values.put(PROPNAME_PARENT, values.view.getParent()); } + /** + * Returns whether the view is 'visible' according to the given values + * object. This is determined by testing the same properties in the values + * object that are used to determine whether the object is appearing or + * disappearing in the {@link + * #play(android.view.ViewGroup, TransitionValues, TransitionValues)} + * method. This method can be called by, for example, subclasses that want + * to know whether the object is visible in the same way that Visibility + * determines it for the actual animation. + * + * @param values The TransitionValues object that holds the information by + * which visibility is determined. + * @return True if the view reference by values is visible, + * false otherwise. + */ + public boolean isVisible(TransitionValues values) { + if (values == null) { + return false; + } + int visibility = (Integer) values.values.get(PROPNAME_VISIBILITY); + View parent = (View) values.values.get(PROPNAME_PARENT); + + return visibility == View.VISIBLE && parent != null; + } + private boolean isHierarchyVisibilityChanging(ViewGroup sceneRoot, ViewGroup view) { if (view == sceneRoot) { @@ -197,5 +232,4 @@ public abstract class Visibility extends Transition { TransitionValues endValues, int endVisibility) { return null; } - }