diff options
| -rw-r--r-- | api/current.txt | 3 | ||||
| -rw-r--r-- | core/java/android/animation/LayoutTransition.java | 314 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 20 | ||||
| -rw-r--r-- | core/java/android/view/ViewAncestor.java | 30 | ||||
| -rw-r--r-- | core/java/android/view/ViewGroup.java | 14 |
5 files changed, 266 insertions, 115 deletions
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 @@ package android.animation { 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 @@ package android.view { 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 @@ package android.animation; 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 @@ import java.util.List; * 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 @@ public class LayoutTransition { */ 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 @@ public class LayoutTransition { 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 @@ public class LayoutTransition { // 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); + 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; } - // 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); } } + // 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 @@ public class LayoutTransition { } /** + * 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 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * 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 @@ public final class ViewAncestor extends Handler implements ViewParent, long mResizeBitmapStartTime; int mResizeBitmapDuration; static final Interpolator mResizeInterpolator = new AccelerateDecelerateInterpolator(); + private ArrayList<LayoutTransition> mPendingTransitions; final ViewConfiguration mViewConfiguration; @@ -700,6 +702,28 @@ public final class ViewAncestor extends Handler implements ViewParent, } } + /** + * 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 @@ public final class ViewAncestor extends Handler implements ViewParent, 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 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * 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 |
