OSDN Git Service

Refactored the notification animations, improved stack scroller
authorSelim Cinek <cinek@google.com>
Fri, 2 May 2014 15:07:49 +0000 (17:07 +0200)
committerSelim Cinek <cinek@google.com>
Wed, 7 May 2014 13:00:45 +0000 (15:00 +0200)
Animations are now only triggered when absolutely needed.
In addition, the notifications are now not clipped anymore when starting
a drag on them and the notification below the dragged one is fadded in if
necessary.

Change-Id: I80e8b3ea8fb48505edfb3cace6176dfa00c5a659

packages/SystemUI/res/values/ids.xml [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/SwipeHelper.java
packages/SystemUI/src/com/android/systemui/recent/RecentsHorizontalScrollView.java
packages/SystemUI/src/com/android/systemui/recent/RecentsVerticalScrollView.java
packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpNotificationView.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/StackStateAnimator.java

diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
new file mode 100644 (file)
index 0000000..e5168c4
--- /dev/null
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 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
+  -->
+
+<resources>
+    <item type="id" name="translation_y_animator_tag"/>
+    <item type="id" name="translation_z_animator_tag"/>
+    <item type="id" name="alpha_animator_tag"/>
+    <item type="id" name="top_inset_animator_tag"/>
+    <item type="id" name="height_animator_tag"/>
+    <item type="id" name="translation_y_animator_end_value_tag"/>
+    <item type="id" name="translation_z_animator_end_value_tag"/>
+    <item type="id" name="alpha_animator_end_value_tag"/>
+    <item type="id" name="top_inset_animator_end_value_tag"/>
+    <item type="id" name="height_animator_end_value_tag"/>
+</resources>
+
index d38d828..6387a92 100644 (file)
@@ -322,6 +322,7 @@ public class SwipeHelper implements Gefingerpoken {
         anim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animator) {
                 updateAlphaFromOffset(animView, canAnimViewBeDismissed);
+                mCallback.onChildSnappedBack(animView);
             }
         });
         anim.start();
@@ -407,5 +408,7 @@ public class SwipeHelper implements Gefingerpoken {
         void onChildDismissed(View v);
 
         void onDragCancelled(View v);
+
+        void onChildSnappedBack(View animView);
     }
 }
index 35c824b..0759b8e 100644 (file)
@@ -217,6 +217,10 @@ public class RecentsHorizontalScrollView extends HorizontalScrollView
     public void onDragCancelled(View v) {
     }
 
