From 2f2022afa1eb85018368398bd150e9575fc099c9 Mon Sep 17 00:00:00 2001 From: Chet Haase Date: Tue, 11 Oct 2011 06:41:59 -0700 Subject: [PATCH] Make notification panel delete-all animation smoother Making the notfication delete-all animation smoother by carefully choreographing the various parts of it. The problem with the previous animation was that there was simply too much going on at the same time, causing things like layout and recreating display-lists in the middle of animations that became choppy as a result. This approach swipes all items off quickly, then scrolls the shade up to the top, making both sets of animations smoother as a result. Fixes #5431207: Notification delete-all should be smoother Change-Id: Iefe8ab5d661e05adcd10379dab85227d17904450 --- core/java/android/view/ViewRootImpl.java | 36 +++++++++ .../src/com/android/systemui/SwipeHelper.java | 23 +++--- .../statusbar/phone/PhoneStatusBar.java | 73 ++++++++++++------- .../policy/NotificationRowLayout.java | 52 ++++++++++--- 4 files changed, 135 insertions(+), 49 deletions(-) diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 7eae7395e9194..081e267f601cf 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -108,6 +108,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, private static final boolean DEBUG_TRACKBALL = false || LOCAL_LOGV; private static final boolean DEBUG_IMF = false || LOCAL_LOGV; private static final boolean DEBUG_CONFIGURATION = false || LOCAL_LOGV; + private static final boolean DEBUG_FPS = false; private static final boolean WATCH_POINTER = false; /** @@ -274,6 +275,11 @@ public final class ViewRootImpl extends Handler implements ViewParent, private Thread mRenderProfiler; private volatile boolean mRenderProfilingEnabled; + // Variables to track frames per second, enabled via DEBUG_FPS flag + private long mFpsStartTime = -1; + private long mFpsPrevTime = -1; + private int mFpsNumFrames; + /** * see {@link #playSoundEffect(int)} */ @@ -1766,12 +1772,42 @@ public final class ViewRootImpl extends Handler implements ViewParent, } } + /** + * Called from draw() when DEBUG_FPS is enabled + */ + private void trackFPS() { + // Tracks frames per second drawn. First value in a series of draws may be bogus + // because it down not account for the intervening idle time + long nowTime = System.currentTimeMillis(); + if (mFpsStartTime < 0) { + mFpsStartTime = mFpsPrevTime = nowTime; + mFpsNumFrames = 0; + } else { + ++mFpsNumFrames; + String thisHash = Integer.toHexString(System.identityHashCode(this)); + long frameTime = nowTime - mFpsPrevTime; + long totalTime = nowTime - mFpsStartTime; + Log.v(TAG, "0x" + thisHash + "\tFrame time:\t" + frameTime); + mFpsPrevTime = nowTime; + if (totalTime > 1000) { + float fps = (float) mFpsNumFrames * 1000 / totalTime; + Log.v(TAG, "0x" + thisHash + "\tFPS:\t" + fps); + mFpsStartTime = nowTime; + mFpsNumFrames = 0; + } + } + } + private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; if (surface == null || !surface.isValid()) { return; } + if (DEBUG_FPS) { + trackFPS(); + } + if (!sFirstDrawComplete) { synchronized (sFirstDrawHandlers) { sFirstDrawComplete = true; diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index 07281d46a7be9..14ce266c63dbc 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -17,6 +17,7 @@ package com.android.systemui; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.Animator.AnimatorListener; import android.animation.ValueAnimator; @@ -40,6 +41,8 @@ public class SwipeHelper { public static final int X = 0; public static final int Y = 1; + private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); + private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms @@ -199,6 +202,10 @@ public class SwipeHelper { return mDragging; } + /** + * @param view The view to be dismissed + * @param velocity The desired pixels/second speed at which the view should move + */ public void dismissChild(final View view, float velocity) { final View animView = mCallback.getChildContentView(view); final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); @@ -221,22 +228,14 @@ public class SwipeHelper { duration = DEFAULT_ESCAPE_ANIMATION_DURATION; } + animView.setLayerType(View.LAYER_TYPE_HARDWARE, null); ObjectAnimator anim = createTranslationAnimation(animView, newPos); - anim.setInterpolator(new LinearInterpolator()); + anim.setInterpolator(sLinearInterpolator); anim.setDuration(duration); - anim.addListener(new AnimatorListener() { - public void onAnimationStart(Animator animation) { - } - - public void onAnimationRepeat(Animator animation) { - } - + anim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { mCallback.onChildDismissed(view); - } - - public void onAnimationCancel(Animator animation) { - mCallback.onChildDismissed(view); + animView.setLayerType(View.LAYER_TYPE_NONE, null); } }); anim.addUpdateListener(new AnimatorUpdateListener() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 4e9b41130503c..b7245522c8b4d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -50,7 +50,6 @@ import android.view.IWindowManager; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.Surface; import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; @@ -213,6 +212,8 @@ public class PhoneStatusBar extends StatusBar { boolean mAnimatingReveal = false; int mViewDelta; int[] mAbsPos = new int[2]; + Runnable mPostCollapseCleanup = null; + // for disabling the status bar int mDisabled = 0; @@ -1238,6 +1239,10 @@ public class PhoneStatusBar extends StatusBar { return; } mExpanded = false; + if (mPostCollapseCleanup != null) { + mPostCollapseCleanup.run(); + mPostCollapseCleanup = null; + } } void doAnimation() { @@ -2066,49 +2071,67 @@ public class PhoneStatusBar extends StatusBar { } public void onClick(View v) { synchronized (mNotificationData) { - // let's also queue up 400ms worth of animated dismissals - final int N = mini(5, mPile.getChildCount()); + // animate-swipe all dismissable notifications, then animate the shade closed + int numChildren = mPile.getChildCount(); - final ArrayList snapshot = new ArrayList(N); - for (int i=0; i snapshot = new ArrayList(numChildren); + for (int i=0; i scrollTop && + child.getTop() < scrollBottom) { + snapshot.add(child); + } } + final int N = snapshot.size(); new Thread(new Runnable() { @Override public void run() { - final int ROW_DELAY = 100; + // Decrease the delay for every row we animate to give the sense of + // accelerating the swipes + final int ROW_DELAY_DECREMENT = 10; + int currentDelay = 140; + int totalDelay = 0; - mHandler.postDelayed(new Runnable() { - public void run() { - animateCollapse(false, 0f); - } - }, (N-1) * ROW_DELAY); + // Set the shade-animating state to avoid doing other work during + // all of these animations. In particular, avoid layout and + // redrawing when collapsing the shade. + mPile.setViewRemoval(false); - mHandler.postDelayed(new Runnable() { + mPostCollapseCleanup = new Runnable() { public void run() { try { + mPile.setViewRemoval(true); mBarService.onClearAllNotifications(); - } catch (RemoteException ex) { } + } catch (Exception ex) { } } - }, N * ROW_DELAY + 500); - - mPile.setAnimateBounds(false); // temporarily disable some re-layouts + }; + View sampleView = snapshot.get(0); + int width = sampleView.getWidth(); + final int velocity = (int)(width * 8); // 1000/8 = 125 ms duration for (View v : snapshot) { final View _v = v; - mHandler.post(new Runnable() { + mHandler.postDelayed(new Runnable() { @Override public void run() { - mPile.dismissRowAnimated(_v, (int)(ROW_DELAY*0.25f)); + mPile.dismissRowAnimated(_v, velocity); } - }); - try { - Thread.sleep(ROW_DELAY); - } catch (InterruptedException ex) { } + }, totalDelay); + currentDelay = Math.max(50, currentDelay - ROW_DELAY_DECREMENT); + totalDelay += currentDelay; } - - mPile.setAnimateBounds(true); // reenable layout animation + // Delay the collapse animation until after all swipe animations have + // finished. Provide some buffer because there may be some extra delay + // before actually starting each swipe animation. Ideally, we'd + // synchronize the end of those animations with the start of the collaps + // exactly. + mHandler.postDelayed(new Runnable() { + public void run() { + animateCollapse(false); + } + }, totalDelay + 225); } }).start(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java index a7342dccccb17..3649f75e37323 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.policy; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; @@ -29,7 +28,6 @@ import android.util.AttributeSet; import android.util.Log; import android.util.Slog; import android.view.MotionEvent; -import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; @@ -59,6 +57,10 @@ public class NotificationRowLayout extends ViewGroup implements SwipeHelper.Call private SwipeHelper mSwipeHelper; + // Flag set during notification removal animation to avoid causing too much work until + // animation is done + boolean mRemoveViews = true; + public NotificationRowLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } @@ -117,7 +119,7 @@ public class NotificationRowLayout extends ViewGroup implements SwipeHelper.Call public void onChildDismissed(View v) { final View veto = v.findViewById(R.id.veto); - if (veto != null && veto.getVisibility() != View.GONE) { + if (veto != null && veto.getVisibility() != View.GONE && mRemoveViews) { veto.performClick(); } } @@ -170,7 +172,6 @@ public class NotificationRowLayout extends ViewGroup implements SwipeHelper.Call final View childF = child; if (mAnimateBounds) { - child.setPivotY(0); final ObjectAnimator alphaFade = ObjectAnimator.ofFloat(child, "alpha", 0f, 1f); alphaFade.setDuration(APPEAR_ANIM_LEN); alphaFade.addListener(new AnimatorListenerAdapter() { @@ -189,6 +190,16 @@ public class NotificationRowLayout extends ViewGroup implements SwipeHelper.Call } } + /** + * Sets a flag to tell us whether to actually remove views. Removal is delayed by setting this + * to false during some animations to smooth out performance. Callers should restore the + * flag to true after the animation is done, and then they should make sure that the views + * get removed properly. + */ + public void setViewRemoval(boolean removeViews) { + mRemoveViews = removeViews; + } + public void dismissRowAnimated(View child) { dismissRowAnimated(child, 0); } @@ -199,16 +210,34 @@ public class NotificationRowLayout extends ViewGroup implements SwipeHelper.Call @Override public void removeView(View child) { - final View childF = child; + if (!mRemoveViews) { + // This flag is cleared during an animation that removes all notifications. There + // should be a call to remove all notifications when the animation is done, at which + // time the view will be removed. + return; + } if (mAnimateBounds) { if (mAppearingViews.containsKey(child)) { mAppearingViews.remove(child); } - child.setPivotY(0); - final ObjectAnimator alphaFade = ObjectAnimator.ofFloat(child, "alpha", 0f); - alphaFade.setDuration(DISAPPEAR_ANIM_LEN); - alphaFade.addListener(new AnimatorListenerAdapter() { + // Don't fade it out if it already has a low alpha value, but run a non-visual + // animation which is used by onLayout() to animate shrinking the gap that it left + // in the list + ValueAnimator anim; + float currentAlpha = child.getAlpha(); + if (currentAlpha > .1) { + anim = ObjectAnimator.ofFloat(child, "alpha", currentAlpha, 0); + } else { + if (currentAlpha > 0) { + // Just make it go away - no need to render it anymore + child.setAlpha(0); + } + anim = ValueAnimator.ofFloat(0, 1); + } + anim.setDuration(DISAPPEAR_ANIM_LEN); + final View childF = child; + anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (DEBUG) Slog.d(TAG, "actually removing child: " + childF); @@ -218,9 +247,8 @@ public class NotificationRowLayout extends ViewGroup implements SwipeHelper.Call } }); - alphaFade.start(); - - mDisappearingViews.put(child, alphaFade); + anim.start(); + mDisappearingViews.put(child, anim); requestLayout(); // start the container animation } else {