OSDN Git Service

Improved the animation logic of the stack scroller.
authorSelim Cinek <cinek@google.com>
Mon, 19 May 2014 14:27:37 +0000 (16:27 +0200)
committerSelim Cinek <cinek@google.com>
Fri, 23 May 2014 12:43:17 +0000 (14:43 +0200)
Newly introduced appear and disappear animations when in the shade.
Also introduced individual child delays such that notifications
appear in a slightly more appealing quantum way.
Also fixed a racecondition, such that added notifications already
have their final visibility state when they are added to the scroller.

Bug: 14081264
Change-Id: I18f5c57c2206f8e05996253981f540e97521e102

13 files changed:
packages/SystemUI/src/com/android/systemui/statusbar/ActivatableNotificationView.java
packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java
packages/SystemUI/src/com/android/systemui/statusbar/ExpandableOutlineView.java
packages/SystemUI/src/com/android/systemui/statusbar/ExpandableView.java
packages/SystemUI/src/com/android/systemui/statusbar/NotificationBackgroundView.java
packages/SystemUI/src/com/android/systemui/statusbar/SpeedBumpView.java
packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java
packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
packages/SystemUI/src/com/android/systemui/statusbar/stack/AnimationFilter.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

index ac16164..e3dac4a 100644 (file)
@@ -21,15 +21,25 @@ import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.RectF;
+import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
 import android.view.animation.PathInterpolator;
-
 import com.android.systemui.R;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
 
 /**
  * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer}
@@ -41,6 +51,36 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
     private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
     private static final int ACTIVATE_ANIMATION_LENGTH = 220;
 
+    /**
+     * The amount of width, which is kept in the end when performing a disappear animation (also
+     * the amount from which the horizontal appearing begins)
+     */
+    private static final float HORIZONTAL_COLLAPSED_REST_PARTIAL = 0.05f;
+
+    /**
+     * At which point from [0,1] does the horizontal collapse animation end (or start when
+     * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
+     */
+    private static final float HORIZONTAL_ANIMATION_END = 0.2f;
+
+    /**
+     * At which point from [0,1] does the alpha animation end (or start when
+     * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
+     */
+    private static final float ALPHA_ANIMATION_END = 0.0f;
+
+    /**
+     * At which point from [0,1] does the horizontal collapse animation start (or start when
+     * expanding)? 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
+     */
+    private static final float HORIZONTAL_ANIMATION_START = 1.0f;
+
+    /**
+     * At which point from [0,1] does the vertical collapse animation start (or end when
+     * expanding) 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
+     */
+    private static final float VERTICAL_ANIMATION_START = 1.0f;
+
     private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
             = new PathInterpolator(0.6f, 0, 0.5f, 1);
     private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
@@ -53,6 +93,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
 
     private int mBgTint = 0;
     private int mDimmedBgTint = 0;