+    @Override
+    public void onChildSnappedBack(View animView) {
+    }
+
     public View getChildAtPosition(MotionEvent ev) {
         final float x = ev.getX() + getScrollX();
         final float y = ev.getY() + getScrollY();
index 297fe0d..c2dde6a 100644 (file)
@@ -225,6 +225,10 @@ public class RecentsVerticalScrollView extends ScrollView
     public void onDragCancelled(View v) {
     }
 
+    @Override
+    public void onChildSnappedBack(View animView) {
+    }
+
     public View getChildAtPosition(MotionEvent ev) {
         final float x = ev.getX() + getScrollX();
         final float y = ev.getY() + getScrollY();
index 1664a32..232795a 100644 (file)
 package com.android.systemui.statusbar;
 
 import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Outline;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.InsetDrawable;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.FrameLayout;
@@ -98,6 +93,10 @@ public abstract class ExpandableView extends FrameLayout {
         mClipTopAmount = clipTopAmount;
     }
 
+    public int getClipTopAmount() {
+        return mClipTopAmount;
+    }
+
     public void setOnHeightChangedListener(OnHeightChangedListener listener) {
         mOnHeightChangedListener = listener;
     }
index 1f15eaf..379bd05 100644 (file)
@@ -94,10 +94,6 @@ public class NotificationContentView extends ExpandableView {
         updateClipping();
     }
 
-    public int getClipTopAmount() {
-        return mClipTopAmount;
-    }
-
     private void updateClipping() {
         mClipBounds.set(0, mClipTopAmount, getWidth(), mActualHeight);
         setClipBounds(mClipBounds);
index 72e22e9..81e2cb3 100644 (file)
@@ -237,6 +237,10 @@ public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper.
     }
 
     @Override
+    public void onChildSnappedBack(View animView) {
+    }
+
+    @Override
     public View getChildAtPosition(MotionEvent ev) {
         return mContentHolder;
     }
index e4e5fb1..b72909f 100644 (file)
@@ -97,6 +97,8 @@ public class NotificationStackScrollLayout extends ViewGroup
     private StackScrollState mCurrentStackScrollState = new StackScrollState(this);
     private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>();
     private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>();
+    private ArrayList<View> mSnappedBackChildren = new ArrayList<View>();
+    private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>();
     private ArrayList<AnimationEvent> mAnimationEvents
             = new ArrayList<AnimationEvent>();
     private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
@@ -377,11 +379,34 @@ public class NotificationStackScrollLayout extends ViewGroup
             veto.performClick();
         }
         setSwipingInProgress(false);
+        if (mDragAnimPendingChildren.contains(v)) {
+            // We start the swipe and finish it in the same frame, we don't want any animation
+            // for the drag
+            mDragAnimPendingChildren.remove(v);
+        }
         mSwipedOutViews.add(v);
+        mStackScrollAlgorithm.onDragFinished(v);
+    }
+
+    @Override
+    public void onChildSnappedBack(View animView) {
+        mStackScrollAlgorithm.onDragFinished(animView);
+        if (!mDragAnimPendingChildren.contains(animView)) {
+            mSnappedBackChildren.add(animView);
+            requestChildrenUpdate();
+            mNeedsAnimation = true;
+        } else {
+            // We start the swipe and snap back in the same frame, we don't want any animation
+            mDragAnimPendingChildren.remove(animView);
+        }
     }
 
     public void onBeginDrag(View v) {
         setSwipingInProgress(true);
+        mDragAnimPendingChildren.add(v);
+        mStackScrollAlgorithm.onBeginDrag(v);
+        requestChildrenUpdate();
+        mNeedsAnimation = true;
     }
 
     public void onDragCancelled(View v) {
@@ -670,7 +695,7 @@ public class NotificationStackScrollLayout extends ViewGroup
 //                        mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
 //                    }
                 }
-                updateChildren();
+                requestChildrenUpdate();
             }
 
             // Keep on drawing until the animation has finished.
@@ -680,7 +705,7 @@ public class NotificationStackScrollLayout extends ViewGroup
 
     private void customScrollTo(int y) {
         mOwnScrollY = y;
-        updateChildren();
+        requestChildrenUpdate();
     }
 
     @Override
@@ -696,7 +721,7 @@ public class NotificationStackScrollLayout extends ViewGroup
             if (clampedY) {
                 mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange());
             }
