Merge "Add ability to transition parent hierarchy in layout transitions"
diff --git a/api/current.txt b/api/current.txt
index 678e454..a51b649 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1978,6 +1978,7 @@
method public boolean isRunning();
method public void removeChild(android.view.ViewGroup, android.view.View);
method public void removeTransitionListener(android.animation.LayoutTransition.TransitionListener);
+ method public void setAnimateParentHierarchy(boolean);
method public void setAnimator(int, android.animation.Animator);
method public void setDuration(long);
method public void setDuration(int, long);
@@ -21441,6 +21442,8 @@
method public void setScaleY(float);
method public void setScrollBarStyle(int);
method public void setScrollContainer(boolean);
+ method public void setScrollX(int);
+ method public void setScrollY(int);
method public void setScrollbarFadingEnabled(boolean);
method public void setSelected(boolean);
method public void setSoundEffectsEnabled(boolean);
diff --git a/core/java/android/animation/LayoutTransition.java b/core/java/android/animation/LayoutTransition.java
index adfda8e..d25de97 100644
--- a/core/java/android/animation/LayoutTransition.java
+++ b/core/java/android/animation/LayoutTransition.java
@@ -18,6 +18,7 @@
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
@@ -70,8 +71,9 @@
* moving as a result of the layout event) as well as the values that are changing (such as the
* position and size of that object). The actual values that are pushed to each animation
* depends on what properties are specified for the animation. For example, the default
- * CHANGE_APPEARING animation animates <code>left</code>, <code>top</code>, <code>right</code>,
- * and <code>bottom</code>. Values for these properties are updated with the pre- and post-layout
+ * CHANGE_APPEARING animation animates the <code>left</code>, <code>top</code>, <code>right</code>,
+ * <code>bottom</code>, <code>scrollX</code>, and <code>scrollY</code> properties.
+ * Values for these properties are updated with the pre- and post-layout
* values when the transition begins. Custom animations will be similarly populated with
* the target and values being animated, assuming they use ObjectAnimator objects with
* property names that are known on the target object.</p>
@@ -210,6 +212,14 @@
*/
private ArrayList<TransitionListener> mListeners;
+ /**
+ * Controls whether changing animations automatically animate the parent hierarchy as well.
+ * This behavior prevents artifacts when wrap_content layouts snap to the end state as the
+ * transition begins, causing visual glitches and clipping.
+ * Default value is true.
+ */
+ private boolean mAnimateParentHierarchy = true;
+
/**
* Constructs a LayoutTransition object. By default, the object will listen to layout
@@ -223,14 +233,17 @@
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top", 0, 1);
PropertyValuesHolder pvhRight = PropertyValuesHolder.ofInt("right", 0, 1);
PropertyValuesHolder pvhBottom = PropertyValuesHolder.ofInt("bottom", 0, 1);
+ PropertyValuesHolder pvhScrollX = PropertyValuesHolder.ofInt("scrollX", 0, 1);
+ PropertyValuesHolder pvhScrollY = PropertyValuesHolder.ofInt("scrollY", 0, 1);
defaultChangeIn = ObjectAnimator.ofPropertyValuesHolder(this,
- pvhLeft, pvhTop, pvhRight, pvhBottom);
+ pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScrollX, pvhScrollY);
defaultChangeIn.setDuration(DEFAULT_DURATION);
defaultChangeIn.setStartDelay(mChangingAppearingDelay);
defaultChangeIn.setInterpolator(mChangingAppearingInterpolator);
defaultChangeOut = defaultChangeIn.clone();
defaultChangeOut.setStartDelay(mChangingDisappearingDelay);
defaultChangeOut.setInterpolator(mChangingDisappearingInterpolator);
+
defaultFadeIn = ObjectAnimator.ofFloat(this, "alpha", 0f, 1f);
defaultFadeIn.setDuration(DEFAULT_DURATION);
defaultFadeIn.setStartDelay(mAppearingDelay);
@@ -572,122 +585,24 @@
// only animate the views not being added or removed
if (child != newView) {
-
-
- // Make a copy of the appropriate animation
- final Animator anim = baseAnimator.clone();
-
- // Set the target object for the animation
- anim.setTarget(child);
-
- // A ObjectAnimator (or AnimatorSet of them) can extract start values from
- // its target object
- anim.setupStartValues();
-
- // If there's an animation running on this view already, cancel it
- Animator currentAnimation = pendingAnimations.get(child);
- if (currentAnimation != null) {
- currentAnimation.cancel();
- pendingAnimations.remove(child);
- }
- // Cache the animation in case we need to cancel it later
- pendingAnimations.put(child, anim);
-
- // For the animations which don't get started, we have to have a means of
- // removing them from the cache, lest we leak them and their target objects.
- // We run an animator for the default duration+100 (an arbitrary time, but one
- // which should far surpass the delay between setting them up here and
- // handling layout events which start them.
- ValueAnimator pendingAnimRemover = ValueAnimator.ofFloat(0f, 1f).
- setDuration(duration+100);
- pendingAnimRemover.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- pendingAnimations.remove(child);
- }
- });
- pendingAnimRemover.start();
-
- // Add a listener to track layout changes on this view. If we don't get a callback,
- // then there's nothing to animate.
- final View.OnLayoutChangeListener listener = new View.OnLayoutChangeListener() {
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft, int oldTop, int oldRight, int oldBottom) {
-
- // Tell the animation to extract end values from the changed object
- anim.setupEndValues();
-
- long startDelay;
- if (changeReason == APPEARING) {
- startDelay = mChangingAppearingDelay + staggerDelay;
- staggerDelay += mChangingAppearingStagger;
- } else {
- startDelay = mChangingDisappearingDelay + staggerDelay;
- staggerDelay += mChangingDisappearingStagger;
- }
- anim.setStartDelay(startDelay);
- anim.setDuration(duration);
-
- Animator prevAnimation = currentChangingAnimations.get(child);
- if (prevAnimation != null) {
- prevAnimation.cancel();
- }
- Animator pendingAnimation = pendingAnimations.get(child);
- if (pendingAnimation != null) {
- pendingAnimations.remove(child);
- }
- // Cache the animation in case we need to cancel it later
- currentChangingAnimations.put(child, anim);
-
- if (anim instanceof ObjectAnimator) {
- ((ObjectAnimator) anim).setCurrentPlayTime(0);
- }
- anim.start();
-
- // this only removes listeners whose views changed - must clear the
- // other listeners later
- child.removeOnLayoutChangeListener(this);
- layoutChangeListenerMap.remove(child);
- }
- };
- // Remove the animation from the cache when it ends
- anim.addListener(new AnimatorListenerAdapter() {
-
- @Override
- public void onAnimationStart(Animator animator) {
- if (mListeners != null) {
- for (TransitionListener listener : mListeners) {
- listener.startTransition(LayoutTransition.this, parent, child,
- changeReason == APPEARING ?
- CHANGE_APPEARING : CHANGE_DISAPPEARING);
- }
- }
- }
-
- @Override
- public void onAnimationCancel(Animator animator) {
- child.removeOnLayoutChangeListener(listener);
- layoutChangeListenerMap.remove(child);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- currentChangingAnimations.remove(child);
- if (mListeners != null) {
- for (TransitionListener listener : mListeners) {
- listener.endTransition(LayoutTransition.this, parent, child,
- changeReason == APPEARING ?
- CHANGE_APPEARING : CHANGE_DISAPPEARING);
- }
- }
- }
- });
-
- child.addOnLayoutChangeListener(listener);
- // cache the listener for later removal
- layoutChangeListenerMap.put(child, listener);
+ setupChangeAnimation(parent, changeReason, baseAnimator, duration, child);
}
}
+ if (mAnimateParentHierarchy) {
+ ViewGroup tempParent = parent;
+ while (tempParent != null) {
+ ViewParent parentParent = tempParent.getParent();
+ if (parentParent instanceof ViewGroup) {
+ setupChangeAnimation((ViewGroup)parentParent, changeReason, baseAnimator,
+ duration, tempParent);
+ tempParent = (ViewGroup) parentParent;
+ } else {
+ tempParent = null;
+ }
+
+ }
+ }
+
// This is the cleanup step. When we get this rendering event, we know that all of
// the appropriate animations have been set up and run. Now we can clear out the
// layout listeners.
@@ -706,6 +621,175 @@
}
/**
+ * This flag controls whether CHANGE_APPEARING or CHANGE_DISAPPEARING animations will
+ * cause the same changing animation to be run on the parent hierarchy as well. This allows
+ * containers of transitioning views to also transition, which may be necessary in situations
+ * where the containers bounds change between the before/after states and may clip their
+ * children during the transition animations. For example, layouts with wrap_content will
+ * adjust their bounds according to the dimensions of their children.
+ *
+ * @param animateParentHierarchy A boolean value indicating whether the parents of
+ * transitioning views should also be animated during the transition. Default value is true.
+ */
+ public void setAnimateParentHierarchy(boolean animateParentHierarchy) {
+ mAnimateParentHierarchy = animateParentHierarchy;
+ }
+
+ /**
+ * Utility function called by runChangingTransition for both the children and the parent
+ * hierarchy.
+ */
+ private void setupChangeAnimation(final ViewGroup parent, final int changeReason,
+ Animator baseAnimator, final long duration, final View child) {
+ // Make a copy of the appropriate animation
+ final Animator anim = baseAnimator.clone();
+
+ // Set the target object for the animation
+ anim.setTarget(child);
+
+ // A ObjectAnimator (or AnimatorSet of them) can extract start values from
+ // its target object
+ anim.setupStartValues();
+
+ // If there's an animation running on this view already, cancel it
+ Animator currentAnimation = pendingAnimations.get(child);
+ if (currentAnimation != null) {
+ currentAnimation.cancel();
+ pendingAnimations.remove(child);
+ }
+ // Cache the animation in case we need to cancel it later
+ pendingAnimations.put(child, anim);
+
+ // For the animations which don't get started, we have to have a means of
+ // removing them from the cache, lest we leak them and their target objects.
+ // We run an animator for the default duration+100 (an arbitrary time, but one
+ // which should far surpass the delay between setting them up here and
+ // handling layout events which start them.
+ ValueAnimator pendingAnimRemover = ValueAnimator.ofFloat(0f, 1f).
+ setDuration(duration + 100);
+ pendingAnimRemover.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ pendingAnimations.remove(child);
+ }
+ });
+ pendingAnimRemover.start();
+
+ // Add a listener to track layout changes on this view. If we don't get a callback,
+ // then there's nothing to animate.
+ final View.OnLayoutChangeListener listener = new View.OnLayoutChangeListener() {
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+
+ // Tell the animation to extract end values from the changed object
+ anim.setupEndValues();
+ if (anim instanceof ValueAnimator) {
+ boolean valuesDiffer = false;
+ ValueAnimator valueAnim = (ValueAnimator)anim;
+ PropertyValuesHolder[] oldValues = valueAnim.getValues();
+ for (int i = 0; i < oldValues.length; ++i) {
+ PropertyValuesHolder pvh = oldValues[i];
+ KeyframeSet keyframeSet = pvh.mKeyframeSet;
+ if (keyframeSet.mFirstKeyframe == null ||
+ keyframeSet.mLastKeyframe == null ||
+ !keyframeSet.mFirstKeyframe.getValue().equals(
+ keyframeSet.mLastKeyframe.getValue())) {
+ valuesDiffer = true;
+ }
+ }
+ if (!valuesDiffer) {
+ return;
+ }
+ }
+
+ long startDelay;
+ if (changeReason == APPEARING) {
+ startDelay = mChangingAppearingDelay + staggerDelay;
+ staggerDelay += mChangingAppearingStagger;
+ } else {
+ startDelay = mChangingDisappearingDelay + staggerDelay;
+ staggerDelay += mChangingDisappearingStagger;
+ }
+ anim.setStartDelay(startDelay);
+ anim.setDuration(duration);
+
+ Animator prevAnimation = currentChangingAnimations.get(child);
+ if (prevAnimation != null) {
+ prevAnimation.cancel();
+ }
+ Animator pendingAnimation = pendingAnimations.get(child);
+ if (pendingAnimation != null) {
+ pendingAnimations.remove(child);
+ }
+ // Cache the animation in case we need to cancel it later
+ currentChangingAnimations.put(child, anim);
+
+ parent.requestTransitionStart(LayoutTransition.this);
+
+ // this only removes listeners whose views changed - must clear the
+ // other listeners later
+ child.removeOnLayoutChangeListener(this);
+ layoutChangeListenerMap.remove(child);
+ }
+ };
+ // Remove the animation from the cache when it ends
+ anim.addListener(new AnimatorListenerAdapter() {
+
+ @Override
+ public void onAnimationStart(Animator animator) {
+ if (mListeners != null) {
+ for (TransitionListener listener : mListeners) {
+ listener.startTransition(LayoutTransition.this, parent, child,
+ changeReason == APPEARING ?
+ CHANGE_APPEARING : CHANGE_DISAPPEARING);
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ child.removeOnLayoutChangeListener(listener);
+ layoutChangeListenerMap.remove(child);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ currentChangingAnimations.remove(child);
+ if (mListeners != null) {
+ for (TransitionListener listener : mListeners) {
+ listener.endTransition(LayoutTransition.this, parent, child,
+ changeReason == APPEARING ?
+ CHANGE_APPEARING : CHANGE_DISAPPEARING);
+ }
+ }
+ }
+ });
+
+ child.addOnLayoutChangeListener(listener);
+ // cache the listener for later removal
+ layoutChangeListenerMap.put(child, listener);
+ }
+
+ /**
+ * Starts the animations set up for a CHANGING transition. We separate the setup of these
+ * animations from actually starting them, to avoid side-effects that starting the animations
+ * may have on the properties of the affected objects. After setup, we tell the affected parent
+ * that this transition should be started. The parent informs its ViewAncestor, which then
+ * starts the transition after the current layout/measurement phase, just prior to drawing
+ * the view hierarchy.
+ *
+ * @hide
+ */
+ public void startChangingAnimations() {
+ for (Animator anim : currentChangingAnimations.values()) {
+ if (anim instanceof ObjectAnimator) {
+ ((ObjectAnimator) anim).setCurrentPlayTime(0);
+ }
+ anim.start();
+ }
+ }
+
+ /**
* Returns true if animations are running which animate layout-related properties. This
* essentially means that either CHANGE_APPEARING or CHANGE_DISAPPEARING animations
* are running, since these animations operate on layout-related properties.
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index d5f573c..98d07c4 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -6048,6 +6048,26 @@
}
/**
+ * Set the horizontal scrolled position of your view. This will cause a call to
+ * {@link #onScrollChanged(int, int, int, int)} and the view will be
+ * invalidated.
+ * @param value the x position to scroll to
+ */
+ public void setScrollX(int value) {
+ scrollTo(value, mScrollY);
+ }
+
+ /**
+ * Set the vertical scrolled position of your view. This will cause a call to
+ * {@link #onScrollChanged(int, int, int, int)} and the view will be
+ * invalidated.
+ * @param value the y position to scroll to
+ */
+ public void setScrollY(int value) {
+ scrollTo(mScrollX, value);
+ }
+
+ /**
* Return the scrolled left position of this view. This is the left edge of
* the displayed part of your view. You do not need to draw any pixels
* farther left, since those are outside of the frame of your view on
diff --git a/core/java/android/view/ViewAncestor.java b/core/java/android/view/ViewAncestor.java
index cf9a1c8..ccba894 100644
--- a/core/java/android/view/ViewAncestor.java
+++ b/core/java/android/view/ViewAncestor.java
@@ -17,6 +17,7 @@
package android.view;
import android.Manifest;
+import android.animation.LayoutTransition;
import android.app.ActivityManagerNative;
import android.content.ClipDescription;
import android.content.ComponentCallbacks;
@@ -232,6 +233,7 @@
long mResizeBitmapStartTime;
int mResizeBitmapDuration;
static final Interpolator mResizeInterpolator = new AccelerateDecelerateInterpolator();
+ private ArrayList<LayoutTransition> mPendingTransitions;
final ViewConfiguration mViewConfiguration;
@@ -700,6 +702,28 @@
}
}
+ /**
+ * Add LayoutTransition to the list of transitions to be started in the next traversal.
+ * This list will be cleared after the transitions on the list are start()'ed. These
+ * transitionsa re added by LayoutTransition itself when it sets up animations. The setup
+ * happens during the layout phase of traversal, which we want to complete before any of the
+ * animations are started (because those animations may side-effect properties that layout
+ * depends upon, like the bounding rectangles of the affected views). So we add the transition
+ * to the list and it is started just prior to starting the drawing phase of traversal.
+ *
+ * @param transition The LayoutTransition to be started on the next traversal.
+ *
+ * @hide
+ */
+ public void requestTransitionStart(LayoutTransition transition) {
+ if (mPendingTransitions == null || !mPendingTransitions.contains(transition)) {
+ if (mPendingTransitions == null) {
+ mPendingTransitions = new ArrayList<LayoutTransition>();
+ }
+ mPendingTransitions.add(transition);
+ }
+ }
+
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
@@ -1395,6 +1419,12 @@
boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw();
if (!cancelDraw && !newSurface) {
+ if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
+ for (int i = 0; i < mPendingTransitions.size(); ++i) {
+ mPendingTransitions.get(i).startChangingAnimations();
+ }
+ mPendingTransitions.clear();
+ }
mFullRedrawNeeded = false;
final long drawStartTime;
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 6937573..f504b90 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -4838,6 +4838,20 @@
}
/**
+ * This method is called by LayoutTransition when there are 'changing' animations that need
+ * to start after the layout/setup phase. The request is forwarded to the ViewAncestor, who
+ * starts all pending transitions prior to the drawing phase in the current traversal.
+ *
+ * @param transition The LayoutTransition to be started on the next traversal.
+ *
+ * @hide
+ */
+ public void requestTransitionStart(LayoutTransition transition) {
+ ViewAncestor viewAncestor = getViewAncestor();
+ viewAncestor.requestTransitionStart(transition);
+ }
+
+ /**
* Return true if the pressed state should be delayed for children or descendants of this
* ViewGroup. Generally, this should be done for containers that can scroll, such as a List.
* This prevents the pressed state from appearing when the user is actually trying to scroll