Volume Motion: Initial show and expand transition.

Also re-enable the content collapse layout transition animations,
supported by ensuring the dialog window is tall enough for long
enough to complete the transition, avoiding clipping.

Bug: 21335976
Change-Id: Ibc4cbb1e882c1e11c4406463752afa177fb1e6d7
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index 0ed1e2a..7617ed4 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -18,7 +18,7 @@
     android:id="@+id/volume_dialog"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_marginBottom="4dp"
+    android:layout_marginBottom="@dimen/volume_dialog_margin_bottom"
     android:layout_marginLeft="@dimen/notification_side_padding"
     android:layout_marginRight="@dimen/notification_side_padding"
     android:background="@drawable/volume_dialog_background"
diff --git a/packages/SystemUI/res/layout/volume_dialog_row.xml b/packages/SystemUI/res/layout/volume_dialog_row.xml
index c6aa588..1a6d34e 100644
--- a/packages/SystemUI/res/layout/volume_dialog_row.xml
+++ b/packages/SystemUI/res/layout/volume_dialog_row.xml
@@ -17,6 +17,7 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:clipChildren="false"
+    android:id="@+id/volume_dialog_row"
     android:paddingEnd="8dp"
     android:paddingStart="8dp" >
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 869b03a..005077f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -576,6 +576,9 @@
     <!-- Standard image button size for volume dialog buttons -->
     <dimen name="volume_button_size">48dp</dimen>
 
+    <!-- Volume dialog root view bottom margin, at rest -->
+    <dimen name="volume_dialog_margin_bottom">4dp</dimen>
+
     <!-- Padding between icon and text for managed profile toast -->
     <dimen name="managed_profile_toast_padding">4dp</dimen>
 
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 67d3312..1889862 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -291,11 +291,6 @@
         <item name="android:textColor">#ffb0b3c5</item>
     </style>
 
-    <style name="VolumeDialogAnimations">
-        <item name="android:windowEnterAnimation">@android:anim/fade_in</item>
-        <item name="android:windowExitAnimation">@android:anim/fade_out</item>
-    </style>
-
     <style name="VolumeButtons" parent="@android:style/Widget.Material.Button.Borderless">
         <item name="android:background">@drawable/btn_borderless_rect</item>
     </style>
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java
index 065523f..0ab0392 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialog.java
@@ -111,6 +111,7 @@
     private final Accessibility mAccessibility = new Accessibility();
     private final ColorStateList mActiveSliderTint;
     private final ColorStateList mInactiveSliderTint;
+    private final VolumeDialogMotion mMotion;
 
     private boolean mShowing;
     private boolean mExpanded;
@@ -120,9 +121,12 @@
     private boolean mSilentMode = VolumePrefs.DEFAULT_ENABLE_SILENT_MODE;
     private State mState;
     private int mExpandButtonRes;
-    private boolean mExpanding;
+    private boolean mExpandButtonAnimationRunning;
     private SafetyWarningDialog mSafetyWarning;
     private Callback mCallback;