-            updateChildren();
+            requestChildrenUpdate();
         } else {
             customScrollTo(scrollY);
             scrollTo(scrollX, mScrollY);
@@ -934,12 +959,30 @@ public class NotificationStackScrollLayout extends ViewGroup
     private void generateChildHierarchyEvents() {
         generateChildAdditionEvents();
         generateChildRemovalEvents();
+        generateSnapBackEvents();
+        generateDragEvents();
         generateTopPaddingEvent();
         mNeedsAnimation = false;
     }
 
+    private void generateSnapBackEvents() {
+        for (View child : mSnappedBackChildren) {
+            mAnimationEvents.add(new AnimationEvent(child,
+                    AnimationEvent.ANIMATION_TYPE_SNAP_BACK));
+        }
+        mSnappedBackChildren.clear();
+    }
+
+    private void generateDragEvents() {
+        for (View child : mDragAnimPendingChildren) {
+            mAnimationEvents.add(new AnimationEvent(child,
+                    AnimationEvent.ANIMATION_TYPE_START_DRAG));
+        }
+        mDragAnimPendingChildren.clear();
+    }
+
     private void generateChildRemovalEvents() {
-        for (View  child : mChildrenToRemoveAnimated) {
+        for (View child : mChildrenToRemoveAnimated) {
             boolean childWasSwipedOut = mSwipedOutViews.contains(child);
             int animationType = childWasSwipedOut
                     ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
@@ -951,7 +994,7 @@ public class NotificationStackScrollLayout extends ViewGroup
     }
 
     private void generateChildAdditionEvents() {
-        for (View  child : mChildrenToAddAnimated) {
+        for (View child : mChildrenToAddAnimated) {
             mAnimationEvents.add(new AnimationEvent(child,
                     AnimationEvent.ANIMATION_TYPE_ADD));
         }
@@ -1173,6 +1216,8 @@ public class NotificationStackScrollLayout extends ViewGroup
         static int ANIMATION_TYPE_REMOVE = 2;
         static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 3;
         static int ANIMATION_TYPE_TOP_PADDING_CHANGED = 4;
+        static int ANIMATION_TYPE_START_DRAG = 5;
+        static int ANIMATION_TYPE_SNAP_BACK = 6;
 
         final long eventStartTime;
         final View changingView;
index 09d8d50..f7818c0 100644 (file)
@@ -61,6 +61,7 @@ public class StackScrollAlgorithm {
     private ExpandableView mFirstChildWhileExpanding;
     private boolean mExpandedOnStart;
     private int mTopStackTotalSize;
+    private ArrayList<View> mDraggedViews = new ArrayList<View>();
 
     public StackScrollAlgorithm(Context context) {
         initConstants(context);
@@ -118,6 +119,34 @@ public class StackScrollAlgorithm {
 
         // Phase 3:
         updateZValuesForState(resultState, algorithmState);
+
+        handleDraggedViews(resultState, algorithmState);
+    }
+
+    /**
+     * Handle the special state when views are being dragged
+     */
+    private void handleDraggedViews(StackScrollState resultState,
+            StackScrollAlgorithmState algorithmState) {
+        for (View draggedView : mDraggedViews) {
+            int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
+            if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
+                View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
+                if (!mDraggedViews.contains(nextChild)) {
+                    // only if the view is not dragged itself we modify its state to be fully
+                    // visible
+                    StackScrollState.ViewState viewState = resultState.getViewStateForView(
+                            nextChild);
+                    // The child below the dragged one must be fully visible
+                    viewState.alpha = 1;
+                }
+
+                // Lets set the alpha to the one it currently has, as its currently being dragged
+                StackScrollState.ViewState viewState = resultState.getViewStateForView(draggedView);
+                // The dragged child should keep the set alpha
+                viewState.alpha = draggedView.getAlpha();
+            }
+        }
     }
 
     /**
@@ -566,6 +595,14 @@ public class StackScrollAlgorithm {
         }
     }
 
+    public void onBeginDrag(View view) {
+        mDraggedViews.add(view);
+    }
+
+    public void onDragFinished(View view) {
+        mDraggedViews.remove(view);
+    }
+
     class StackScrollAlgorithmState {
 
         /**
index 26cef36..70126f5 100644 (file)
@@ -93,6 +93,7 @@ public class StackScrollState {
         int numChildren = mHostView.getChildCount();
         float previousNotificationEnd = 0;
         float previousNotificationStart = 0;
+        boolean previousNotificationIsSwiped = false;
         for (int i = 0; i < numChildren; i++) {
             ExpandableView child = (ExpandableView) mHostView.getChildAt(i);
             ViewState state = mStateMap.get(child);
@@ -153,12 +154,20 @@ public class StackScrollState {
 
                 // apply clipping and shadow
                 float newNotificationEnd = newYTranslation + newHeight;
+
+                // When the previous notification is swiped, we don't clip the content to the
+                // bottom of it.
+                float clipHeight = previousNotificationIsSwiped
+                        ? newHeight
+                        : newNotificationEnd - (previousNotificationEnd);
+
                 updateChildClippingAndBackground(child, newHeight,
-                        newNotificationEnd - (previousNotificationEnd),
+                        clipHeight,
                         (int) (newHeight - (previousNotificationStart - newYTranslation)));
 
-                previousNotificationStart = newYTranslation;
+                previousNotificationStart = newYTranslation + child.getClipTopAmount();
                 previousNotificationEnd = newNotificationEnd;
+                previousNotificationIsSwiped = child.getTranslationX() != 0;
             }
         }
     }
index 4dce288..3281b67 100644 (file)
 
 package com.android.systemui.statusbar.stack;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.view.View;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
+
+import com.android.systemui.R;
 import com.android.systemui.statusbar.ExpandableView;
 
 import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Stack;
 
 /**
  * An stack state animator which handles animations to new StackScrollStates
@@ -30,130 +38,360 @@ import java.util.ArrayList;
 public class StackStateAnimator {
 
     private static final int ANIMATION_DURATION = 360;
+    private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag;
+    private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag;
+    private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag;
+    private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag;
+    private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag;
+    private static final int TAG_END_TRANSLATION_Y = R.id.translation_y_animator_end_value_tag;
+    private static final int TAG_END_TRANSLATION_Z = R.id.translation_z_animator_end_value_tag;
+    private static final int TAG_END_ALPHA = R.id.alpha_animator_end_value_tag;
+    private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag;
+    private static final int TAG_END_TOP_INSET = R.id.top_inset_animator_end_value_tag;
 
     private final Interpolator mFastOutSlowInInterpolator;
     public NotificationStackScrollLayout mHostLayout;
-    private boolean mAnimationIsRunning;
     private ArrayList<NotificationStackScrollLayout.AnimationEvent> mHandledEvents =
             new ArrayList<>();
+    private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
+            new ArrayList<>();
+    private Set<Animator> mAnimatorSet = new HashSet<Animator>();
+    private Stack<AnimatorListenerAdapter> mAnimationListenerPool
+            = new Stack<AnimatorListenerAdapter>();
 
     public StackStateAnimator(NotificationStackScrollLayout hostLayout) {
         mHostLayout = hostLayout;
         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(hostLayout.getContext(),
-                        android.R.interpolator.fast_out_slow_in);
+                android.R.interpolator.fast_out_slow_in);
     }
 
     public boolean isRunning() {
-        return mAnimationIsRunning;
+        return !mAnimatorSet.isEmpty();
     }
 
     public void startAnimationForEvents(
             ArrayList<NotificationStackScrollLayout.AnimationEvent> mAnimationEvents,
             StackScrollState finalState) {
-        int numEvents = mAnimationEvents.size();
-        if (numEvents == 0) {
-            // No events, so we don't perform any animation
-            return;
-        }
-        long lastEventStartTime = mAnimationEvents.get(numEvents - 1).eventStartTime;
-        long eventEnd = lastEventStartTime + ANIMATION_DURATION;
-        long currentTime = AnimationUtils.currentAnimationTimeMillis();
-        long newDuration = eventEnd - currentTime;
-        if (newDuration <= 0) {
-            // last event is long before this, so we don't do anything
-            return;
-        }
-        initializeAddedViewStates(mAnimationEvents, finalState);
+
+        processAnimationEvents(mAnimationEvents, finalState);
+
+        boolean hasNewEvents = !mNewEvents.isEmpty();
         int childCount = mHostLayout.getChildCount();
-        boolean isFirstAnimatingView = true;
         for (int i = 0; i < childCount; i++) {
             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
             StackScrollState.ViewState viewState = finalState.getViewStateForView(child);
             if (viewState == null) {
                 continue;
             }
-            int childVisibility = child.getVisibility();
-            boolean wasVisible = childVisibility == View.VISIBLE;
-            final float alpha = viewState.alpha;
-            if (!wasVisible && alpha != 0 && !viewState.gone) {
-                child.setVisibility(View.VISIBLE);
-            }
 
-            startPropertyAnimation(newDuration, isFirstAnimatingView, child, viewState, alpha);
+            startAnimations(child, viewState, hasNewEvents);
 
-            // TODO: animate clipBounds
             child.setClipBounds(null);
-            int currentHeigth = child.getActualHeight();
-            if (viewState.height != currentHeigth) {
-                startHeightAnimation(newDuration, child, viewState, currentHeigth);
-            }
-            isFirstAnimatingView = false;
         }
-        mAnimationIsRunning = true;
+        if (!isRunning()) {
+            // no child has preformed any animation, lets finish
+            onAnimationFinished();
+        }
     }
 
-    private void startPropertyAnimation(long newDuration, final boolean hasFinishAction,
-            final ExpandableView child, StackScrollState.ViewState viewState, final float alpha) {
-        child.animate().setInterpolator(mFastOutSlowInInterpolator)
-                .translationY(viewState.yTranslation)
-                .translationZ(viewState.zTranslation)
-                .setDuration(newDuration)
-                .withEndAction(new Runnable() {
-                    @Override
-                    public void run() {
-                        mAnimationIsRunning = false;
-                        if (hasFinishAction) {
-                            mHandledEvents.clear();
-                            mHostLayout.onChildAnimationFinished();
-                        }
-                        if (alpha == 0) {
-                            child.setVisibility(View.INVISIBLE);
-                        }
-                    }
-                });
+    /**
+     * Start an animation to the given viewState
+     */
+    private void startAnimations(final ExpandableView child, StackScrollState.ViewState viewState,
+            boolean hasNewEvents) {
+        int childVisibility = child.getVisibility();
+        boolean wasVisible = childVisibility == View.VISIBLE;
+        final float alpha = viewState.alpha;
+        if (!wasVisible && alpha != 0 && !viewState.gone) {
+            child.setVisibility(View.VISIBLE);
+        }
+        // start translationY animation
+        if (child.getTranslationY() != viewState.yTranslation) {
+            startYTranslationAnimation(child, viewState, hasNewEvents);
+        }
+        // start translationZ animation
+        if (child.getTranslationZ() != viewState.zTranslation) {
+            startZTranslationAnimation(child, viewState, hasNewEvents);
+        }
+        // start alpha animation
         if (alpha != child.getAlpha()) {
-            child.animate().withLayer().alpha(alpha);
+            startAlphaAnimation(child, viewState, hasNewEvents);
+        }
+        // start height animation
+        if (viewState.height != child.getActualHeight()) {
+            startHeightAnimation(child, viewState, hasNewEvents);
         }
     }
 
-    private void startHeightAnimation(long newDuration, final ExpandableView child,
-            StackScrollState.ViewState viewState, int currentHeigth) {
-        ValueAnimator heightAnimator = ValueAnimator.ofInt(currentHeigth, viewState.height);
-        heightAnimator.setInterpolator(mFastOutSlowInInterpolator);
-        heightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+    private void startHeightAnimation(final ExpandableView child,
+            StackScrollState.ViewState viewState, boolean hasNewEvents) {
+        Integer previousEndValue = getChildTag(child,TAG_END_HEIGHT);
+        if (previousEndValue != null && previousEndValue == viewState.height) {
+            return;
+        }
+        ValueAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_HEIGHT);
+        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator, hasNewEvents);
+        if (newDuration <= 0) {
+            if (previousAnimator == null) {
+                // no animation was running, but also no new animation should be performed,
+                // lets just apply the value
+                child.setActualHeight(viewState.height);
+            }
+            return;
+        }
+
+        ValueAnimator animator = ValueAnimator.ofInt(child.getActualHeight(), viewState.height);
+        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator animation) {
                 child.setActualHeight((int) animation.getAnimatedValue());
             }
         });
-        heightAnimator.setDuration(newDuration);
-        heightAnimator.start();
+        animator.setInterpolator(mFastOutSlowInInterpolator);
+        animator.setDuration(newDuration);
+        animator.addListener(getGlobalAnimationFinishedListener());
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                child.setTag(TAG_ANIMATOR_HEIGHT, null);
+                child.setTag(TAG_END_HEIGHT, null);
+            }
+        });
+        animator.start();
+        child.setTag(TAG_ANIMATOR_HEIGHT, animator);
+        child.setTag(TAG_END_HEIGHT, viewState.height);
+    }
+
+    private void startAlphaAnimation(final ExpandableView child,
+            final StackScrollState.ViewState viewState, boolean hasNewEvents) {
+        final float endAlpha = viewState.alpha;
+        Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
+        if (previousEndValue != null && previousEndValue == endAlpha) {
+            return;
+        }
+        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_ALPHA);
+        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator, hasNewEvents);
+        if (newDuration <= 0) {
+            if (previousAnimator == null) {
+                // no animation was running, but also no new animation should be performed,
+                // lets just apply the value
+                child.setAlpha(endAlpha);
+                if (endAlpha == 0) {
+                    child.setVisibility(View.INVISIBLE);
+                }
+            }
+            return;
+        }
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA,
+                child.getAlpha(), endAlpha);
+        animator.setInterpolator(mFastOutSlowInInterpolator);
+        // Handle layer type
+        final int currentLayerType = child.getLayerType();
+        child.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        animator.addListener(new AnimatorListenerAdapter() {
+            public boolean mWasCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                child.setLayerType(currentLayerType, null);
+                if (endAlpha == 0 && !mWasCancelled) {
+                    child.setVisibility(View.INVISIBLE);
+                }
+                child.setTag(TAG_ANIMATOR_ALPHA, null);
+                child.setTag(TAG_END_ALPHA, null);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mWasCancelled = true;
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mWasCancelled = false;
+            }
+        });
+        animator.addListener(getGlobalAnimationFinishedListener());
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+
+            }
+        });
+        animator.start();
+        child.setTag(TAG_ANIMATOR_ALPHA, animator);
+        child.setTag(TAG_END_ALPHA, endAlpha);
     }
 
     /**
-     * Initialize the viewStates for the added children
+     * @return an adapter which ensures that onAnimationFinished is called once no animation is
+     *         running anymore
+     */
+    private AnimatorListenerAdapter getGlobalAnimationFinishedListener() {
+        if (!mAnimationListenerPool.empty()) {
+            return mAnimationListenerPool.pop();
+        }
+
+        // We need to create a new one, no reusable ones found
+        return new AnimatorListenerAdapter() {
+            private boolean mWasCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimatorSet.remove(animation);
+                if (mAnimatorSet.isEmpty() && !mWasCancelled) {
+                    onAnimationFinished();
+                }
+                mAnimationListenerPool.push(this);
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mWasCancelled = true;
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mAnimatorSet.add(animation);
+                mWasCancelled = false;
+            }
+        };
+
+    }
+
+    private void startZTranslationAnimation(final ExpandableView child,
+            final StackScrollState.ViewState viewState, boolean hasNewEvents) {
+        Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
+        if (previousEndValue != null && previousEndValue == viewState.zTranslation) {
+            return;
+        }
+        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Z);
+        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator, hasNewEvents);
+        if (newDuration <= 0) {
+            if (previousAnimator == null) {
+                // no animation was running, but also no new animation should be performed,
+                // lets just apply the value
+                child.setTranslationZ(viewState.zTranslation);
+            }
+            return;
+        }
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Z,
+                child.getTranslationZ(), viewState.zTranslation);
+        animator.setInterpolator(mFastOutSlowInInterpolator);
+        animator.addListener(getGlobalAnimationFinishedListener());
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                child.setTag(TAG_ANIMATOR_TRANSLATION_Z, null);
+                child.setTag(TAG_END_TRANSLATION_Z, null);
+            }
+        });
+        animator.start();
+        child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
+        child.setTag(TAG_END_TRANSLATION_Z, viewState.zTranslation);
+    }
+
+    private void startYTranslationAnimation(final ExpandableView child,
+            StackScrollState.ViewState viewState, boolean hasNewEvents) {
+        Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
+        if (previousEndValue != null && previousEndValue == viewState.yTranslation) {
+            return;
+        }
+        ObjectAnimator previousAnimator = getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y);
+        long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator, hasNewEvents);
+        if (newDuration <= 0) {
+            if (previousAnimator == null) {
+                // no animation was running, but also no new animation should be performed,
+                // lets just apply the value
+                child.setTranslationY(viewState.yTranslation);
+            }
+            return;
+        }
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.TRANSLATION_Y,
+                child.getTranslationY(), viewState.yTranslation);
+        animator.setInterpolator(mFastOutSlowInInterpolator);
+        animator.addListener(getGlobalAnimationFinishedListener());
+        // remove the tag when the animation is finished
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                child.setTag(TAG_ANIMATOR_TRANSLATION_Y, null);
+                child.setTag(TAG_END_TRANSLATION_Y, null);
+            }
+        });
+        animator.start();
+        child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
+        child.setTag(TAG_END_TRANSLATION_Y, viewState.yTranslation);
+    }
+
+    private <T> T getChildTag(View child, int tag) {
+        return (T) child.getTag(tag);
+    }
+
+    /**
+     * Cancel the previous animator and get the duration of the new animation.
      *
-     * @param animationEvents the animation events who contain the added children
+     * @param previousAnimator the animator which was running before
+     * @param hasNewEvents indicating whether new events came in in this animation
+     * @return the new duration
+     */
+    private long cancelAnimatorAndGetNewDuration(ValueAnimator previousAnimator,
+            boolean hasNewEvents) {
+        if (previousAnimator != null) {
+            previousAnimator.cancel();
+            if (!hasNewEvents) {
+                // This is only an update, no new event came in. lets just take the remaining
+                // duration as the new duration
+                return (long) ((1.0f - previousAnimator.getAnimatedFraction()) *
+                        previousAnimator.getDuration());
+            }
+        } else if (!hasNewEvents){
+            return 0;
+        }
+        return ANIMATION_DURATION;
+    }
+
+    private void onAnimationFinished() {
+        mHandledEvents.clear();
+        mNewEvents.clear();
+        mHostLayout.onChildAnimationFinished();
+    }
+
+    /**
+     * Process the animationEvents for a new animation
+     *
+     * @param animationEvents the animation events for the animation to perform
      * @param finalState the final state to animate to
      */