+    private final int mRoundedRectCornerRadius;
 
     /**
      * Flag to indicate that the notification has been touched once and the second touch will
@@ -66,22 +107,41 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
 
     private OnActivatedListener mOnActivatedListener;
 
-    private Interpolator mLinearOutSlowInInterpolator;
-    private Interpolator mFastOutSlowInInterpolator;
+    private final Interpolator mLinearOutSlowInInterpolator;
+    private final Interpolator mFastOutSlowInInterpolator;
+    private final Interpolator mSlowOutFastInInterpolator;
+    private final Interpolator mSlowOutLinearInInterpolator;
+    private final Interpolator mLinearInterpolator;
+    private Interpolator mCurrentAppearInterpolator;
+    private Interpolator mCurrentAlphaInterpolator;
 
     private NotificationBackgroundView mBackgroundNormal;
     private NotificationBackgroundView mBackgroundDimmed;
     private ObjectAnimator mBackgroundAnimator;
+    private RectF mAppearAnimationRect = new RectF();
+    private PorterDuffColorFilter mAppearAnimationFilter;
+    private float mAnimationTranslationY;
+    private boolean mDrawingAppearAnimation;
+    private Paint mAppearPaint = new Paint();
+    private ValueAnimator mAppearAnimator;
+    private float mAppearAnimationFraction = -1.0f;
+    private float mAppearAnimationTranslation;
 
     public ActivatableNotificationView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
         mFastOutSlowInInterpolator =
                 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
+        mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
         mLinearOutSlowInInterpolator =
                 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
+        mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f);
+        mLinearInterpolator = new LinearInterpolator();
         setClipChildren(false);
         setClipToPadding(false);
+        mAppearAnimationFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
+        mRoundedRectCornerRadius = getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.notification_quantum_rounded_rect_radius);
     }
 
     @Override
@@ -316,6 +376,202 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
         mBackgroundDimmed.setClipTopAmount(clipTopAmount);
     }
 
+    @Override
+    public void performRemoveAnimation(float translationDirection, Runnable onFinishedRunnable) {
+        enableAppearDrawing(true);
+        if (mDrawingAppearAnimation) {
+            startAppearAnimation(false /* isAppearing */, translationDirection,
+                    0, onFinishedRunnable);
+        }
+    }
+
+    @Override
+    public void performAddAnimation(long delay) {
+        enableAppearDrawing(true);
+        if (mDrawingAppearAnimation) {
+            startAppearAnimation(true /* isAppearing */, -1.0f, delay, null);
+        }
+    }
+
+    private void startAppearAnimation(boolean isAppearing,
+            float translationDirection, long delay, final Runnable onFinishedRunnable) {
+        if (mAppearAnimator != null) {
+            mAppearAnimator.cancel();
+        }
+        mAnimationTranslationY = translationDirection * mActualHeight;
+        if (mAppearAnimationFraction == -1.0f) {
+            // not initialized yet, we start anew
+            if (isAppearing) {
+                mAppearAnimationFraction = 0.0f;
+                mAppearAnimationTranslation = mAnimationTranslationY;
+            } else {
+                mAppearAnimationFraction = 1.0f;
+                mAppearAnimationTranslation = 0;
+            }
+        }
+
+        float targetValue;
+        if (isAppearing) {
+            mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
+            mCurrentAlphaInterpolator = mLinearOutSlowInInterpolator;
+            targetValue = 1.0f;
+        } else {
+            mCurrentAppearInterpolator = mFastOutSlowInInterpolator;
+            mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
+            targetValue = 0.0f;
+        }
+        mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
+                targetValue);
+        mAppearAnimator.setInterpolator(mLinearInterpolator);
+        mAppearAnimator.setDuration(
+                (long) (StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR
+                        * Math.abs(mAppearAnimationFraction - targetValue)));
+        mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                mAppearAnimationFraction = (float) animation.getAnimatedValue();
+                updateAppearAnimationAlpha();
+                updateAppearRect();
+                invalidate();
+            }
+        });
+        if (delay > 0) {
+            // we need to apply the initial state already to avoid drawn frames in the wrong state
+            updateAppearAnimationAlpha();
+            updateAppearRect();
+            mAppearAnimator.setStartDelay(delay);
+        }
+        mAppearAnimator.addListener(new AnimatorListenerAdapter() {
+            private boolean mWasCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (onFinishedRunnable != null) {
+                    onFinishedRunnable.run();
+                }
+                if (!mWasCancelled) {
+                    mAppearAnimationFraction = -1;
+                    setOutlineRect(null);
+                    enableAppearDrawing(false);
+                }
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mWasCancelled = false;
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                mWasCancelled = true;
+            }
+        });
+        mAppearAnimator.start();
+    }
+
+    private void updateAppearRect() {
+        float inverseFraction = (1.0f - mAppearAnimationFraction);
+        float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
+        float translateYTotalAmount = translationFraction * mAnimationTranslationY;
+        mAppearAnimationTranslation = translateYTotalAmount;
+
+        // handle width animation
+        float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
+                / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
+        widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
+        widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
+        float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) *
+                widthFraction);
+        float right = getWidth() - left;
+
+        // handle top animation
+        float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
+                VERTICAL_ANIMATION_START;
+        heightFraction = Math.max(0.0f, heightFraction);
+        heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
+
+        float top;
+        float bottom;
+        if (mAnimationTranslationY > 0.0f) {
+            bottom = mActualHeight - heightFraction * mAnimationTranslationY * 0.1f
+                    - translateYTotalAmount;
+            top = bottom * heightFraction;
+        } else {
+            top = heightFraction * (mActualHeight + mAnimationTranslationY) * 0.1f -
+                    translateYTotalAmount;
+            bottom = mActualHeight * (1 - heightFraction) + top * heightFraction;
+        }
+        mAppearAnimationRect.set(left, top, right, bottom);
+        setOutlineRect(left, top + mAppearAnimationTranslation, right,
+                bottom + mAppearAnimationTranslation);
+    }
+
+    private void updateAppearAnimationAlpha() {
+        int backgroundColor = getBackgroundColor();
+        if (backgroundColor != -1) {
+            float contentAlphaProgress = mAppearAnimationFraction;
+            contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
+            contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
+            contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
+            int sourceColor = Color.argb((int) (255 * (1.0f - contentAlphaProgress)),
+                    Color.red(backgroundColor), Color.green(backgroundColor),
+                    Color.blue(backgroundColor));
+            mAppearAnimationFilter.setColor(sourceColor);
+            mAppearPaint.setColorFilter(mAppearAnimationFilter);
+        }
+    }
+
+    private int getBackgroundColor() {
+        // TODO: get real color
+        return 0xfffafafa;
+    }
+
+    /**
+     * When we draw the appear animation, we render the view in a bitmap and render this bitmap
+     * as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
+     * such that the normal drawing of the views does not happen anymore.
+     *
+     * @param enable Should it be enabled.
+     */
+    private void enableAppearDrawing(boolean enable) {
+        if (enable != mDrawingAppearAnimation) {
+            if (enable) {
+                if (getWidth() == 0 || getActualHeight() == 0) {
+                    // TODO: This should not happen, but it can during expansion. Needs
+                    // investigation
+                    return;
+                }
+                Bitmap bitmap = Bitmap.createBitmap(getWidth(), getActualHeight(),
+                        Bitmap.Config.ARGB_8888);
+                Canvas canvas = new Canvas(bitmap);
+                draw(canvas);
+                mAppearPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP,
+                        Shader.TileMode.CLAMP));
+            } else {
+                mAppearPaint.setShader(null);
+            }
+            mDrawingAppearAnimation = enable;
+            invalidate();
+        }
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (!mDrawingAppearAnimation) {
+            super.dispatchDraw(canvas);
+        } else {
+            drawAppearRect(canvas);
+        }
+    }
+
+    private void drawAppearRect(Canvas canvas) {
+        canvas.save();
+        canvas.translate(0, mAppearAnimationTranslation);
+        canvas.drawRoundRect(mAppearAnimationRect, mRoundedRectCornerRadius,
+                mRoundedRectCornerRadius, mAppearPaint);
+        canvas.restore();
+    }
+
     public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
         mOnActivatedListener = onActivatedListener;
     }
index 457d32e..7bd4894 100644 (file)
@@ -1039,6 +1039,7 @@ public abstract class BaseStatusBar extends SystemUI implements
         if (rowParent != null) rowParent.removeView(entry.row);
         updateRowStates();
         updateNotificationIcons();
+        updateSpeedBump();
 
         return entry.notification;
     }
@@ -1083,8 +1084,22 @@ public abstract class BaseStatusBar extends SystemUI implements
         if (DEBUG) {
             Log.d(TAG, "addNotificationViews: added at " + pos);
         }
-        updateNotificationIcons();
         updateRowStates();