+    private boolean mPendingStateChanged;
+    private boolean mPendingRecheckAll;
+    private long mCollapseTime;
 
     public VolumeDialog(Context context, int windowType, VolumeDialogController controller,
             ZenModeController zenModeController, Callback callback) {
@@ -151,7 +155,6 @@
         lp.format = PixelFormat.TRANSLUCENT;
         lp.setTitle(VolumeDialog.class.getSimpleName());
         lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
-        lp.windowAnimations = R.style.VolumeDialogAnimations;
         lp.y = res.getDimensionPixelSize(R.dimen.volume_offset_top);
         lp.gravity = Gravity.TOP;
         window.setAttributes(lp);
@@ -168,9 +171,22 @@
         updateExpandButtonH();
         mLayoutTransition = new LayoutTransition();
         mLayoutTransition.setDuration(new ValueAnimator().getDuration() / 2);
-        mLayoutTransition.disableTransitionType(LayoutTransition.DISAPPEARING);
-        mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
         mDialogContentView.setLayoutTransition(mLayoutTransition);
+        mMotion = new VolumeDialogMotion(mDialog, mDialogView, mDialogContentView, mExpandButton,
+                new VolumeDialogMotion.Callback() {
+            @Override
+            public void onAnimatingChanged(boolean animating) {
+                if (animating) return;
+                if (mPendingStateChanged) {
+                    mHandler.sendEmptyMessage(H.STATE_CHANGED);
+                    mPendingStateChanged = false;
+                }
+                if (mPendingRecheckAll) {
+                    mHandler.sendEmptyMessage(H.RECHECK_ALL);
+                    mPendingRecheckAll = false;
+                }
+            }
+        });
 
         addRow(AudioManager.STREAM_RING,
                 R.drawable.ic_volume_ringer, R.drawable.ic_volume_ringer_mute, true);
@@ -242,6 +258,7 @@
         final VolumeRow row = initRow(stream, iconRes, iconMuteRes, important);
         if (!mRows.isEmpty()) {
             final View v = new View(mContext);
+            v.setId(android.R.id.background);
             final int h = mContext.getResources()
                     .getDimensionPixelSize(R.dimen.volume_slider_interspacing);
             final LinearLayout.LayoutParams lp =
@@ -253,10 +270,11 @@
             @Override
             public void onLayoutChange(View v, int left, int top, int right, int bottom,
                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
-                if (D.BUG) Log.d(TAG, "onLayoutChange"
+                final boolean moved = oldLeft != left || oldTop != top;
+                if (D.BUG) Log.d(TAG, "onLayoutChange moved=" + moved
                         + " old=" + new Rect(oldLeft, oldTop, oldRight, oldBottom).toShortString()
                         + " new=" + new Rect(left,top,right,bottom).toShortString());
-                if (oldLeft != left || oldTop != top) {
+                if (moved) {
                     for (int i = 0; i < mDialogContentView.getChildCount(); i++) {
                         final View c = mDialogContentView.getChildAt(i);
                         if (!c.isShown()) continue;
@@ -302,18 +320,21 @@
         if (D.BUG) Log.d(TAG, "repositionExpandAnim x=" + x + " y=" + y);
         mExpandButton.setTranslationX(x);
         mExpandButton.setTranslationY(y);
+        mExpandButton.setTag((Integer) y);
     }
 
     public void dump(PrintWriter writer) {
         writer.println(VolumeDialog.class.getSimpleName() + " state:");
         writer.print("  mShowing: "); writer.println(mShowing);
         writer.print("  mExpanded: "); writer.println(mExpanded);
-        writer.print("  mExpanding: "); writer.println(mExpanding);
+        writer.print("  mExpandButtonAnimationRunning: ");
+        writer.println(mExpandButtonAnimationRunning);
         writer.print("  mActiveStream: "); writer.println(mActiveStream);
         writer.print("  mDynamic: "); writer.println(mDynamic);
         writer.print("  mShowHeaders: "); writer.println(mShowHeaders);
         writer.print("  mAutomute: "); writer.println(mAutomute);
         writer.print("  mSilentMode: "); writer.println(mSilentMode);
+        writer.print("  mCollapseTime: "); writer.println(mCollapseTime);
         writer.print("  mAccessibility.mFeedbackEnabled: ");
         writer.println(mAccessibility.mFeedbackEnabled);
     }
@@ -412,12 +433,13 @@
     }
 
     private void showH(int reason) {
+        if (D.BUG) Log.d(TAG, "showH r=" + Events.DISMISS_REASONS[reason]);
         mHandler.removeMessages(H.SHOW);
         mHandler.removeMessages(H.DISMISS);
         rescheduleTimeoutH();
         if (mShowing) return;
         mShowing = true;
-        mDialog.show();
+        mMotion.startShow();
         Events.writeEvent(mContext, Events.EVENT_SHOW_DIALOG, reason, mKeyguard.isKeyguardLocked());
         mController.notifyVisible(true);
     }
@@ -434,7 +456,7 @@
     private int computeTimeoutH() {
         if (mAccessibility.mFeedbackEnabled) return 20000;
         if (mSafetyWarning != null) return 5000;
-        if (mExpanded || mExpanding) return 5000;
+        if (mExpanded || mExpandButtonAnimationRunning) return 5000;
         if (mActiveStream == AudioManager.STREAM_MUSIC) return 1500;
         return 3000;
     }
@@ -444,9 +466,13 @@
         mHandler.removeMessages(H.SHOW);
         if (!mShowing) return;
         mShowing = false;
-        mDialog.dismiss();
+        mMotion.startDismiss(new Runnable() {
+            @Override
+            public void run() {
+                setExpandedH(false);
+            }
+        });
         Events.writeEvent(mContext, Events.EVENT_DISMISS_DIALOG, reason);
-        setExpandedH(false);
         mController.notifyVisible(false);
         synchronized (mSafetyWarningLock) {
             if (mSafetyWarning != null) {
@@ -456,13 +482,40 @@
         }
     }
 
+    private void updateDialogBottomMarginH() {
+        final long diff = System.currentTimeMillis() - mCollapseTime;
+        final boolean collapsing = mCollapseTime != 0 && diff < getConservativeCollapseDuration();
+        final ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) mDialogView.getLayoutParams();
+        final int bottomMargin = collapsing ? mDialogContentView.getHeight() :
+                mContext.getResources().getDimensionPixelSize(R.dimen.volume_dialog_margin_bottom);
+        if (bottomMargin != mlp.bottomMargin) {
+            if (D.BUG) Log.d(TAG, "bottomMargin " + mlp.bottomMargin + " -> " + bottomMargin);
+            mlp.bottomMargin = bottomMargin;
+            mDialogView.setLayoutParams(mlp);
+        }
+    }
+
+    private long getConservativeCollapseDuration() {
+        return mExpandButtonAnimationDuration * 3;
+    }
+
+    private void prepareForCollapse() {
+        mHandler.removeMessages(H.UPDATE_BOTTOM_MARGIN);
+        mCollapseTime = System.currentTimeMillis();
+        updateDialogBottomMarginH();
+        mHandler.sendEmptyMessageDelayed(H.UPDATE_BOTTOM_MARGIN, getConservativeCollapseDuration());
+    }
+
     private void setExpandedH(boolean expanded) {
         if (mExpanded == expanded) return;
         mExpanded = expanded;
-        mExpanding = isAttached();
+        mExpandButtonAnimationRunning = isAttached();
         if (D.BUG) Log.d(TAG, "setExpandedH " + expanded);
+        if (!mExpanded && mExpandButtonAnimationRunning) {
+            prepareForCollapse();
+        }
         updateRowsH();
-        if (mExpanding) {
+        if (mExpandButtonAnimationRunning) {
             final Drawable d = mExpandButton.getDrawable();
             if (d instanceof AnimatedVectorDrawable) {
                 // workaround to reset drawable
@@ -473,7 +526,7 @@
                 mHandler.postDelayed(new Runnable() {
                     @Override
                     public void run() {
-                        mExpanding = false;
+                        mExpandButtonAnimationRunning = false;
                         updateExpandButtonH();
                         rescheduleTimeoutH();
                     }
@@ -484,8 +537,9 @@
     }
 
     private void updateExpandButtonH() {
-        mExpandButton.setClickable(!mExpanding);
-        if (mExpanding && isAttached()) return;
+        if (D.BUG) Log.d(TAG, "updateExpandButtonH");
+        mExpandButton.setClickable(!mExpandButtonAnimationRunning);
+        if (mExpandButtonAnimationRunning && isAttached()) return;
         final int res = mExpanded ? R.drawable.ic_volume_collapse_animation
                 : R.drawable.ic_volume_expand_animation;
         if (res == mExpandButtonRes) return;
@@ -502,6 +556,7 @@
     }
 
     private void updateRowsH() {
+        if (D.BUG) Log.d(TAG, "updateRowsH");
         final VolumeRow activeRow = getActiveRow();
         updateFooterH();
         updateExpandButtonH();
@@ -531,6 +586,7 @@
     }
 
     private void trimObsoleteH() {
+        if (D.BUG) Log.d(TAG, "trimObsoleteH");
         for (int i = mRows.size() -1; i >= 0; i--) {
             final VolumeRow row = mRows.get(i);
             if (row.ss == null || !row.ss.dynamic) continue;
@@ -543,7 +599,13 @@
     }
 
     private void onStateChangedH(State state) {
+        final boolean animating = mMotion.isAnimating();
+        if (D.BUG) Log.d(TAG, "onStateChangedH animating=" + animating);
         mState = state;
+        if (animating) {
+            mPendingStateChanged = true;
+            return;
+        }
         mDynamic.clear();
         // add any new dynamic rows
         for (int i = 0; i < state.states.size(); i++) {
@@ -568,11 +630,18 @@
     }
 
     private void updateFooterH() {
-        Util.setVisOrGone(mZenFooter, mState.zenMode != Global.ZEN_MODE_OFF);
+        if (D.BUG) Log.d(TAG, "updateFooterH");
+        final boolean wasVisible = mZenFooter.getVisibility() == View.VISIBLE;
+        final boolean visible = mState.zenMode != Global.ZEN_MODE_OFF;
+        if (wasVisible != visible && !visible) {
+            prepareForCollapse();
+        }
+        Util.setVisOrGone(mZenFooter, visible);
         mZenFooter.update();
     }
 
     private void updateVolumeRowH(VolumeRow row) {
+        if (D.BUG) Log.d(TAG, "updateVolumeRowH s=" + row.stream);
         if (mState == null) return;
         final StreamState ss = mState.states.get(row.stream);
         if (ss == null) return;
@@ -841,7 +910,7 @@
     private final OnClickListener mClickExpand = new OnClickListener() {
         @Override
         public void onClick(View v) {
-            if (mExpanding) return;
+            if (mExpandButtonAnimationRunning) return;
             final boolean newExpand = !mExpanded;
             Events.writeEvent(mContext, Events.EVENT_EXPAND, newExpand);
             setExpandedH(newExpand);
@@ -870,6 +939,8 @@
         private static final int RECHECK_ALL = 4;
         private static final int SET_STREAM_IMPORTANT = 5;
         private static final int RESCHEDULE_TIMEOUT = 6;
+        private static final int STATE_CHANGED = 7;
+        private static final int UPDATE_BOTTOM_MARGIN = 8;
 
         public H() {
             super(Looper.getMainLooper());
@@ -884,6 +955,8 @@
                 case RECHECK_ALL: recheckH(null); break;
                 case SET_STREAM_IMPORTANT: setStreamImportantH(msg.arg1, msg.arg2 != 0); break;
                 case RESCHEDULE_TIMEOUT: rescheduleTimeoutH(); break;
+                case STATE_CHANGED: onStateChangedH(mState); break;
+                case UPDATE_BOTTOM_MARGIN: updateDialogBottomMarginH(); break;
             }
         }
     }
@@ -902,6 +975,12 @@
         @Override
         protected void onStop() {
             super.onStop();
+            final boolean animating = mMotion.isAnimating();
+            if (D.BUG) Log.d(TAG, "onStop animating=" + animating);
+            if (animating) {
+                mPendingRecheckAll = true;
+                return;
+            }
             mHandler.sendEmptyMessage(H.RECHECK_ALL);
         }
 
@@ -978,11 +1057,13 @@
             mDialogView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
                 @Override
                 public void onViewDetachedFromWindow(View v) {
+                    if (D.BUG) Log.d(TAG, "onViewDetachedFromWindow");
                     // noop
                 }
 
                 @Override
                 public void onViewAttachedToWindow(View v) {
+                    if (D.BUG) Log.d(TAG, "onViewAttachedToWindow");
                     updateFeedbackEnabled();
                 }
             });
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogMotion.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogMotion.java
new file mode 100644
index 0000000..fdf1840
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogMotion.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.DialogInterface.OnShowListener;
+import android.os.Handler;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.PathInterpolator;
+
+public class VolumeDialogMotion {
+    private static final String TAG = Util.logTag(VolumeDialogMotion.class);
+
+    private static final float ANIMATION_SCALE = 1.0f;
+    private static final int PRE_DISMISS_DELAY = 50;
+    private static final int POST_SHOW_DELAY = 200;
+
+    private final Dialog mDialog;
+    private final View mDialogView;
+    private final ViewGroup mContents;  // volume rows + zen footer
+    private final View mChevron;
+    private final Handler mHandler = new Handler();
+    private final Callback mCallback;
+
+    private boolean mAnimating;  // show or dismiss animation is running
+    private boolean mShowing;  // show animation is running
+    private boolean mDismissing;  // dismiss animation is running
+    private ValueAnimator mChevronPositionAnimator;
+    private ValueAnimator mContentsPositionAnimator;
+
+    public VolumeDialogMotion(Dialog dialog, View dialogView, ViewGroup contents, View chevron,
+            Callback callback) {
+        mDialog = dialog;
+        mDialogView = dialogView;
+        mContents = contents;
+        mChevron = chevron;
+        mCallback = callback;
+        mDialog.setOnDismissListener(new OnDismissListener() {
+            @Override
+            public void onDismiss(DialogInterface dialog) {
+                if (D.BUG) Log.d(TAG, "mDialog.onDismiss");
+            }
+        });
+        mDialog.setOnShowListener(new OnShowListener() {
+            @Override
+            public void onShow(DialogInterface dialog) {
+                if (D.BUG) Log.d(TAG, "mDialog.onShow");
+                final int h = mDialogView.getHeight();
+                mDialogView.setTranslationY(-h);
+                mHandler.postDelayed(new Runnable() {
+                    @Override
+                    public void run() {
+                        startShowAnimation();
+                    }
+                }, POST_SHOW_DELAY);
+            }
+        });
+    }
+
+    public boolean isAnimating() {
+        return mAnimating;
+    }
+
+    private void setShowing(boolean showing) {
+        if (showing == mShowing) return;
+        mShowing = showing;
+        if (D.BUG) Log.d(TAG, "mShowing = " + mShowing);
+        updateAnimating();
+    }
+
+    private void setDismissing(boolean dismissing) {
+        if (dismissing == mDismissing) return;
+        mDismissing = dismissing;
+        if (D.BUG) Log.d(TAG, "mDismissing = " + mDismissing);
+        updateAnimating();
+    }
+
+    private void updateAnimating() {
+        final boolean animating = mShowing || mDismissing;
+        if (animating == mAnimating) return;
+        mAnimating = animating;
+        if (D.BUG) Log.d(TAG, "mAnimating = " + mAnimating);
+        if (mCallback != null) {
+            mCallback.onAnimatingChanged(mAnimating);
+        }
+    }
+
+    public void startShow() {
+        if (D.BUG) Log.d(TAG, "startShow");
+        if (mShowing) return;
+        setShowing(true);
+        if (mDismissing) {
+            mDialogView.animate().cancel();
+            setDismissing(false);
+            startShowAnimation();
+            return;
+        }
+        if (D.BUG) Log.d(TAG, "mDialog.show()");
+        mDialog.show();
+    }
+
+    private int chevronDistance() {
+        return mChevron.getHeight() / 6;
+    }
+
+    private void startShowAnimation() {
+        if (D.BUG) Log.d(TAG, "startShowAnimation");
+        mDialogView.animate()
+                .translationY(0)
+                .setDuration(scaledDuration(300))
+                .setInterpolator(new LogDecelerateInterpolator())
+                .setListener(null)
+                .setUpdateListener(new AnimatorUpdateListener() {
+                    @Override
+                    public void onAnimationUpdate(ValueAnimator animation) {
+                        if (mChevronPositionAnimator == null) return;
+                        // reposition chevron
+                        final float v = (Float) mChevronPositionAnimator.getAnimatedValue();
+                        final int posY = (Integer) mChevron.getTag();
+                        mChevron.setTranslationY(posY + v + -mDialogView.getTranslationY());
+                    }})
+                .start();
+
+        mContentsPositionAnimator = ValueAnimator.ofFloat(-chevronDistance(), 0)
+                .setDuration(scaledDuration(400));
+        mContentsPositionAnimator.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                if (mCancelled) return;
+                if (D.BUG) Log.d(TAG, "show.onAnimationEnd");
+                setShowing(false);
+            }
+            @Override
+            public void onAnimationCancel(Animator animation) {
+                if (D.BUG) Log.d(TAG, "show.onAnimationCancel");
+                mCancelled = true;
+            }
+        });
+        mContentsPositionAnimator.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float v = (Float) animation.getAnimatedValue();
+                mContents.setTranslationY(v + -mDialogView.getTranslationY());
+            }
+        });
+        mContentsPositionAnimator.setInterpolator(new LogDecelerateInterpolator());
+        mContentsPositionAnimator.start();
+
+        mContents.setAlpha(0);
+        mContents.animate()
+                .alpha(1)
+                .setDuration(scaledDuration(150))
+                .setInterpolator(new PathInterpolator(0f, 0f, .2f, 1f))
+                .start();
+
+        mChevronPositionAnimator = ValueAnimator.ofFloat(-chevronDistance(), 0)
+                .setDuration(scaledDuration(250));
+        mChevronPositionAnimator.setInterpolator(new PathInterpolator(.4f, 0f, .2f, 1f));
+        mChevronPositionAnimator.start();
+
+        mChevron.setAlpha(0);
+        mChevron.animate()
+                .alpha(1)
+                .setStartDelay(scaledDuration(50))
+                .setDuration(scaledDuration(150))
+                .setInterpolator(new PathInterpolator(.4f, 0f, 1f, 1f))
+                .start();
+    }
+
+    public void startDismiss(final Runnable onComplete) {
+        if (D.BUG) Log.d(TAG, "startDismiss");
+        if (mDismissing) return;
+        setDismissing(true);
+        if (mShowing) {
+            mDialogView.animate().cancel();
+            mContentsPositionAnimator.cancel();
+            mContents.animate().cancel();
+            mChevronPositionAnimator.cancel();
+            mChevron.animate().cancel();
+            setShowing(false);
+        }
+        mDialogView.animate()
+                .translationY(-mDialogView.getHeight())
+                .setDuration(scaledDuration(250))
+                .setInterpolator(new LogAccelerateInterpolator())
+                .setUpdateListener(new AnimatorUpdateListener() {
+                    @Override
+                    public void onAnimationUpdate(ValueAnimator animation) {
+                        mContents.setTranslationY(-mDialogView.getTranslationY());
+                        int posY = (Integer) mChevron.getTag();
+                        mChevron.setTranslationY(posY + -mDialogView.getTranslationY());
+                    }
+                })
+                .setListener(new AnimatorListenerAdapter() {
+                    private boolean mCancelled;
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        if (mCancelled) return;
+                        if (D.BUG) Log.d(TAG, "dismiss.onAnimationEnd");
+                        mHandler.postDelayed(new Runnable() {
+                            @Override
+                            public void run() {
+                                if (D.BUG) Log.d(TAG, "mDialog.dismiss()");
+                                mDialog.dismiss();
+                                onComplete.run();
+                                setDismissing(false);
+                            }
+                        }, PRE_DISMISS_DELAY);
+
+                    }
+                    @Override
+                    public void onAnimationCancel(Animator animation) {
+                        if (D.BUG) Log.d(TAG, "dismiss.onAnimationCancel");
+                        mCancelled = true;
+                    }
+                }).start();
+    }
+
+    private static int scaledDuration(int base) {
+        return (int) (base * ANIMATION_SCALE);
+    }
+
+    private static final class LogDecelerateInterpolator implements TimeInterpolator {
+        private final float mBase;
+        private final float mDrift;
+        private final float mTimeScale;
+        private final float mOutputScale;
+
+        private LogDecelerateInterpolator() {
+            this(400f, 1.4f, 0);
+        }
+
+        private LogDecelerateInterpolator(float base, float timeScale, float drift) {
+            mBase = base;
+            mDrift = drift;
+            mTimeScale = 1f / timeScale;
+
+            mOutputScale = 1f / computeLog(1f);
+        }
+
+        private float computeLog(float t) {
+            return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t);
+        }
+
+        @Override
+        public float getInterpolation(float t) {
+            return computeLog(t) * mOutputScale;
+        }
+    }
+
+    private static final class LogAccelerateInterpolator implements TimeInterpolator {
+        private final int mBase;
+        private final int mDrift;
+        private final float mLogScale;
+
+        private LogAccelerateInterpolator() {
+            this(100, 0);
+        }
+
+        private LogAccelerateInterpolator(int base, int drift) {
+            mBase = base;
+            mDrift = drift;
+            mLogScale = 1f / computeLog(1, mBase, mDrift);
+        }
+
+        private static float computeLog(float t, int base, int drift) {
+            return (float) -Math.pow(base, -t) + 1 + (drift * t);
+        }
+
+        @Override
+        public float getInterpolation(float t) {
+            return 1 - computeLog(1 - t, mBase, mDrift) * mLogScale;
+        }
+    }
+
+    public interface Callback {
+        void onAnimatingChanged(boolean animating);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java
index 3f6294d..af7ee08 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/ZenFooter.java
@@ -16,6 +16,7 @@
 package com.android.systemui.volume;
 
 import android.animation.LayoutTransition;
+import android.animation.ValueAnimator;
 import android.content.Context;
 import android.provider.Settings.Global;
 import android.service.notification.ZenModeConfig;
@@ -51,7 +52,9 @@
         super(context, attrs);
         mContext = context;
         mSpTexts = new SpTexts(mContext);
-        setLayoutTransition(new LayoutTransition());
+        final LayoutTransition layoutTransition = new LayoutTransition();
+        layoutTransition.setDuration(new ValueAnimator().getDuration() / 2);
+        setLayoutTransition(layoutTransition);
     }
 
     @Override