-    private void initializeAddedViewStates(
+    private void processAnimationEvents(
             ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
             StackScrollState finalState) {
+        mNewEvents.clear();
         for (NotificationStackScrollLayout.AnimationEvent event: animationEvents) {
             View changingView = event.changingView;
-            if (event.animationType == NotificationStackScrollLayout.AnimationEvent
-                    .ANIMATION_TYPE_ADD && !mHandledEvents.contains(event)) {
-
-                // This item is added, initialize it's properties.
-                StackScrollState.ViewState viewState = finalState.getViewStateForView(changingView);
-                if (viewState == null) {
-                    // The position for this child was never generated, let's continue.
-                    continue;
+            if (!mHandledEvents.contains(event)) {
+                if (event.animationType == NotificationStackScrollLayout.AnimationEvent
+                        .ANIMATION_TYPE_ADD) {
+
+                    // This item is added, initialize it's properties.
+                    StackScrollState.ViewState viewState = finalState
+                            .getViewStateForView(changingView);
+                    if (viewState == null) {
+                        // The position for this child was never generated, let's continue.
+                        continue;
+                    }
+                    changingView.setAlpha(0);
+                    changingView.setTranslationY(viewState.yTranslation);
+                    changingView.setTranslationZ(viewState.zTranslation);
                 }
-                changingView.setAlpha(0);
-                changingView.setTranslationY(viewState.yTranslation);
-                changingView.setTranslationZ(viewState.zTranslation);
                 mHandledEvents.add(event);
+                mNewEvents.add(event);
             }
         }
     }