+        updateNotificationIcons();
+        updateSpeedBump();
+    }
+
+    protected void updateSpeedBump() {
+        int n = mNotificationData.size();
+        int speedBumpIndex = -1;
+        for (int i = n-1; i >= 0; i--) {
+            NotificationData.Entry entry = mNotificationData.get(i);
+            if (entry.row.getVisibility() != View.GONE && speedBumpIndex == -1
+                    && entry.row.isBelowSpeedBump() ) {
+                speedBumpIndex = n - 1 - i;
+            }
+        }
+        mStackScroller.updateSpeedBumpIndex(speedBumpIndex);
     }
 
     private void addNotificationViews(IBinder key, StatusBarNotification notification) {
@@ -1104,7 +1119,6 @@ public abstract class BaseStatusBar extends SystemUI implements
         mKeyguardIconOverflowContainer.getIconsView().removeAllViews();
         int n = mNotificationData.size();
         int visibleNotifications = 0;
-        int speedBumpIndex = -1;
         boolean onKeyguard = mState == StatusBarState.KEYGUARD;
         for (int i = n-1; i >= 0; i--) {
             NotificationData.Entry entry = mNotificationData.get(i);
@@ -1125,17 +1139,14 @@ public abstract class BaseStatusBar extends SystemUI implements
                     mKeyguardIconOverflowContainer.getIconsView().addNotification(entry);
                 }
             } else {
-                if (entry.row.getVisibility() == View.GONE) {
+                boolean wasGone = entry.row.getVisibility() == View.GONE;
+                entry.row.setVisibility(View.VISIBLE);
+                if (wasGone) {
                     // notify the scroller of a child addition
                     mStackScroller.generateAddAnimation(entry.row);
                 }
-                entry.row.setVisibility(View.VISIBLE);
                 visibleNotifications++;
             }
-            if (entry.row.getVisibility() != View.GONE && speedBumpIndex == -1
-                    && entry.row.isBelowSpeedBump() ) {
-                speedBumpIndex = n - 1 - i;
-            }
         }
 
         if (onKeyguard && mKeyguardIconOverflowContainer.getIconsView().getChildCount() > 0) {
@@ -1143,8 +1154,6 @@ public abstract class BaseStatusBar extends SystemUI implements
         } else {
             mKeyguardIconOverflowContainer.setVisibility(View.GONE);
         }
-
-        mStackScroller.updateSpeedBumpIndex(speedBumpIndex);
     }
 
     private boolean shouldShowOnKeyguard(StatusBarNotification sbn) {
@@ -1280,6 +1289,7 @@ public abstract class BaseStatusBar extends SystemUI implements
                     return;
                 }
                 updateRowStates();
+                updateSpeedBump();
             }
             catch (RuntimeException e) {
                 // It failed to add cleanly.  Log, and remove the view from the panel.
index a42c194..843db04 100644 (file)
@@ -18,8 +18,8 @@ package com.android.systemui.statusbar;
 
 import android.content.Context;
 import android.graphics.Outline;
+import android.graphics.RectF;
 import android.util.AttributeSet;
-import android.widget.FrameLayout;
 
 /**
  * Like {@link ExpandableView}, but setting an outline for the height and clipping.
@@ -27,9 +27,12 @@ import android.widget.FrameLayout;
 public abstract class ExpandableOutlineView extends ExpandableView {
 
     private final Outline mOutline = new Outline();
+    private boolean mCustomOutline;
+    private float mDensity;
 
     public ExpandableOutlineView(Context context, AttributeSet attrs) {
         super(context, attrs);
+        mDensity = getResources().getDisplayMetrics().density;
     }
 
     @Override
@@ -50,11 +53,37 @@ public abstract class ExpandableOutlineView extends ExpandableView {
         updateOutline();
     }
 
-    private void updateOutline() {
-        mOutline.setRect(0,
-                mClipTopAmount,
-                getWidth(),
-                Math.max(mActualHeight, mClipTopAmount));
+    protected void setOutlineRect(RectF rect) {
+        if (rect != null) {
+            setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
+        } else {
+            mCustomOutline = false;
+            updateOutline();
+        }
+    }
+
+    protected void setOutlineRect(float left, float top, float right, float bottom) {
+        mCustomOutline = true;
+
+        int rectLeft = (int) left;
+        int rectTop = (int) top;
+        int rectRight = (int) right;
+        int rectBottom = (int) bottom;
+
+        // Outlines need to be at least 1 dp
+        rectBottom = (int) Math.max(top + mDensity, rectBottom);
+        rectRight = (int) Math.max(left + mDensity, rectRight);
+        mOutline.setRect(rectLeft, rectTop, rectRight, rectBottom);
         setOutline(mOutline);
     }
+
+    private void updateOutline() {
+        if (!mCustomOutline) {
+            mOutline.setRect(0,
+                    mClipTopAmount,
+                    getWidth(),
+                    Math.max(mActualHeight, mClipTopAmount));
+            setOutline(mOutline);
+        }
+    }
 }
index eaaac10..088f076 100644 (file)
@@ -205,6 +205,21 @@ public abstract class ExpandableView extends FrameLayout {
     }
 
     /**
+     * Perform a remove animation on this view.
+     *
+     * @param translationDirection The direction value from [-1 ... 1] indicating in which the
+     *                             animation should be performed. A value of -1 means that The
+     *                             remove animation should be performed upwards,
+     *                             such that the  child appears to be going away to the top. 1
+     *                             Should mean the opposite.
+     * @param onFinishedRunnable A runnable which should be run when the animation is finished.
+     */
+    public abstract void performRemoveAnimation(float translationDirection,
+            Runnable onFinishedRunnable);
+
+    public abstract void performAddAnimation(long delay);
+
+    /**
      * A listener notifying when {@link #getActualHeight} changes.
      */
     public interface OnHeightChangedListener {
index 3c080fe..1c2ca91 100644 (file)
@@ -34,7 +34,6 @@ public class NotificationBackgroundView extends View {
 
     public NotificationBackgroundView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        setWillNotDraw(false);
     }
 
     @Override
index 8ae503a..a2f8991 100644 (file)
@@ -184,7 +184,7 @@ public class SpeedBumpView extends ExpandableView implements View.OnClickListene
     }
 
     public void performVisibilityAnimation(boolean nowVisible) {
-        animateDivider(nowVisible);
+        animateDivider(nowVisible, null /* onFinishedRunnable */);
 
         // Animate explanation Text
         if (mIsExpanded) {
@@ -192,7 +192,14 @@ public class SpeedBumpView extends ExpandableView implements View.OnClickListene
         }
     }
 
-    public void animateDivider(boolean nowVisible) {
+    /**
+     * Animate the divider to a new visibility.
+     *
+     * @param nowVisible should it now be visible
+     * @param onFinishedRunnable A runnable which should be run when the animation is
+     *        finished.
+     */
+    public void animateDivider(boolean nowVisible, Runnable onFinishedRunnable) {
         if (nowVisible != mDividerVisible) {
             // Animate dividers
             float endValue = nowVisible ? 1.0f : 0.0f;
@@ -204,7 +211,8 @@ public class SpeedBumpView extends ExpandableView implements View.OnClickListene
                     .scaleX(endValue)
                     .scaleY(endValue)
                     .translationX(endTranslationXLeft)
-                    .setInterpolator(mFastOutSlowInInterpolator);
+                    .setInterpolator(mFastOutSlowInInterpolator)
+                    .withEndAction(onFinishedRunnable);
             mLineRight.animate()
                     .alpha(endValue)
                     .withLayer()
@@ -216,6 +224,10 @@ public class SpeedBumpView extends ExpandableView implements View.OnClickListene
             // Animate dots
             mDots.performVisibilityAnimation(nowVisible);
             mDividerVisible = nowVisible;
+        } else {
+            if (onFinishedRunnable != null) {
+                onFinishedRunnable.run();
+            }
         }
     }
 
@@ -250,6 +262,16 @@ public class SpeedBumpView extends ExpandableView implements View.OnClickListene
         }
     }
 
