/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.animation; import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; 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; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * This class enables automatic animations on layout changes in ViewGroup objects. To enable * transitions for a layout container, create a LayoutTransition object and set it on any * ViewGroup by calling {@link ViewGroup#setLayoutTransition(LayoutTransition)}. This will cause * default animations to run whenever items are added to or removed from that container. To specify * custom animations, use the {@link LayoutTransition#setAnimator(int, Animator) * setAnimator()} method. * *

One of the core concepts of these transition animations is that there are two types of * changes that cause the transition and four different animations that run because of * those changes. The changes that trigger the transition are items being added to a container * (referred to as an "appearing" transition) or removed from a container (also known as * "disappearing"). Setting the visibility of views (between GONE and VISIBLE) will trigger * the same add/remove logic. The animations that run due to those events are one that animates * items being added, one that animates items being removed, and two that animate the other * items in the container that change due to the add/remove occurrence. Users of * the transition may want different animations for the changing items depending on whether * they are changing due to an appearing or disappearing event, so there is one animation for * each of these variations of the changing event. Most of the API of this class is concerned * with setting up the basic properties of the animations used in these four situations, * or with setting up custom animations for any or all of the four.

* *

By default, the DISAPPEARING animation begins immediately, as does the CHANGE_APPEARING * animation. The other animations begin after a delay that is set to the default duration * of the animations. This behavior facilitates a sequence of animations in transitions as * follows: when an item is being added to a layout, the other children of that container will * move first (thus creating space for the new item), then the appearing animation will run to * animate the item being added. Conversely, when an item is removed from a container, the * animation to remove it will run first, then the animations of the other children in the * layout will run (closing the gap created in the layout when the item was removed). If this * default choreography behavior is not desired, the {@link #setDuration(int, long)} and * {@link #setStartDelay(int, long)} of any or all of the animations can be changed as * appropriate. Keep in mind, however, that if you start an APPEARING animation before a * DISAPPEARING animation is completed, the DISAPPEARING animation stops, and any effects from * the DISAPPEARING animation are reverted. If you instead start a DISAPPEARING animation * before an APPEARING animation is completed, a similar set of effects occurs for the * APPEARING animation.

* *

The animations specified for the transition, both the defaults and any custom animations * set on the transition object, are templates only. That is, these animations exist to hold the * basic animation properties, such as the duration, start delay, and properties being animated. * But the actual target object, as well as the start and end values for those properties, are * set automatically in the process of setting up the transition each time it runs. Each of the * animations is cloned from the original copy and the clone is then populated with the dynamic * values of the target being animated (such as one of the items in a layout container that is * 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 the left, top, right, * bottom, scrollX, and scrollY 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.

* *

This class, and the associated XML flag for containers, animateLayoutChanges="true", * provides a simple utility meant for automating changes in straightforward situations. * Using LayoutTransition at multiple levels of a nested view hierarchy may not work due to the * interrelationship of the various levels of layout. Also, a container that is being scrolled * at the same time as items are being added or removed is probably not a good candidate for * this utility, because the before/after locations calculated by LayoutTransition * may not match the actual locations when the animations finish due to the container * being scrolled as the animations are running. You can work around that * particular issue by disabling the 'changing' animations by setting the CHANGE_APPEARING * and CHANGE_DISAPPEARING animations to null, and setting the startDelay of the * other animations appropriately.

*/ public class LayoutTransition { /** * A flag indicating the animation that runs on those items that are changing * due to a new item appearing in the container. */ public static final int CHANGE_APPEARING = 0; /** * A flag indicating the animation that runs on those items that are changing * due to an item disappearing from the container. */ public static final int CHANGE_DISAPPEARING = 1; /** * A flag indicating the animation that runs on those items that are appearing * in the container. */ public static final int APPEARING = 2; /** * A flag indicating the animation that runs on those items that are disappearing * from the container. */ public static final int DISAPPEARING = 3; /** * A flag indicating the animation that runs on those items that are changing * due to a layout change not caused by items being added to or removed * from the container. This transition type is not enabled by default; it can be * enabled via {@link #enableTransitionType(int)}. */ public static final int CHANGING = 4; /** * Private bit fields used to set the collection of enabled transition types for * mTransitionTypes. */ private static final int FLAG_APPEARING = 0x01; private static final int FLAG_DISAPPEARING = 0x02; private static final int FLAG_CHANGE_APPEARING = 0x04; private static final int FLAG_CHANGE_DISAPPEARING = 0x08; private static final int FLAG_CHANGING = 0x10; /** * These variables hold the animations that are currently used to run the transition effects. * These animations are set to defaults, but can be changed to custom animations by * calls to setAnimator(). */ private Animator mDisappearingAnim = null; private Animator mAppearingAnim = null; private Animator mChangingAppearingAnim = null; private Animator mChangingDisappearingAnim = null; private Animator mChangingAnim = null; /** * These are the default animations, defined in the constructor, that will be used * unless the user specifies custom animations. */ private static ObjectAnimator defaultChange; private static ObjectAnimator defaultChangeIn; private static ObjectAnimator defaultChangeOut; private static ObjectAnimator defaultFadeIn; private static ObjectAnimator defaultFadeOut; /** * The default duration used by all animations. */ private static long DEFAULT_DURATION = 300; /** * The durations of the different animations */ private long mChangingAppearingDuration = DEFAULT_DURATION; private long mChangingDisappearingDuration = DEFAULT_DURATION; private long mChangingDuration = DEFAULT_DURATION; private long mAppearingDuration = DEFAULT_DURATION; private long mDisappearingDuration = DEFAULT_DURATION; /** * The start delays of the different animations. Note that the default behavior of * the appearing item is the default duration, since it should wait for the items to move * before fading it. Same for the changing animation when disappearing; it waits for the item * to fade out before moving the other items. */ private long mAppearingDelay = DEFAULT_DURATION; private long mDisappearingDelay = 0; private long mChangingAppearingDelay = 0; private long mChangingDisappearingDelay = DEFAULT_DURATION; private long mChangingDelay = 0; /** * The inter-animation delays used on the changing animations */ private long mChangingAppearingStagger = 0; private long mChangingDisappearingStagger = 0; private long mChangingStagger = 0; /** * Static interpolators - these are stateless and can be shared across the instances */ private static TimeInterpolator ACCEL_DECEL_INTERPOLATOR = new AccelerateDecelerateInterpolator(); private static TimeInterpolator DECEL_INTERPOLATOR = new DecelerateInterpolator(); private static TimeInterpolator sAppearingInterpolator = ACCEL_DECEL_INTERPOLATOR; private static TimeInterpolator sDisappearingInterpolator = ACCEL_DECEL_INTERPOLATOR; private static TimeInterpolator sChangingAppearingInterpolator = DECEL_INTERPOLATOR; private static TimeInterpolator sChangingDisappearingInterpolator = DECEL_INTERPOLATOR; private static TimeInterpolator sChangingInterpolator = DECEL_INTERPOLATOR; /** * The default interpolators used for the animations */ private TimeInterpolator mAppearingInterpolator = sAppearingInterpolator; private TimeInterpolator mDisappearingInterpolator = sDisappearingInterpolator; private TimeInterpolator mChangingAppearingInterpolator = sChangingAppearingInterpolator; private TimeInterpolator mChangingDisappearingInterpolator = sChangingDisappearingInterpolator; private TimeInterpolator mChangingInterpolator = sChangingInterpolator; /** * These hashmaps are used to store the animations that are currently running as part of * the transition. The reason for this is that a further layout event should cause * existing animations to stop where they are prior to starting new animations. So * we cache all of the current animations in this map for possible cancellation on * another layout event. LinkedHashMaps are used to preserve the order in which animations * are inserted, so that we process events (such as setting up start values) in the same order. */ private final HashMap pendingAnimations = new HashMap(); private final LinkedHashMap currentChangingAnimations = new LinkedHashMap(); private final LinkedHashMap currentAppearingAnimations = new LinkedHashMap(); private final LinkedHashMap currentDisappearingAnimations = new LinkedHashMap(); /** * This hashmap is used to track the listeners that have been added to the children of * a container. When a layout change occurs, an animation is created for each View, so that * the pre-layout values can be cached in that animation. Then a listener is added to the * view to see whether the layout changes the bounds of that view. If so, the animation * is set with the final values and then run. If not, the animation is not started. When * the process of setting up and running all appropriate animations is done, we need to * remove these listeners and clear out the map. */ private final HashMap layoutChangeListenerMap = new HashMap(); /** * Used to track the current delay being assigned to successive animations as they are * started. This value is incremented for each new animation, then zeroed before the next * transition begins. */ private long staggerDelay; /** * These are the types of transition animations that the LayoutTransition is reacting * to. By default, appearing/disappearing and the change animations related to them are * enabled (not CHANGING). */ private int mTransitionTypes = FLAG_CHANGE_APPEARING | FLAG_CHANGE_DISAPPEARING | FLAG_APPEARING | FLAG_DISAPPEARING; /** * The set of listeners that should be notified when APPEARING/DISAPPEARING transitions * start and end. */ private ArrayList 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 * events on any ViewGroup that it is set on and will run default animations for each * type of layout event. */ public LayoutTransition() { if (defaultChangeIn == null) { // "left" is just a placeholder; we'll put real properties/values in when needed PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt("left", 0, 1); 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((Object)null, 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); defaultChange = defaultChangeIn.clone(); defaultChange.setStartDelay(mChangingDelay); defaultChange.setInterpolator(mChangingInterpolator); defaultFadeIn = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f); defaultFadeIn.setDuration(DEFAULT_DURATION); defaultFadeIn.setStartDelay(mAppearingDelay); defaultFadeIn.setInterpolator(mAppearingInterpolator); defaultFadeOut = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f); defaultFadeOut.setDuration(DEFAULT_DURATION); defaultFadeOut.setStartDelay(mDisappearingDelay); defaultFadeOut.setInterpolator(mDisappearingInterpolator); } mChangingAppearingAnim = defaultChangeIn; mChangingDisappearingAnim = defaultChangeOut; mChangingAnim = defaultChange; mAppearingAnim = defaultFadeIn; mDisappearingAnim = defaultFadeOut; } /** * Sets the duration to be used by all animations of this transition object. If you want to * set the duration of just one of the animations in particular, use the * {@link #setDuration(int, long)} method. * * @param duration The length of time, in milliseconds, that the transition animations * should last. */ public void setDuration(long duration) { mChangingAppearingDuration = duration; mChangingDisappearingDuration = duration; mChangingDuration = duration; mAppearingDuration = duration; mDisappearingDuration = duration; } /** * Enables the specified transitionType for this LayoutTransition object. * By default, a LayoutTransition listens for changes in children being * added/remove/hidden/shown in the container, and runs the animations associated with * those events. That is, all transition types besides {@link #CHANGING} are enabled by default. * You can also enable {@link #CHANGING} animations by calling this method with the * {@link #CHANGING} transitionType. * * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}. */ public void enableTransitionType(int transitionType) { switch (transitionType) { case APPEARING: mTransitionTypes |= FLAG_APPEARING; break; case DISAPPEARING: mTransitionTypes |= FLAG_DISAPPEARING; break; case CHANGE_APPEARING: mTransitionTypes |= FLAG_CHANGE_APPEARING; break; case CHANGE_DISAPPEARING: mTransitionTypes |= FLAG_CHANGE_DISAPPEARING; break; case CHANGING: mTransitionTypes |= FLAG_CHANGING; break; } } /** * Disables the specified transitionType for this LayoutTransition object. * By default, all transition types except {@link #CHANGING} are enabled. * * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}. */ public void disableTransitionType(int transitionType) { switch (transitionType) { case APPEARING: mTransitionTypes &= ~FLAG_APPEARING; break; case DISAPPEARING: mTransitionTypes &= ~FLAG_DISAPPEARING; break; case CHANGE_APPEARING: mTransitionTypes &= ~FLAG_CHANGE_APPEARING; break; case CHANGE_DISAPPEARING: mTransitionTypes &= ~FLAG_CHANGE_DISAPPEARING; break; case CHANGING: mTransitionTypes &= ~FLAG_CHANGING; break; } } /** * Returns whether the specified transitionType is enabled for this LayoutTransition object. * By default, all transition types except {@link #CHANGING} are enabled. * * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}. * @return true if the specified transitionType is currently enabled, false otherwise. */ public boolean isTransitionTypeEnabled(int transitionType) { switch (transitionType) { case APPEARING: return (mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING; case DISAPPEARING: return (mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING; case CHANGE_APPEARING: return (mTransitionTypes & FLAG_CHANGE_APPEARING) == FLAG_CHANGE_APPEARING; case CHANGE_DISAPPEARING: return (mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING; case CHANGING: return (mTransitionTypes & FLAG_CHANGING) == FLAG_CHANGING; } return false; } /** * Sets the start delay on one of the animation objects used by this transition. The * transitionType parameter determines the animation whose start delay * is being set. * * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines * the animation whose start delay is being set. * @param delay The length of time, in milliseconds, to delay before starting the animation. * @see Animator#setStartDelay(long) */ public void setStartDelay(int transitionType, long delay) { switch (transitionType) { case CHANGE_APPEARING: mChangingAppearingDelay = delay; break; case CHANGE_DISAPPEARING: mChangingDisappearingDelay = delay; break; case CHANGING: mChangingDelay = delay; break; case APPEARING: mAppearingDelay = delay; break; case DISAPPEARING: mDisappearingDelay = delay; break; } } /** * Gets the start delay on one of the animation objects used by this transition. The * transitionType parameter determines the animation whose start delay * is returned. * * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines * the animation whose start delay is returned. * @return long The start delay of the specified animation. * @see Animator#getStartDelay() */ public long getStartDelay(int transitionType) { switch (transitionType) { case CHANGE_APPEARING: return mChangingAppearingDelay; case CHANGE_DISAPPEARING: return mChangingDisappearingDelay; case CHANGING: return mChangingDelay; case APPEARING: return mAppearingDelay; case DISAPPEARING: return mDisappearingDelay; } // shouldn't reach here return 0; } /** * Sets the duration on one of the animation objects used by this transition. The * transitionType parameter determines the animation whose duration * is being set. * * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines * the animation whose duration is being set. * @param duration The length of time, in milliseconds, that the specified animation should run. * @see Animator#setDuration(long) */ public void setDuration(int transitionType, long duration) { switch (transitionType) { case CHANGE_APPEARING: mChangingAppearingDuration = duration; break; case CHANGE_DISAPPEARING: mChangingDisappearingDuration = duration; break; case CHANGING: mChangingDuration = duration; break; case APPEARING: mAppearingDuration = duration; break; case DISAPPEARING: mDisappearingDuration = duration; break; } } /** * Gets the duration on one of the animation objects used by this transition. The * transitionType parameter determines the animation whose duration * is returned. * * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines * the animation whose duration is returned. * @return long The duration of the specified animation. * @see Animator#getDuration() */ public long getDuration(int transitionType) { switch (transitionType) { case CHANGE_APPEARING: return mChangingAppearingDuration; case CHANGE_DISAPPEARING: return mChangingDisappearingDuration; case CHANGING: return mChangingDuration; case APPEARING: return mAppearingDuration; case DISAPPEARING: return mDisappearingDuration; } // shouldn't reach here return 0; } /** * Sets the length of time to delay between starting each animation during one of the * change animations. * * @param transitionType A value of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, or * {@link #CHANGING}. * @param duration The length of time, in milliseconds, to delay before launching the next * animation in the sequence. */ public void setStagger(int transitionType, long duration) { switch (transitionType) { case CHANGE_APPEARING: mChangingAppearingStagger = duration; break; case CHANGE_DISAPPEARING: mChangingDisappearingStagger = duration; break; case CHANGING: mChangingStagger = duration; break; // noop other cases } } /** * Gets the length of time to delay between starting each animation during one of the * change animations. * * @param transitionType A value of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, or * {@link #CHANGING}. * @return long The length of time, in milliseconds, to delay before launching the next * animation in the sequence. */ public long getStagger(int transitionType) { switch (transitionType) { case CHANGE_APPEARING: return mChangingAppearingStagger; case CHANGE_DISAPPEARING: return mChangingDisappearingStagger; case CHANGING: return mChangingStagger; } // shouldn't reach here return 0; } /** * Sets the interpolator on one of the animation objects used by this transition. The * transitionType parameter determines the animation whose interpolator * is being set. * * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines * the animation whose interpolator is being set. * @param interpolator The interpolator that the specified animation should use. * @see Animator#setInterpolator(TimeInterpolator) */ public void setInterpolator(int transitionType, TimeInterpolator interpolator) { switch (transitionType) { case CHANGE_APPEARING: mChangingAppearingInterpolator = interpolator; break; case CHANGE_DISAPPEARING: mChangingDisappearingInterpolator = interpolator; break; case CHANGING: mChangingInterpolator = interpolator; break; case APPEARING: mAppearingInterpolator = interpolator; break; case DISAPPEARING: mDisappearingInterpolator = interpolator; break; } } /** * Gets the interpolator on one of the animation objects used by this transition. The * transitionType parameter determines the animation whose interpolator * is returned. * * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines * the animation whose interpolator is being returned. * @return TimeInterpolator The interpolator that the specified animation uses. * @see Animator#setInterpolator(TimeInterpolator) */ public TimeInterpolator getInterpolator(int transitionType) { switch (transitionType) { case CHANGE_APPEARING: return mChangingAppearingInterpolator; case CHANGE_DISAPPEARING: return mChangingDisappearingInterpolator; case CHANGING: return mChangingInterpolator; case APPEARING: return mAppearingInterpolator; case DISAPPEARING: return mDisappearingInterpolator; } // shouldn't reach here return null; } /** * Sets the animation used during one of the transition types that may run. Any * Animator object can be used, but to be most useful in the context of layout * transitions, the animation should either be a ObjectAnimator or a AnimatorSet * of animations including PropertyAnimators. Also, these ObjectAnimator objects * should be able to get and set values on their target objects automatically. For * example, a ObjectAnimator that animates the property "left" is able to set and get the * left property from the View objects being animated by the layout * transition. The transition works by setting target objects and properties * dynamically, according to the pre- and post-layoout values of those objects, so * having animations that can handle those properties appropriately will work best * for custom animation. The dynamic setting of values is only the case for the * CHANGE animations; the APPEARING and DISAPPEARING animations are simply run with * the values they have. * *

It is also worth noting that any and all animations (and their underlying * PropertyValuesHolder objects) will have their start and end values set according * to the pre- and post-layout values. So, for example, a custom animation on "alpha" * as the CHANGE_APPEARING animation will inherit the real value of alpha on the target * object (presumably 1) as its starting and ending value when the animation begins. * Animations which need to use values at the beginning and end that may not match the * values queried when the transition begins may need to use a different mechanism * than a standard ObjectAnimator object.

* * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines the * animation whose animator is being set. * @param animator The animation being assigned. A value of null means that no * animation will be run for the specified transitionType. */ public void setAnimator(int transitionType, Animator animator) { switch (transitionType) { case CHANGE_APPEARING: mChangingAppearingAnim = animator; break; case CHANGE_DISAPPEARING: mChangingDisappearingAnim = animator; break; case CHANGING: mChangingAnim = animator; break; case APPEARING: mAppearingAnim = animator; break; case DISAPPEARING: mDisappearingAnim = animator; break; } } /** * Gets the animation used during one of the transition types that may run. * * @param transitionType One of {@link #CHANGE_APPEARING}, {@link #CHANGE_DISAPPEARING}, * {@link #CHANGING}, {@link #APPEARING}, or {@link #DISAPPEARING}, which determines * the animation whose animator is being returned. * @return Animator The animation being used for the given transition type. * @see #setAnimator(int, Animator) */ public Animator getAnimator(int transitionType) { switch (transitionType) { case CHANGE_APPEARING: return mChangingAppearingAnim; case CHANGE_DISAPPEARING: return mChangingDisappearingAnim; case CHANGING: return mChangingAnim; case APPEARING: return mAppearingAnim; case DISAPPEARING: return mDisappearingAnim; } // shouldn't reach here return null; } /** * This function sets up animations on all of the views that change during layout. * For every child in the parent, we create a change animation of the appropriate * type (appearing, disappearing, or changing) and ask it to populate its start values from its * target view. We add layout listeners to all child views and listen for changes. For * those views that change, we populate the end values for those animations and start them. * Animations are not run on unchanging views. * * @param parent The container which is undergoing a change. * @param newView The view being added to or removed from the parent. May be null if the * changeReason is CHANGING. * @param changeReason A value of APPEARING, DISAPPEARING, or CHANGING, indicating whether the * transition is occurring because an item is being added to or removed from the parent, or * if it is running in response to a layout operation (that is, if the value is CHANGING). */ private void runChangeTransition(final ViewGroup parent, View newView, final int changeReason) { Animator baseAnimator = null; Animator parentAnimator = null; final long duration; switch (changeReason) { case APPEARING: baseAnimator = mChangingAppearingAnim; duration = mChangingAppearingDuration; parentAnimator = defaultChangeIn; break; case DISAPPEARING: baseAnimator = mChangingDisappearingAnim; duration = mChangingDisappearingDuration; parentAnimator = defaultChangeOut; break; case CHANGING: baseAnimator = mChangingAnim; duration = mChangingDuration; parentAnimator = defaultChange; break; default: // Shouldn't reach here duration = 0; break; } // If the animation is null, there's nothing to do if (baseAnimator == null) { return; } // reset the inter-animation delay, in case we use it later staggerDelay = 0; final ViewTreeObserver observer = parent.getViewTreeObserver(); if (!observer.isAlive()) { // If the observer's not in a good state, skip the transition return; } int numChildren = parent.getChildCount(); for (int i = 0; i < numChildren; ++i) { final View child = parent.getChildAt(i); // only animate the views not being added or removed if (child != newView) { 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, parentAnimator, 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. CleanupCallback callback = new CleanupCallback(layoutChangeListenerMap, parent); observer.addOnPreDrawListener(callback); parent.addOnAttachStateChangeListener(callback); } /** * This flag controls whether CHANGE_APPEARING or CHANGE_DISAPPEARING animations will * cause the default 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. * *

The default changing transitions animate the bounds and scroll positions of the * target views. These are the animations that will run on the parent hierarchy, not * the custom animations that happen to be set on the transition. This allows custom * behavior for the children of the transitioning container, but uses standard behavior * of resizing/rescrolling on any changing parents. * * @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) { // If we already have a listener for this child, then we've already set up the // changing animation we need. Multiple calls for a child may occur when several // add/remove operations are run at once on a container; each one will trigger // changes for the existing children in the container. if (layoutChangeListenerMap.get(child) != null) { return; } // Don't animate items up from size(0,0); this is likely because the objects // were offscreen/invisible or otherwise measured to be infinitely small. We don't // want to see them animate into their real size; just ignore animation requests // on these views if (child.getWidth() == 0 && child.getHeight() == 0) { return; } // 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]; if (pvh.mKeyframes instanceof KeyframeSet) { KeyframeSet keyframeSet = (KeyframeSet) pvh.mKeyframes; if (keyframeSet.mFirstKeyframe == null || keyframeSet.mLastKeyframe == null || !keyframeSet.mFirstKeyframe.getValue().equals( keyframeSet.mLastKeyframe.getValue())) { valuesDiffer = true; } } else if (!pvh.mKeyframes.getValue(0).equals(pvh.mKeyframes.getValue(1))) { valuesDiffer = true; } } if (!valuesDiffer) { return; } } long startDelay = 0; switch (changeReason) { case APPEARING: startDelay = mChangingAppearingDelay + staggerDelay; staggerDelay += mChangingAppearingStagger; if (mChangingAppearingInterpolator != sChangingAppearingInterpolator) { anim.setInterpolator(mChangingAppearingInterpolator); } break; case DISAPPEARING: startDelay = mChangingDisappearingDelay + staggerDelay; staggerDelay += mChangingDisappearingStagger; if (mChangingDisappearingInterpolator != sChangingDisappearingInterpolator) { anim.setInterpolator(mChangingDisappearingInterpolator); } break; case CHANGING: startDelay = mChangingDelay + staggerDelay; staggerDelay += mChangingStagger; if (mChangingInterpolator != sChangingInterpolator) { anim.setInterpolator(mChangingInterpolator); } break; } 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 (hasListeners()) { ArrayList listeners = (ArrayList) mListeners.clone(); for (TransitionListener listener : listeners) { listener.startTransition(LayoutTransition.this, parent, child, changeReason == APPEARING ? CHANGE_APPEARING : changeReason == DISAPPEARING ? CHANGE_DISAPPEARING : CHANGING); } } } @Override public void onAnimationCancel(Animator animator) { child.removeOnLayoutChangeListener(listener); layoutChangeListenerMap.remove(child); } @Override public void onAnimationEnd(Animator animator) { currentChangingAnimations.remove(child); if (hasListeners()) { ArrayList listeners = (ArrayList) mListeners.clone(); for (TransitionListener listener : listeners) { listener.endTransition(LayoutTransition.this, parent, child, changeReason == APPEARING ? CHANGE_APPEARING : changeReason == DISAPPEARING ? CHANGE_DISAPPEARING : CHANGING); } } } }); 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() { LinkedHashMap currentAnimCopy = (LinkedHashMap) currentChangingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { if (anim instanceof ObjectAnimator) { ((ObjectAnimator) anim).setCurrentPlayTime(0); } anim.start(); } } /** * Ends the animations that are set up for a CHANGING transition. This is a variant of * startChangingAnimations() which is called when the window the transition is playing in * is not visible. We need to make sure the animations put their targets in their end states * and that the transition finishes to remove any mid-process state (such as isRunning()). * * @hide */ public void endChangingAnimations() { LinkedHashMap currentAnimCopy = (LinkedHashMap) currentChangingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.start(); anim.end(); } // listeners should clean up the currentChangingAnimations list, but just in case... currentChangingAnimations.clear(); } /** * 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. * * @return true if CHANGE_APPEARING or CHANGE_DISAPPEARING animations are currently * running. */ public boolean isChangingLayout() { return (currentChangingAnimations.size() > 0); } /** * Returns true if any of the animations in this transition are currently running. * * @return true if any animations in the transition are running. */ public boolean isRunning() { return (currentChangingAnimations.size() > 0 || currentAppearingAnimations.size() > 0 || currentDisappearingAnimations.size() > 0); } /** * Cancels the currently running transition. Note that we cancel() the changing animations * but end() the visibility animations. This is because this method is currently called * in the context of starting a new transition, so we want to move things from their mid- * transition positions, but we want them to have their end-transition visibility. * * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) public void cancel() { if (currentChangingAnimations.size() > 0) { LinkedHashMap currentAnimCopy = (LinkedHashMap) currentChangingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.cancel(); } currentChangingAnimations.clear(); } if (currentAppearingAnimations.size() > 0) { LinkedHashMap currentAnimCopy = (LinkedHashMap) currentAppearingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.end(); } currentAppearingAnimations.clear(); } if (currentDisappearingAnimations.size() > 0) { LinkedHashMap currentAnimCopy = (LinkedHashMap) currentDisappearingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.end(); } currentDisappearingAnimations.clear(); } } /** * Cancels the specified type of transition. Note that we cancel() the changing animations * but end() the visibility animations. This is because this method is currently called * in the context of starting a new transition, so we want to move things from their mid- * transition positions, but we want them to have their end-transition visibility. * * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) public void cancel(int transitionType) { switch (transitionType) { case CHANGE_APPEARING: case CHANGE_DISAPPEARING: case CHANGING: if (currentChangingAnimations.size() > 0) { LinkedHashMap currentAnimCopy = (LinkedHashMap) currentChangingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.cancel(); } currentChangingAnimations.clear(); } break; case APPEARING: if (currentAppearingAnimations.size() > 0) { LinkedHashMap currentAnimCopy = (LinkedHashMap) currentAppearingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.end(); } currentAppearingAnimations.clear(); } break; case DISAPPEARING: if (currentDisappearingAnimations.size() > 0) { LinkedHashMap currentAnimCopy = (LinkedHashMap) currentDisappearingAnimations.clone(); for (Animator anim : currentAnimCopy.values()) { anim.end(); } currentDisappearingAnimations.clear(); } break; } } /** * This method runs the animation that makes an added item appear. * * @param parent The ViewGroup to which the View is being added. * @param child The View being added to the ViewGroup. */ private void runAppearingTransition(final ViewGroup parent, final View child) { Animator currentAnimation = currentDisappearingAnimations.get(child); if (currentAnimation != null) { currentAnimation.cancel(); } if (mAppearingAnim == null) { if (hasListeners()) { ArrayList listeners = (ArrayList) mListeners.clone(); for (TransitionListener listener : listeners) { listener.endTransition(LayoutTransition.this, parent, child, APPEARING); } } return; } Animator anim = mAppearingAnim.clone(); anim.setTarget(child); anim.setStartDelay(mAppearingDelay); anim.setDuration(mAppearingDuration); if (mAppearingInterpolator != sAppearingInterpolator) { anim.setInterpolator(mAppearingInterpolator); } if (anim instanceof ObjectAnimator) { ((ObjectAnimator) anim).setCurrentPlayTime(0); } anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator anim) { currentAppearingAnimations.remove(child); if (hasListeners()) { ArrayList listeners = (ArrayList) mListeners.clone(); for (TransitionListener listener : listeners) { listener.endTransition(LayoutTransition.this, parent, child, APPEARING); } } } }); currentAppearingAnimations.put(child, anim); anim.start(); } /** * This method runs the animation that makes a removed item disappear. * * @param parent The ViewGroup from which the View is being removed. * @param child The View being removed from the ViewGroup. */ private void runDisappearingTransition(final ViewGroup parent, final View child) { Animator currentAnimation = currentAppearingAnimations.get(child); if (currentAnimation != null) { currentAnimation.cancel(); } if (mDisappearingAnim == null) { if (hasListeners()) { ArrayList listeners = (ArrayList) mListeners.clone(); for (TransitionListener listener : listeners) { listener.endTransition(LayoutTransition.this, parent, child, DISAPPEARING); } } return; } Animator anim = mDisappearingAnim.clone(); anim.setStartDelay(mDisappearingDelay); anim.setDuration(mDisappearingDuration); if (mDisappearingInterpolator != sDisappearingInterpolator) { anim.setInterpolator(mDisappearingInterpolator); } anim.setTarget(child); final float preAnimAlpha = child.getAlpha(); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator anim) { currentDisappearingAnimations.remove(child); child.setAlpha(preAnimAlpha); if (hasListeners()) { ArrayList listeners = (ArrayList) mListeners.clone(); for (TransitionListener listener : listeners) { listener.endTransition(LayoutTransition.this, parent, child, DISAPPEARING); } } } }); if (anim instanceof ObjectAnimator) { ((ObjectAnimator) anim).setCurrentPlayTime(0); } currentDisappearingAnimations.put(child, anim); anim.start(); } /** * This method is called by ViewGroup when a child view is about to be added to the * container. This callback starts the process of a transition; we grab the starting * values, listen for changes to all of the children of the container, and start appropriate * animations. * * @param parent The ViewGroup to which the View is being added. * @param child The View being added to the ViewGroup. * @param changesLayout Whether the removal will cause changes in the layout of other views * in the container. INVISIBLE views becoming VISIBLE will not cause changes and thus will not * affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations. */ private void addChild(ViewGroup parent, View child, boolean changesLayout) { if (parent.getWindowVisibility() != View.VISIBLE) { return; } if ((mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) { // Want disappearing animations to finish up before proceeding cancel(DISAPPEARING); } if (changesLayout && (mTransitionTypes & FLAG_CHANGE_APPEARING) == FLAG_CHANGE_APPEARING) { // Also, cancel changing animations so that we start fresh ones from current locations cancel(CHANGE_APPEARING); cancel(CHANGING); } if (hasListeners() && (mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) { ArrayList listeners = (ArrayList) mListeners.clone(); for (TransitionListener listener : listeners) { listener.startTransition(this, parent, child, APPEARING); } } if (changesLayout && (mTransitionTypes & FLAG_CHANGE_APPEARING) == FLAG_CHANGE_APPEARING) { runChangeTransition(parent, child, APPEARING); } if ((mTransitionTypes & FLAG_APPEARING) == FLAG_APPEARING) { runAppearingTransition(parent, child); } } private boolean hasListeners() { return mListeners != null && mListeners.size() > 0; } /** * This method is called by ViewGroup when there is a call to layout() on the container * with this LayoutTransition. If the CHANGING transition is enabled and if there is no other * transition currently running on the container, then this call runs a CHANGING transition. * The transition does not start immediately; it just sets up the mechanism to run if any * of the children of the container change their layout parameters (similar to * the CHANGE_APPEARING and CHANGE_DISAPPEARING transitions). * * @param parent The ViewGroup whose layout() method has been called. * * @hide */ public void layoutChange(ViewGroup parent) { if (parent.getWindowVisibility() != View.VISIBLE) { return; } if ((mTransitionTypes & FLAG_CHANGING) == FLAG_CHANGING && !isRunning()) { // This method is called for all calls to layout() in the container, including // those caused by add/remove/hide/show events, which will already have set up // transition animations. Avoid setting up CHANGING animations in this case; only // do so when there is not a transition already running on the container. runChangeTransition(parent, null, CHANGING); } } /** * This method is called by ViewGroup when a child view is about to be added to the * container. This callback starts the process of a transition; we grab the starting * values, listen for changes to all of the children of the container, and start appropriate * animations. * * @param parent The ViewGroup to which the View is being added. * @param child The View being added to the ViewGroup. */ public void addChild(ViewGroup parent, View child) { addChild(parent, child, true); } /** * @deprecated Use {@link #showChild(android.view.ViewGroup, android.view.View, int)}. */ @Deprecated public void showChild(ViewGroup parent, View child) { addChild(parent, child, true); } /** * This method is called by ViewGroup when a child view is about to be made visible in the * container. This callback starts the process of a transition; we grab the starting * values, listen for changes to all of the children of the container, and start appropriate * animations. * * @param parent The ViewGroup in which the View is being made visible. * @param child The View being made visible. * @param oldVisibility The previous visibility value of the child View, either * {@link View#GONE} or {@link View#INVISIBLE}. */ public void showChild(ViewGroup parent, View child, int oldVisibility) { addChild(parent, child, oldVisibility == View.GONE); } /** * This method is called by ViewGroup when a child view is about to be removed from the * container. This callback starts the process of a transition; we grab the starting * values, listen for changes to all of the children of the container, and start appropriate * animations. * * @param parent The ViewGroup from which the View is being removed. * @param child The View being removed from the ViewGroup. * @param changesLayout Whether the removal will cause changes in the layout of other views * in the container. Views becoming INVISIBLE will not cause changes and thus will not * affect CHANGE_APPEARING or CHANGE_DISAPPEARING animations. */ private void removeChild(ViewGroup parent, View child, boolean changesLayout) { if (parent.getWindowVisibility() != View.VISIBLE) { return; } if ((mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) { // Want appearing animations to finish up before proceeding cancel(APPEARING); } if (changesLayout && (mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) { // Also, cancel changing animations so that we start fresh ones from current locations cancel(CHANGE_DISAPPEARING); cancel(CHANGING); } if (hasListeners() && (mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) { ArrayList listeners = (ArrayList) mListeners .clone(); for (TransitionListener listener : listeners) { listener.startTransition(this, parent, child, DISAPPEARING); } } if (changesLayout && (mTransitionTypes & FLAG_CHANGE_DISAPPEARING) == FLAG_CHANGE_DISAPPEARING) { runChangeTransition(parent, child, DISAPPEARING); } if ((mTransitionTypes & FLAG_DISAPPEARING) == FLAG_DISAPPEARING) { runDisappearingTransition(parent, child); } } /** * This method is called by ViewGroup when a child view is about to be removed from the * container. This callback starts the process of a transition; we grab the starting * values, listen for changes to all of the children of the container, and start appropriate * animations. * * @param parent The ViewGroup from which the View is being removed. * @param child The View being removed from the ViewGroup. */ public void removeChild(ViewGroup parent, View child) { removeChild(parent, child, true); } /** * @deprecated Use {@link #hideChild(android.view.ViewGroup, android.view.View, int)}. */ @Deprecated public void hideChild(ViewGroup parent, View child) { removeChild(parent, child, true); } /** * This method is called by ViewGroup when a child view is about to be hidden in * container. This callback starts the process of a transition; we grab the starting * values, listen for changes to all of the children of the container, and start appropriate * animations. * * @param parent The parent ViewGroup of the View being hidden. * @param child The View being hidden. * @param newVisibility The new visibility value of the child View, either * {@link View#GONE} or {@link View#INVISIBLE}. */ public void hideChild(ViewGroup parent, View child, int newVisibility) { removeChild(parent, child, newVisibility == View.GONE); } /** * Add a listener that will be called when the bounds of the view change due to * layout processing. * * @param listener The listener that will be called when layout bounds change. */ public void addTransitionListener(TransitionListener listener) { if (mListeners == null) { mListeners = new ArrayList(); } mListeners.add(listener); } /** * Remove a listener for layout changes. * * @param listener The listener for layout bounds change. */ public void removeTransitionListener(TransitionListener listener) { if (mListeners == null) { return; } mListeners.remove(listener); } /** * Gets the current list of listeners for layout changes. * @return */ public List getTransitionListeners() { return mListeners; } /** * This interface is used for listening to starting and ending events for transitions. */ public interface TransitionListener { /** * This event is sent to listeners when any type of transition animation begins. * * @param transition The LayoutTransition sending out the event. * @param container The ViewGroup on which the transition is playing. * @param view The View object being affected by the transition animation. * @param transitionType The type of transition that is beginning, * {@link android.animation.LayoutTransition#APPEARING}, * {@link android.animation.LayoutTransition#DISAPPEARING}, * {@link android.animation.LayoutTransition#CHANGE_APPEARING}, or * {@link android.animation.LayoutTransition#CHANGE_DISAPPEARING}. */ public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType); /** * This event is sent to listeners when any type of transition animation ends. * * @param transition The LayoutTransition sending out the event. * @param container The ViewGroup on which the transition is playing. * @param view The View object being affected by the transition animation. * @param transitionType The type of transition that is ending, * {@link android.animation.LayoutTransition#APPEARING}, * {@link android.animation.LayoutTransition#DISAPPEARING}, * {@link android.animation.LayoutTransition#CHANGE_APPEARING}, or * {@link android.animation.LayoutTransition#CHANGE_DISAPPEARING}. */ public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType); } /** * Utility class to clean up listeners after animations are setup. Cleanup happens * when either the OnPreDrawListener method is called or when the parent is detached, * whichever comes first. */ private static final class CleanupCallback implements ViewTreeObserver.OnPreDrawListener, View.OnAttachStateChangeListener { final Map layoutChangeListenerMap; final ViewGroup parent; CleanupCallback(Map listenerMap, ViewGroup parent) { this.layoutChangeListenerMap = listenerMap; this.parent = parent; } private void cleanup() { parent.getViewTreeObserver().removeOnPreDrawListener(this); parent.removeOnAttachStateChangeListener(this); int count = layoutChangeListenerMap.size(); if (count > 0) { Collection views = layoutChangeListenerMap.keySet(); for (View view : views) { View.OnLayoutChangeListener listener = layoutChangeListenerMap.get(view); view.removeOnLayoutChangeListener(listener); } layoutChangeListenerMap.clear(); } } @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { cleanup(); } @Override public boolean onPreDraw() { cleanup(); return true; } }; }