+    @Override
+    public void performRemoveAnimation(float translationDirection, Runnable onFinishedRunnable) {
+        performVisibilityAnimation(false);
+    }
+
+    @Override
+    public void performAddAnimation(long delay) {
+        performVisibilityAnimation(true);
+    }
+
     private void resetExplanationText() {
         mExplanationText.setTranslationY(0);
         mExplanationText.setVisibility(INVISIBLE);
index fe7546d..142241c 100644 (file)
@@ -214,7 +214,7 @@ public class NotificationPanelView extends PanelView implements
                         mClockAnimationTarget = -1;
                     }
                 });
-                StackStateAnimator.startInstantly(mClockAnimator);
+                mClockAnimator.start();
                 return true;
             }
         });
index 54af2c5..3fed860 100644 (file)
@@ -2766,6 +2766,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode,
         updateStackScrollerState();
         updatePublicMode();
         updateRowStates();
+        updateSpeedBump();
         checkBarModes();
         updateNotificationIcons();
         updateCarrierLabelVisibility(false);
index 41914ed..5e2d06b 100644 (file)
@@ -28,6 +28,7 @@ public class AnimationFilter {
     boolean animateScale;
     boolean animateHeight;
     boolean animateDimmed;
+    boolean hasDelays;
 
     public AnimationFilter animateAlpha() {
         animateAlpha = true;
@@ -39,6 +40,11 @@ public class AnimationFilter {
         return this;
     }
 
+    public AnimationFilter hasDelays() {
+        hasDelays = true;
+        return this;
+    }
+
     public AnimationFilter animateZ() {
         animateZ = true;
         return this;
@@ -79,6 +85,7 @@ public class AnimationFilter {
         animateScale |= filter.animateScale;
         animateHeight |= filter.animateHeight;
         animateDimmed |= filter.animateDimmed;
+        hasDelays |= filter.hasDelays;
     }
 
     private void reset() {
@@ -88,5 +95,6 @@ public class AnimationFilter {
         animateScale = false;
         animateHeight = false;
         animateDimmed = false;
+        hasDelays = false;
     }
 }
index 90f3d17..079b184 100644 (file)
@@ -103,6 +103,7 @@ public class NotificationStackScrollLayout extends ViewGroup
     private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>();
     private ArrayList<View> mSnappedBackChildren = new ArrayList<View>();
     private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>();
+    private ArrayList<View> mChildrenChangingPositions = new ArrayList<View>();
     private ArrayList<AnimationEvent> mAnimationEvents
             = new ArrayList<AnimationEvent>();
     private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
@@ -969,9 +970,24 @@ public class NotificationStackScrollLayout extends ViewGroup
     }
 
     /**
+     * @return The first child which has visibility unequal to GONE which is currently below the
+     *         given translationY or equal to it.
+     */
+    private View getFirstChildBelowTranlsationY(float translationY) {
+        int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    /**
      * @return the last child which has visibility unequal to GONE
      */
-    private View getLastChildNotGone() {
+    public View getLastChildNotGone() {
         int childCount = getChildCount();
         for (int i = childCount - 1; i >= 0; i--) {
             View child = getChildAt(i);
@@ -1094,23 +1110,41 @@ public class NotificationStackScrollLayout extends ViewGroup
     @Override
     protected void onViewRemoved(View child) {
         super.onViewRemoved(child);
+        mStackScrollAlgorithm.notifyChildrenChanged(this);
+        if (mChildrenChangingPositions.contains(child)) {
+            // This is only a position change, don't do anything special
+            return;
+        }
         ((ExpandableView) child).setOnHeightChangedListener(null);
         mCurrentStackScrollState.removeViewStateForView(child);
-        mStackScrollAlgorithm.notifyChildrenChanged(this);
         updateScrollStateForRemovedChild(child);
-        generateRemoveAnimation(child);
+        boolean animationGenerated = generateRemoveAnimation(child);
+        if (animationGenerated && !mSwipedOutViews.contains(child)) {
+            // Add this view to an overlay in order to ensure that it will still be temporary
+            // drawn when removed
+            getOverlay().add(child);
+        }
     }
 
-    private void generateRemoveAnimation(View child) {
+    /**
+     * Generate a remove animation for a child view.
+     *
+     * @param child The view to generate the remove animation for.
+     * @return Whether an animation was generated.
+     */
+    private boolean generateRemoveAnimation(View child) {
         if (mIsExpanded && mAnimationsEnabled) {
             if (!mChildrenToAddAnimated.contains(child)) {
                 // Generate Animations
                 mChildrenToRemoveAnimated.add(child);
                 mNeedsAnimation = true;
+                return true;
             } else {
                 mChildrenToAddAnimated.remove(child);
+                return false;
             }
         }
+        return false;
     }
 
     /**
@@ -1155,9 +1189,7 @@ public class NotificationStackScrollLayout extends ViewGroup
         super.onViewAdded(child);
         mStackScrollAlgorithm.notifyChildrenChanged(this);
         ((ExpandableView) child).setOnHeightChangedListener(this);
-        if (child.getVisibility() != View.GONE) {
-            generateAddAnimation(child);
-        }
+        generateAddAnimation(child);
     }
 
     public void setAnimationsEnabled(boolean animationsEnabled) {
@@ -1168,10 +1200,13 @@ public class NotificationStackScrollLayout extends ViewGroup
         return mNeedsAnimation
                 && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty());
     }
-
+    /**
+     * Generate an animation for an added child view.
+     *
+     * @param child The view to be added.
+     */
     public void generateAddAnimation(View child) {
-        if (mIsExpanded && mAnimationsEnabled) {
-
+        if (mIsExpanded && mAnimationsEnabled && !mChildrenChangingPositions.contains(child)) {
             // Generate Animations
             mChildrenToAddAnimated.add(child);
             mNeedsAnimation = true;
@@ -1186,9 +1221,10 @@ public class NotificationStackScrollLayout extends ViewGroup
      */
     public void changeViewPosition(View child, int newIndex) {
         if (child != null && child.getParent() == this) {
+            mChildrenChangingPositions.add(child);
             removeView(child);
             addView(child, newIndex);
-            // TODO: handle events
+            mNeedsAnimation = true;
         }
     }
 
@@ -1197,16 +1233,18 @@ public class NotificationStackScrollLayout extends ViewGroup
             generateChildHierarchyEvents();
             mNeedsAnimation = false;
         }
-        if (!mAnimationEvents.isEmpty()) {
+        if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) {
             mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState);
+            mAnimationEvents.clear();
         } else {
             applyCurrentState();
         }
     }
 
     private void generateChildHierarchyEvents() {
-        generateChildAdditionEvents();
         generateChildRemovalEvents();
+        generateChildAdditionEvents();
+        generatePositionChangeEvents();
         generateSnapBackEvents();
         generateDragEvents();
         generateTopPaddingEvent();
@@ -1237,12 +1275,24 @@ public class NotificationStackScrollLayout extends ViewGroup
             int animationType = childWasSwipedOut
                     ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
                     : AnimationEvent.ANIMATION_TYPE_REMOVE;
-            mAnimationEvents.add(new AnimationEvent(child, animationType));
+            AnimationEvent event = new AnimationEvent(child, animationType);
+
+            // we need to know the view after this one
+            event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY());
+            mAnimationEvents.add(event);
         }
         mSwipedOutViews.clear();
         mChildrenToRemoveAnimated.clear();
     }
 
+    private void generatePositionChangeEvents() {
+        for (View child : mChildrenChangingPositions) {
+            mAnimationEvents.add(new AnimationEvent(child,
+                    AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
+        }
+        mChildrenChangingPositions.clear();
+    }
+
     private void generateChildAdditionEvents() {
         for (View child : mChildrenToAddAnimated) {
             mAnimationEvents.add(new AnimationEvent(child,
@@ -1467,7 +1517,6 @@ public class NotificationStackScrollLayout extends ViewGroup
 
     public void onChildAnimationFinished() {
         requestChildrenUpdate();
-        mAnimationEvents.clear();
     }
 
     /**
@@ -1513,9 +1562,9 @@ public class NotificationStackScrollLayout extends ViewGroup
     }
 
     private void updateSpeedBump(boolean visible) {
-        int newVisibility = visible ? VISIBLE : GONE;
-        int oldVisibility = mSpeedBumpView.getVisibility();
-        if (newVisibility != oldVisibility) {
+        boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE;
+        if (visible != notGoneBefore) {
+            int newVisibility = visible ? VISIBLE : GONE;
             mSpeedBumpView.setVisibility(newVisibility);
             if (visible) {
                 mSpeedBumpView.collapse();
@@ -1551,21 +1600,24 @@ public class NotificationStackScrollLayout extends ViewGroup
                         .animateAlpha()
                         .animateHeight()
                         .animateY()
-                        .animateZ(),
+                        .animateZ()
+                        .hasDelays(),
 
                 // ANIMATION_TYPE_REMOVE
                 new AnimationFilter()
                         .animateAlpha()
                         .animateHeight()
                         .animateY()
-                        .animateZ(),
+                        .animateZ()
+                        .hasDelays(),
 
                 // ANIMATION_TYPE_REMOVE_SWIPED_OUT
                 new AnimationFilter()
                         .animateAlpha()
                         .animateHeight()
                         .animateY()
-                        .animateZ(),
+                        .animateZ()
+                        .hasDelays(),
 
                 // ANIMATION_TYPE_TOP_PADDING_CHANGED
                 new AnimationFilter()
@@ -1593,16 +1645,23 @@ public class NotificationStackScrollLayout extends ViewGroup
                 new AnimationFilter()
                         .animateY()
                         .animateScale()
-                        .animateDimmed()
+                        .animateDimmed(),
+
+                // ANIMATION_TYPE_CHANGE_POSITION
+                new AnimationFilter()
+                        .animateAlpha()
+                        .animateHeight()
+                        .animateY()
+                        .animateZ()
         };
 
         static int[] LENGTHS = new int[] {
 
                 // ANIMATION_TYPE_ADD
-                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+                StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
 
                 // ANIMATION_TYPE_REMOVE
-                StackStateAnimator.ANIMATION_DURATION_STANDARD,
+                StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
 
                 // ANIMATION_TYPE_REMOVE_SWIPED_OUT
                 StackStateAnimator.ANIMATION_DURATION_STANDARD,
@@ -1621,22 +1680,27 @@ public class NotificationStackScrollLayout extends ViewGroup
 
                 // ANIMATION_TYPE_DIMMED
                 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
+
+                // ANIMATION_TYPE_CHANGE_POSITION
+                StackStateAnimator.ANIMATION_DURATION_STANDARD,
         };
 
-        static int ANIMATION_TYPE_ADD = 0;
-        static int ANIMATION_TYPE_REMOVE = 1;
-        static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
-        static int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
-        static int ANIMATION_TYPE_START_DRAG = 4;
-        static int ANIMATION_TYPE_SNAP_BACK = 5;
-        static int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
-        static int ANIMATION_TYPE_DIMMED = 7;
+        static final int ANIMATION_TYPE_ADD = 0;
+        static final int ANIMATION_TYPE_REMOVE = 1;
+        static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
+        static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
+        static final int ANIMATION_TYPE_START_DRAG = 4;
+        static final int ANIMATION_TYPE_SNAP_BACK = 5;
+        static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
+        static final int ANIMATION_TYPE_DIMMED = 7;
+        static final int ANIMATION_TYPE_CHANGE_POSITION = 8;
 
         final long eventStartTime;
         final View changingView;
         final int animationType;
         final AnimationFilter filter;
         final long length;
+        View viewAfterChangingView;
 
         AnimationEvent(View view, int type) {
             eventStartTime = AnimationUtils.currentAnimationTimeMillis();
index d572ea5..bd2541a 100644 (file)
@@ -208,6 +208,8 @@ public class StackScrollAlgorithm {
         for (int i = 0; i < childCount; i++) {
             ExpandableView v = (ExpandableView) hostView.getChildAt(i);
             if (v.getVisibility() != View.GONE) {
+                StackScrollState.ViewState viewState = resultState.getViewStateForView(v);
+                viewState.notGoneIndex = state.visibleChildren.size();
                 state.visibleChildren.add(v);
             }
         }
index 011411c..ae2acab 100644 (file)
@@ -39,14 +39,10 @@ public class StackScrollState {
     private final ViewGroup mHostView;
     private Map<ExpandableView, ViewState> mStateMap;
     private final Rect mClipRect = new Rect();
-    private int mBackgroundRoundedRectCornerRadius;
-    private final Outline mChildOutline = new Outline();
 
     public StackScrollState(ViewGroup hostView) {
         mHostView = hostView;
         mStateMap = new HashMap<ExpandableView, ViewState>();
-        mBackgroundRoundedRectCornerRadius = hostView.getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.notification_quantum_rounded_rect_radius);
     }
 
     public ViewGroup getHostView() {
@@ -66,6 +62,7 @@ public class StackScrollState {
             viewState.height = child.getIntrinsicHeight();
             viewState.gone = child.getVisibility() == View.GONE;
             viewState.alpha = 1;
+            viewState.notGoneIndex = -1;
         }
     }
 
@@ -190,7 +187,7 @@ public class StackScrollState {
         if (nextChild != null) {
             ViewState nextState = getViewStateForView(nextChild);
             boolean startIsAboveNext = nextState.yTranslation > speedBumpStart;
-            speedBump.animateDivider(startIsAboveNext);
+            speedBump.animateDivider(startIsAboveNext, null /* onFinishedRunnable */);
 
             // handle expanded case
             if (speedBump.isExpanded()) {
@@ -272,6 +269,11 @@ public class StackScrollState {
         boolean dimmed;
 
         /**
+         * The index of the view, only accounting for views not equal to GONE
+         */
+        int notGoneIndex;
+
+        /**
          * The location this view is currently rendered at.
          *
          * <p>See <code>LOCATION_</code> flags.</p>
index a9dcdd6..045a99d 100644 (file)
@@ -39,7 +39,11 @@ import java.util.Stack;
 public class StackStateAnimator {
 
     public static final int ANIMATION_DURATION_STANDARD = 360;
+    public static final int ANIMATION_DURATION_APPEAR_DISAPPEAR = 464;
     public static final int ANIMATION_DURATION_DIMMED_ACTIVATED = 220;
+    public static final int ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING = 80;
+    public static final int ANIMATION_DELAY_PER_ELEMENT_MANUAL = 32;
+    private static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2;
 
     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;
@@ -62,10 +66,9 @@ public class StackStateAnimator {
 
     private final Interpolator mFastOutSlowInInterpolator;
     public NotificationStackScrollLayout mHostLayout;
-    private ArrayList<NotificationStackScrollLayout.AnimationEvent> mHandledEvents =
-            new ArrayList<>();
     private ArrayList<NotificationStackScrollLayout.AnimationEvent> mNewEvents =
             new ArrayList<>();
+    private ArrayList<View> mNewAddChildren = new ArrayList<>();
     private Set<Animator> mAnimatorSet = new HashSet<Animator>();
     private Stack<AnimatorListenerAdapter> mAnimationListenerPool
             = new Stack<AnimatorListenerAdapter>();
@@ -96,57 +99,130 @@ public class StackStateAnimator {
         mCurrentLength = NotificationStackScrollLayout.AnimationEvent.combineLength(mNewEvents);
         for (int i = 0; i < childCount; i++) {
             final ExpandableView child = (ExpandableView) mHostLayout.getChildAt(i);
+
             StackScrollState.ViewState viewState = finalState.getViewStateForView(child);
-            if (viewState == null) {
+            if (viewState == null || child.getVisibility() == View.GONE) {
                 continue;
             }
 
-            startAnimations(child, viewState);
-
             child.setClipBounds(null);
+            startAnimations(child, viewState, finalState);
         }
         if (!isRunning()) {
             // no child has preformed any animation, lets finish
             onAnimationFinished();
         }
+        mNewEvents.clear();
+        mNewAddChildren.clear();
     }
 
     /**
      * Start an animation to the given viewState
      */
-    private void startAnimations(final ExpandableView child, StackScrollState.ViewState viewState) {
+    private void startAnimations(final ExpandableView child, StackScrollState.ViewState viewState,
+            StackScrollState finalState) {
         int childVisibility = child.getVisibility();
         boolean wasVisible = childVisibility == View.VISIBLE;
         final float alpha = viewState.alpha;
         if (!wasVisible && alpha != 0 && !viewState.gone) {
             child.setVisibility(View.VISIBLE);
         }
+
+        boolean yTranslationChanging = child.getTranslationY() != viewState.yTranslation;
+        boolean zTranslationChanging = child.getTranslationZ() != viewState.zTranslation;
+        boolean scaleChanging = child.getScaleX() != viewState.scale;
+        boolean alphaChanging = alpha != child.getAlpha();
+        boolean heightChanging = viewState.height != child.getActualHeight();
+        boolean wasAdded = mNewAddChildren.contains(child);
+        boolean hasDelays = mAnimationFilter.hasDelays;
+        boolean isDelayRelevant = yTranslationChanging || zTranslationChanging || scaleChanging ||
+                alphaChanging || heightChanging;
+        long delay = 0;
+        if (hasDelays && isDelayRelevant || wasAdded) {
+            delay = calculateChildAnimationDelay(viewState, finalState);
+        }
+
         // start translationY animation
-        if (child.getTranslationY() != viewState.yTranslation) {
-            startYTranslationAnimation(child, viewState);
+        if (yTranslationChanging) {
+            startYTranslationAnimation(child, viewState, delay);
         }
+
         // start translationZ animation
-        if (child.getTranslationZ() != viewState.zTranslation) {
-            startZTranslationAnimation(child, viewState);
+        if (zTranslationChanging) {
+            startZTranslationAnimation(child, viewState, delay);
         }
+
         // start scale animation
-        if (child.getScaleX() != viewState.scale) {
+        if (scaleChanging) {
             startScaleAnimation(child, viewState);
         }
+
         // start alpha animation
-        if (alpha != child.getAlpha()) {
-            startAlphaAnimation(child, viewState);
+        if (alphaChanging) {
+            startAlphaAnimation(child, viewState, delay);
         }
+
         // start height animation
-        if (viewState.height != child.getActualHeight()) {
-            startHeightAnimation(child, viewState);
+        if (heightChanging) {
+            startHeightAnimation(child, viewState, delay);
         }
+
         // start dimmed animation
         child.setDimmed(viewState.dimmed, mAnimationFilter.animateDimmed);
+
+        if (wasAdded) {
+            child.performAddAnimation(delay);
+        }
+    }
+
+    private long calculateChildAnimationDelay(StackScrollState.ViewState viewState,
+            StackScrollState finalState) {
+        long minDelay = 0;
+        for (NotificationStackScrollLayout.AnimationEvent event : mNewEvents) {
+            long delayPerElement = ANIMATION_DELAY_PER_ELEMENT_INTERRUPTING;
+            switch (event.animationType) {
+                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_ADD: {
+                    int ownIndex = viewState.notGoneIndex;
+                    int changingIndex = finalState
+                            .getViewStateForView(event.changingView).notGoneIndex;
+                    int difference = Math.abs(ownIndex - changingIndex);
+                    difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
+                            difference - 1));
+                    long delay = (DELAY_EFFECT_MAX_INDEX_DIFFERENCE - difference) * delayPerElement;
+                    minDelay = Math.max(delay, minDelay);
+                    break;
+                }
+                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT:
+                    delayPerElement = ANIMATION_DELAY_PER_ELEMENT_MANUAL;
+                case NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE: {
+                    int ownIndex = viewState.notGoneIndex;
+                    boolean noNextView = event.viewAfterChangingView == null;
+                    View viewAfterChangingView = noNextView
+                            ? mHostLayout.getLastChildNotGone()
+                            : event.viewAfterChangingView;
+
+                    int nextIndex = finalState
+                            .getViewStateForView(viewAfterChangingView).notGoneIndex;
+                    if (ownIndex >= nextIndex) {
+                        // we only have the view afterwards
+                        ownIndex++;
+                    }
+                    int difference = Math.abs(ownIndex - nextIndex);
+                    difference = Math.max(0, Math.min(DELAY_EFFECT_MAX_INDEX_DIFFERENCE,
+                            difference - 1));
+                    long delay = difference * delayPerElement;
+                    minDelay = Math.max(delay, minDelay);
+                    break;
+                }
+                default:
+                    break;
+            }
+        }
+        return minDelay;
     }
 
     private void startHeightAnimation(final ExpandableView child,
-            StackScrollState.ViewState viewState) {
+            StackScrollState.ViewState viewState, long delay) {
         Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT);
         Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT);
         int newEndValue = viewState.height;
@@ -185,6 +261,9 @@ public class StackStateAnimator {
         animator.setInterpolator(mFastOutSlowInInterpolator);
         long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
         animator.setDuration(newDuration);
+        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
+            animator.setStartDelay(delay);
+        }
         animator.addListener(getGlobalAnimationFinishedListener());
         // remove the tag when the animation is finished
         animator.addListener(new AnimatorListenerAdapter() {
@@ -195,14 +274,14 @@ public class StackStateAnimator {
                 child.setTag(TAG_END_HEIGHT, null);
             }
         });
-        startInstantly(animator);
+        startAnimator(animator);
         child.setTag(TAG_ANIMATOR_HEIGHT, animator);
         child.setTag(TAG_START_HEIGHT, child.getActualHeight());
         child.setTag(TAG_END_HEIGHT, newEndValue);
     }
 
     private void startAlphaAnimation(final ExpandableView child,
-            final StackScrollState.ViewState viewState) {
+            final StackScrollState.ViewState viewState, long delay) {
         Float previousStartValue = getChildTag(child,TAG_START_ALPHA);
         Float previousEndValue = getChildTag(child,TAG_END_ALPHA);
         final float newEndValue = viewState.alpha;
@@ -264,6 +343,9 @@ public class StackStateAnimator {
         });
         long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
         animator.setDuration(newDuration);
+        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
+            animator.setStartDelay(delay);
+        }
         animator.addListener(getGlobalAnimationFinishedListener());
         // remove the tag when the animation is finished
         animator.addListener(new AnimatorListenerAdapter() {
@@ -272,14 +354,14 @@ public class StackStateAnimator {
 
             }
         });
-        startInstantly(animator);
+        startAnimator(animator);
         child.setTag(TAG_ANIMATOR_ALPHA, animator);
         child.setTag(TAG_START_ALPHA, child.getAlpha());
         child.setTag(TAG_END_ALPHA, newEndValue);
     }
 
     private void startZTranslationAnimation(final ExpandableView child,
-            final StackScrollState.ViewState viewState) {
+            final StackScrollState.ViewState viewState, long delay) {
         Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Z);
         Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Z);
         float newEndValue = viewState.zTranslation;
@@ -311,6 +393,9 @@ public class StackStateAnimator {
         animator.setInterpolator(mFastOutSlowInInterpolator);
         long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
         animator.setDuration(newDuration);
+        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
+            animator.setStartDelay(delay);
+        }
         animator.addListener(getGlobalAnimationFinishedListener());
         // remove the tag when the animation is finished
         animator.addListener(new AnimatorListenerAdapter() {
@@ -321,14 +406,14 @@ public class StackStateAnimator {
                 child.setTag(TAG_END_TRANSLATION_Z, null);
             }
         });
-        startInstantly(animator);
+        startAnimator(animator);
         child.setTag(TAG_ANIMATOR_TRANSLATION_Z, animator);
         child.setTag(TAG_START_TRANSLATION_Z, child.getTranslationZ());
         child.setTag(TAG_END_TRANSLATION_Z, newEndValue);
     }
 
     private void startYTranslationAnimation(final ExpandableView child,
-            StackScrollState.ViewState viewState) {
+            StackScrollState.ViewState viewState, long delay) {
         Float previousStartValue = getChildTag(child,TAG_START_TRANSLATION_Y);
         Float previousEndValue = getChildTag(child,TAG_END_TRANSLATION_Y);
         float newEndValue = viewState.yTranslation;
@@ -361,6 +446,9 @@ public class StackStateAnimator {
         animator.setInterpolator(mFastOutSlowInInterpolator);
         long newDuration = cancelAnimatorAndGetNewDuration(previousAnimator);
         animator.setDuration(newDuration);
+        if (delay > 0 && (previousAnimator == null || !previousAnimator.isRunning())) {
+            animator.setStartDelay(delay);
+        }
         animator.addListener(getGlobalAnimationFinishedListener());
         // remove the tag when the animation is finished
         animator.addListener(new AnimatorListenerAdapter() {
@@ -371,7 +459,7 @@ public class StackStateAnimator {
                 child.setTag(TAG_END_TRANSLATION_Y, null);
             }
         });
-        startInstantly(animator);
+        startAnimator(animator);
         child.setTag(TAG_ANIMATOR_TRANSLATION_Y, animator);
         child.setTag(TAG_START_TRANSLATION_Y, child.getTranslationY());
         child.setTag(TAG_END_TRANSLATION_Y, newEndValue);
@@ -425,18 +513,15 @@ public class StackStateAnimator {
                 child.setTag(TAG_END_SCALE, null);
             }
         });
-        startInstantly(animator);
+        startAnimator(animator);
         child.setTag(TAG_ANIMATOR_SCALE, animator);
         child.setTag(TAG_START_SCALE, child.getScaleX());
         child.setTag(TAG_END_SCALE, newEndValue);
     }
 
-    /**
-     * Start an animator instantly instead of waiting on the next synchronization frame
-     */
-    public static void startInstantly(ValueAnimator animator) {
+    private void startAnimator(ValueAnimator animator) {
+        mAnimatorSet.add(animator);
         animator.start();
-        animator.setCurrentPlayTime(0);
     }
 
     /**
@@ -468,7 +553,6 @@ public class StackStateAnimator {
 
             @Override
             public void onAnimationStart(Animator animation) {
-                mAnimatorSet.add(animation);
                 mWasCancelled = false;
             }
         };
@@ -497,8 +581,6 @@ public class StackStateAnimator {
     }
 
     private void onAnimationFinished() {
-        mHandledEvents.clear();
-        mNewEvents.clear();
         mHostLayout.onChildAnimationFinished();
     }
 
@@ -511,27 +593,60 @@ public class StackStateAnimator {
     private void processAnimationEvents(
             ArrayList<NotificationStackScrollLayout.AnimationEvent> animationEvents,
             StackScrollState finalState) {
-        mNewEvents.clear();
         for (NotificationStackScrollLayout.AnimationEvent event : animationEvents) {
-            View changingView = event.changingView;
-            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);
+            final ExpandableView changingView = (ExpandableView) event.changingView;
+            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;
+                }
+                if (changingView.getVisibility() == View.GONE) {
+                    // The view was set to gone but the state never removed
+                    finalState.removeViewStateForView(changingView);
+                    continue;
                 }
-                mHandledEvents.add(event);
-                mNewEvents.add(event);
+                changingView.setAlpha(viewState.alpha);
+                changingView.setTranslationY(viewState.yTranslation);
+                changingView.setTranslationZ(viewState.zTranslation);
+                changingView.setActualHeight(viewState.height, false);
+                mNewAddChildren.add(changingView);
+
+            } else if (event.animationType ==
+                    NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE) {
+                if (changingView.getVisibility() == View.GONE) {
+                    continue;
+                }
+
+                // Find the amount to translate up. This is needed in order to understand the
+                // direction of the remove animation (either downwards or upwards)
+                StackScrollState.ViewState viewState = finalState
+                        .getViewStateForView(event.viewAfterChangingView);
+                int actualHeight = changingView.getActualHeight();
+                // upwards by default
+                float translationDirection = -1.0f;
+                if (viewState != null) {
+                    // there was a view after this one, Approximate the distance the next child
+                    // travelled
+                    translationDirection = ((viewState.yTranslation
+                            - (changingView.getTranslationY() + actualHeight / 2.0f)) * 2 /
+                            actualHeight);
+                    translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f);
+
+                }
+                changingView.performRemoveAnimation(translationDirection, new Runnable() {
+                    @Override
+                    public void run() {
+                        // remove the temporary overlay
+                        mHostLayout.getOverlay().remove(changingView);
+                    }
+                });
             }
+            mNewEvents.add(event);
         }
     }