Make All Apps<-->Workspace transition smoother

- grouping workspace/all apps anims into one
animator set, prevents blip when one of them ends
early and its end anim handler does lots of work
mid-animation
- updating pivots for all apps zoom if layout
changes
- avoid running unnecessary animations on
workspace pages
- prevent unnecessary reloading of pages in
All Apps

Change-Id: I53a75f7c4c7d254057e2f8f4fd17711e8862256d
diff --git a/src/com/android/launcher2/AppsCustomizeTabHost.java b/src/com/android/launcher2/AppsCustomizeTabHost.java
index 2963240..023946b 100644
--- a/src/com/android/launcher2/AppsCustomizeTabHost.java
+++ b/src/com/android/launcher2/AppsCustomizeTabHost.java
@@ -54,7 +54,6 @@
 
     private boolean mInTransition;
     private boolean mResetAfterTransition;
-    private Animator mLauncherTransition;
 
     public AppsCustomizeTabHost(Context context, AttributeSet attrs) {
         super(context, attrs);
@@ -341,29 +340,17 @@
         }
     }
 
-    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        super.onLayout(changed, left, top, right, bottom);
-        if (mLauncherTransition != null) {
-            enableAndBuildHardwareLayer();
-            mLauncherTransition.start();
-            mLauncherTransition = null;
-        }
+    @Override
+    public View getContent() {
+        return mContent;
     }
 
     /* LauncherTransitionable overrides */
     @Override
-    public boolean onLauncherTransitionStart(Launcher l, Animator animation, boolean toWorkspace) {
+    public void onLauncherTransitionStart(Launcher l, Animator animation, boolean toWorkspace) {
         mInTransition = true;
-        boolean delayLauncherTransitionUntilLayout = false;
         boolean animated = (animation != null);
-        mLauncherTransition = null;
 
-        // if the content wasn't visible before, delay the launcher animation until after a call
-        // to layout -- this prevents a blip
-        if (animated && mContent.getVisibility() == GONE) {
-            mLauncherTransition = animation;
-            delayLauncherTransitionUntilLayout = true;
-        }
         mContent.setVisibility(VISIBLE);
 
         if (!toWorkspace) {
@@ -371,7 +358,7 @@
             // transition to prevent slowing down the animation)
             mAppsCustomizePane.loadAssociatedPages(mAppsCustomizePane.getCurrentPage(), true);
         }
-        if (animated && !delayLauncherTransitionUntilLayout) {
+        if (animated) {
             enableAndBuildHardwareLayer();
         }
 
@@ -382,7 +369,6 @@
             mAppsCustomizePane.reset();
             mResetAfterTransition = false;
         }
-        return delayLauncherTransitionUntilLayout;
     }
 
     @Override
@@ -406,7 +392,7 @@
         }
     }
 
-    public void onResume() {
+    public void onWindowVisible() {
         if (getVisibility() == VISIBLE) {
             mContent.setVisibility(VISIBLE);
             // We unload the widget previews when the UI is hidden, so need to reload pages
diff --git a/src/com/android/launcher2/Launcher.java b/src/com/android/launcher2/Launcher.java
index 15da501..5abfa28 100644
--- a/src/com/android/launcher2/Launcher.java
+++ b/src/com/android/launcher2/Launcher.java
@@ -28,7 +28,6 @@
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.SearchManager;
-import android.app.StatusBarManager;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProviderInfo;
@@ -54,7 +53,6 @@
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.AsyncTask;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
@@ -80,6 +78,7 @@
 import android.view.View.OnLongClickListener;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.AccelerateDecelerateInterpolator;
@@ -568,28 +567,6 @@
         // When we resume Launcher, a different Activity might be responsible for the app
         // market intent, so refresh the icon
         updateAppMarketIcon();
-        mAppsCustomizeTabHost.onResume();
-        if (!mWorkspaceLoading) {
-            final ViewTreeObserver observer = mWorkspace.getViewTreeObserver();
-            final Workspace workspace = mWorkspace;
-            // We want to let Launcher draw itself at least once before we force it to build
-            // layers on all the workspace pages, so that transitioning to Launcher from other
-            // apps is nice and speedy. Usually the first call to preDraw doesn't correspond to
-            // a true draw so we wait until the second preDraw call to be safe
-            observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
-                boolean mFirstTime = true;
-                public boolean onPreDraw() {
-                    if (mFirstTime) {
-                        mFirstTime = false;
-                    } else {
-                        workspace.post(mBuildLayersRunnable);
-                        observer.removeOnPreDrawListener(this);
-                    }
-                    return true;
-                }
-            });
-        }
-        clearTypedText();
     }
 
     @Override
@@ -1067,6 +1044,33 @@
     public void onWindowVisibilityChanged(int visibility) {
         mVisible = visibility == View.VISIBLE;
         updateRunning();
+        // The following code used to be in onResume, but it turns out onResume is called when
+        // you're in All Apps and click home to go to the workspace. onWindowVisibilityChanged
+        // is a more appropriate event to handle
+        if (mVisible) {
+            mAppsCustomizeTabHost.onWindowVisible();
+            if (!mWorkspaceLoading) {
+                final ViewTreeObserver observer = mWorkspace.getViewTreeObserver();
+                final Workspace workspace = mWorkspace;
+                // We want to let Launcher draw itself at least once before we force it to build
+                // layers on all the workspace pages, so that transitioning to Launcher from other
+                // apps is nice and speedy. Usually the first call to preDraw doesn't correspond to
+                // a true draw so we wait until the second preDraw call to be safe
+                observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+                    boolean mFirstTime = true;
+                    public boolean onPreDraw() {
+                        if (mFirstTime) {
+                            mFirstTime = false;
+                        } else {
+                            //workspace.post(mBuildLayersRunnable);
+                            observer.removeOnPreDrawListener(this);
+                        }
+                        return true;
+                    }
+                });
+            }
+            clearTypedText();
+        }
     }
 
     private void sendAdvanceMessage(long delay) {
@@ -2218,7 +2222,8 @@
         setPivotsForZoom(toView, scale);
 
         // Shrink workspaces away if going to AppsCustomize from workspace
-        mWorkspace.changeState(Workspace.State.SMALL, animated);
+        Animator workspaceAnim =
+                mWorkspace.getChangeStateAnimation(Workspace.State.SMALL, animated);
 
         if (animated) {
             final ValueAnimator scaleAnim = ValueAnimator.ofFloat(0f, 1f).setDuration(duration);
@@ -2236,14 +2241,17 @@
             alphaAnim.setInterpolator(new DecelerateInterpolator(1.5f));
             alphaAnim.addUpdateListener(new LauncherAnimatorUpdateListener() {
                 public void onAnimationUpdate(float a, float b) {
-                    // don't need to invalidate because we do so above
                     toView.setAlpha(a * 0f + b * 1f);
                 }
             });
-            alphaAnim.setStartDelay(startDelay);
-            alphaAnim.start();
 
-            scaleAnim.addListener(new AnimatorListenerAdapter() {
+            // toView should appear right at the end of the workspace shrink
+            // animation
+            mStateAnimation = new AnimatorSet();
+            mStateAnimation.play(alphaAnim).after(startDelay);
+            mStateAnimation.play(scaleAnim).after(startDelay);
+
+            mStateAnimation.addListener(new AnimatorListenerAdapter() {
                 boolean animationCancelled = false;
 
                 @Override
@@ -2283,19 +2291,43 @@
                 }
             });
 
-            // toView should appear right at the end of the workspace shrink animation
-            mStateAnimation = new AnimatorSet();
-            mStateAnimation.play(scaleAnim).after(startDelay);
+            if (workspaceAnim != null) {
+                mStateAnimation.play(workspaceAnim);
+            }
 
             boolean delayAnim = false;
-            if (toView instanceof LauncherTransitionable) {
-                LauncherTransitionable lt = (LauncherTransitionable) toView;
-                delayAnim = lt.onLauncherTransitionStart(instance, mStateAnimation, false);
+            LauncherTransitionable lt = (LauncherTransitionable) toView;
+            final ViewTreeObserver observer;
+
+            lt.onLauncherTransitionStart(instance, mStateAnimation, false);
+
+            // If any of the objects being animated haven't been measured/laid out
+            // yet, delay the animation until we get a layout pass
+            if ((lt.getContent().getMeasuredWidth() == 0) ||
+                    (mWorkspace.getMeasuredWidth() == 0) ||
+                    (toView.getMeasuredWidth() == 0)) {
+                observer = mWorkspace.getViewTreeObserver();
+                delayAnim = true;
+            } else {
+                observer = null;
             }
-            // if the anim is delayed, the LauncherTransitionable is responsible for starting it
-            if (!delayAnim) {
-                // TODO: q-- what if this anim is cancelled before being started? or started after
-                // being cancelled?
+
+            if (delayAnim) {
+                final OnGlobalLayoutListener delayedStart = new OnGlobalLayoutListener() {
+                    public void onGlobalLayout() {
+                        mWorkspace.post(new Runnable() {
+                            public void run() {
+                                // Need to update pivots for zoom if layout changed
+                                setPivotsForZoom(toView, scale);
+                                mStateAnimation.start();
+                            }
+                        });
+                        observer.removeGlobalOnLayoutListener(this);
+                    }
+                };
+                observer.addOnGlobalLayoutListener(delayedStart);
+            } else {
+                setPivotsForZoom(toView, scale);
                 mStateAnimation.start();
             }
         } else {
@@ -2324,7 +2356,8 @@
      * This is the opposite of showAppsCustomizeHelper.
      * @param animated If true, the transition will be animated.
      */
-    private void hideAppsCustomizeHelper(boolean animated, final boolean springLoaded) {
+    private void hideAppsCustomizeHelper(
+            State toState, boolean animated, final boolean springLoaded) {
         if (mStateAnimation != null) {
             mStateAnimation.cancel();
             mStateAnimation = null;
@@ -2336,6 +2369,16 @@
         final float scaleFactor = (float)
                 res.getInteger(R.integer.config_appsCustomizeZoomScaleFactor);
         final View fromView = mAppsCustomizeTabHost;
+        Animator workspaceAnim = null;
+
+        if (toState == State.WORKSPACE) {
+            int stagger = res.getInteger(R.integer.config_appsCustomizeWorkspaceAnimationStagger);
+            workspaceAnim = mWorkspace.getChangeStateAnimation(
+                    Workspace.State.NORMAL, animated, stagger);
+        } else if (toState == State.APPS_CUSTOMIZE_SPRING_LOADED) {
+            workspaceAnim = mWorkspace.getChangeStateAnimation(
+                    Workspace.State.SPRING_LOADED, animated);
+        }
 
         setPivotsForZoom(fromView, scaleFactor);
         updateWallpaperVisibility(true);
@@ -2379,6 +2422,9 @@
 
             mStateAnimation = new AnimatorSet();
             mStateAnimation.playTogether(scaleAnim, alphaAnim);
+            if (workspaceAnim != null) {
+                mStateAnimation.play(workspaceAnim);
+            }
             mStateAnimation.start();
         } else {
             fromView.setVisibility(View.GONE);
@@ -2399,13 +2445,9 @@
     }
 
     void showWorkspace(boolean animated) {
-        Resources res = getResources();
-        int stagger = res.getInteger(R.integer.config_appsCustomizeWorkspaceAnimationStagger);
-
-        mWorkspace.changeState(Workspace.State.NORMAL, animated, stagger);
         if (mState != State.WORKSPACE) {
             mWorkspace.setVisibility(View.VISIBLE);
-            hideAppsCustomizeHelper(animated, false);
+            hideAppsCustomizeHelper(State.WORKSPACE, animated, false);
 
             // Show the search bar and hotseat
             mSearchDropTargetBar.showSearchBar(animated);
@@ -2454,8 +2496,7 @@
 
     void enterSpringLoadedDragMode() {
         if (mState == State.APPS_CUSTOMIZE) {
-            mWorkspace.changeState(Workspace.State.SPRING_LOADED);
-            hideAppsCustomizeHelper(true, true);
+            hideAppsCustomizeHelper(State.APPS_CUSTOMIZE_SPRING_LOADED, true, true);
             hideDockDivider();
             mState = State.APPS_CUSTOMIZE_SPRING_LOADED;
         }
@@ -3327,7 +3368,7 @@
 }
 
 interface LauncherTransitionable {
-    // return true if the callee will take care of start the animation by itself
-    boolean onLauncherTransitionStart(Launcher l, Animator animation, boolean toWorkspace);
+    View getContent();
+    void onLauncherTransitionStart(Launcher l, Animator animation, boolean toWorkspace);
     void onLauncherTransitionEnd(Launcher l, Animator animation, boolean toWorkspace);
 }
diff --git a/src/com/android/launcher2/PagedView.java b/src/com/android/launcher2/PagedView.java
index 2db03da..975686e 100644
--- a/src/com/android/launcher2/PagedView.java
+++ b/src/com/android/launcher2/PagedView.java
@@ -1603,9 +1603,7 @@
                 // First, clear any pages that should no longer be loaded
                 for (int i = 0; i < count; ++i) {
                     Page layout = (Page) getPageAt(i);
-                    if ((immediateAndOnly && i != page) ||
-                            (i < lowerPageBound) ||
-                            (i > upperPageBound)) {
+                    if ((i < lowerPageBound) || (i > upperPageBound)) {
                         if (layout.getPageChildCount() > 0) {
                             layout.removeAllViewsOnPage();
                         }
@@ -1617,7 +1615,6 @@
                     if ((i != page) && immediateAndOnly) {
                         continue;
                     }
-                    Page layout = (Page) getPageAt(i);
                     if (lowerPageBound <= i && i <= upperPageBound) {
                         if (mDirtyPageContent.get(i)) {
                             syncPageItems(i, (i == page) && immediateAndOnly);
diff --git a/src/com/android/launcher2/PagedViewGridLayout.java b/src/com/android/launcher2/PagedViewGridLayout.java
index b1b6215..90bfe88 100644
--- a/src/com/android/launcher2/PagedViewGridLayout.java
+++ b/src/com/android/launcher2/PagedViewGridLayout.java
@@ -109,6 +109,7 @@
     @Override
     public void removeAllViewsOnPage() {
         removeAllViews();
+        mOnLayoutListener = null;
         destroyHardwareLayer();
     }
 
diff --git a/src/com/android/launcher2/Workspace.java b/src/com/android/launcher2/Workspace.java
index fa3dfcb..83408ba 100644
--- a/src/com/android/launcher2/Workspace.java
+++ b/src/com/android/launcher2/Workspace.java
@@ -159,10 +159,7 @@
     enum State { NORMAL, SPRING_LOADED, SMALL };
     private State mState = State.NORMAL;
     private boolean mIsSwitchingState = false;
-    private boolean mSwitchStateAfterFirstLayout = false;
-    private State mStateAfterFirstLayout;
 
-    private AnimatorSet mAnimator;
     private AnimatorListener mChangeStateAnimationListener;
 
     boolean mAnimatingViewIntoPlace = false;
@@ -405,7 +402,6 @@
             public void onAnimationEnd(Animator animation) {
                 mIsSwitchingState = false;
                 mWallpaperOffset.setOverrideHorizontalCatchupConstant(false);
-                mAnimator = null;
                 updateChildrenLayersEnabled();
             }
         };
@@ -1249,19 +1245,6 @@
             mUpdateWallpaperOffsetImmediately = true;
         }
         super.onLayout(changed, left, top, right, bottom);
-
-        // if shrinkToBottom() is called on initialization, it has to be deferred
-        // until after the first call to onLayout so that it has the correct width
-        if (mSwitchStateAfterFirstLayout) {
-            mSwitchStateAfterFirstLayout = false;
-            // shrink can trigger a synchronous onLayout call, so we
-            // post this to avoid a stack overflow / tangled onLayout calls
-            post(new Runnable() {
-                public void run() {
-                    changeState(mStateAfterFirstLayout, false);
-                }
-            });
-        }
     }
 
     @Override
@@ -1565,32 +1548,19 @@
         mNewRotationYs = new float[childCount];
     }
 
-    public void changeState(State shrinkState) {
-        changeState(shrinkState, true);
+    Animator getChangeStateAnimation(final State state, boolean animated) {
+        return getChangeStateAnimation(state, animated, 0);
     }
 
-    void changeState(final State state, boolean animated) {
-        changeState(state, animated, 0);
-    }
-
-    void changeState(final State state, boolean animated, int delay) {
+    Animator getChangeStateAnimation(final State state, boolean animated, int delay) {
         if (mState == state) {
-            return;
-        }
-        if (mFirstLayout) {
-            // (mFirstLayout == "first layout has not happened yet")
-            // cancel any pending shrinks that were set earlier
-            mSwitchStateAfterFirstLayout = false;
-            mStateAfterFirstLayout = state;
-            return;
+            return null;
         }
 
         // Initialize animation arrays for the first time if necessary
         initAnimationArrays();
 
-        // Cancel any running transition animations
-        if (mAnimator != null) mAnimator.cancel();
-        mAnimator = new AnimatorSet();
+        AnimatorSet anim = animated ? new AnimatorSet() : null;
 
         // Stop any scrolling, move to the current page right away
         setCurrentPage((mNextPage != INVALID_PAGE) ? mNextPage : mCurrentPage);
@@ -1717,6 +1687,21 @@
                     }
                 }
             });
+            for (int i = 0; i < getChildCount(); i++) {
+                invalidate();
+                if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) {
+                    final CellLayout cl = (CellLayout) getChildAt(i);
+                    cl.fastInvalidate();
+                    cl.setFastTranslationX(mNewTranslationXs[i]);
+                    cl.setFastTranslationY(mNewTranslationYs[i]);
+                    cl.setFastScaleX(mNewScaleXs[i]);
+                    cl.setFastScaleY(mNewScaleYs[i]);
+                    cl.setFastBackgroundAlpha(mNewBackgroundAlphas[i]);
+                    cl.setBackgroundAlphaMultiplier(mNewBackgroundAlphaMultipliers[i]);
+                    cl.setFastAlpha(mNewAlphas[i]);
+                }
+            }
+
             animWithInterpolator.addUpdateListener(new LauncherAnimatorUpdateListener() {
                 public void onAnimationUpdate(float a, float b) {
                     mTransitionProgress = b;
@@ -1726,17 +1711,21 @@
                     }
                     invalidate();
                     for (int i = 0; i < getChildCount(); i++) {
-                        final CellLayout cl = (CellLayout) getChildAt(i);
-                        cl.fastInvalidate();
-                        cl.setFastTranslationX(a * mOldTranslationXs[i] + b * mNewTranslationXs[i]);
-                        cl.setFastTranslationY(a * mOldTranslationYs[i] + b * mNewTranslationYs[i]);
-                        cl.setFastScaleX(a * mOldScaleXs[i] + b * mNewScaleXs[i]);
-                        cl.setFastScaleY(a * mOldScaleYs[i] + b * mNewScaleYs[i]);
-                        cl.setFastBackgroundAlpha(
-                                a * mOldBackgroundAlphas[i] + b * mNewBackgroundAlphas[i]);
-                        cl.setBackgroundAlphaMultiplier(a * mOldBackgroundAlphaMultipliers[i] +
-                                b * mNewBackgroundAlphaMultipliers[i]);
-                        cl.setFastAlpha(a * mOldAlphas[i] + b * mNewAlphas[i]);
+                        if (mOldAlphas[i] != 0 || mNewAlphas[i] != 0) {
+                            final CellLayout cl = (CellLayout) getChildAt(i);
+                            cl.fastInvalidate();
+                            cl.setFastTranslationX(
+                                    a * mOldTranslationXs[i] + b * mNewTranslationXs[i]);
+                            cl.setFastTranslationY(
+                                    a * mOldTranslationYs[i] + b * mNewTranslationYs[i]);
+                            cl.setFastScaleX(a * mOldScaleXs[i] + b * mNewScaleXs[i]);
+                            cl.setFastScaleY(a * mOldScaleYs[i] + b * mNewScaleYs[i]);
+                            cl.setFastBackgroundAlpha(
+                                    a * mOldBackgroundAlphas[i] + b * mNewBackgroundAlphas[i]);
+                            cl.setBackgroundAlphaMultiplier(a * mOldBackgroundAlphaMultipliers[i] +
+                                    b * mNewBackgroundAlphaMultipliers[i]);
+                            cl.setFastAlpha(a * mOldAlphas[i] + b * mNewAlphas[i]);
+                        }
                     }
                     syncChildrenLayersEnabledOnVisiblePages();
                 }
@@ -1752,18 +1741,20 @@
                         return;
                     }
                     for (int i = 0; i < getChildCount(); i++) {
-                        final CellLayout cl = (CellLayout) getChildAt(i);
-                        cl.setFastRotationY(a * mOldRotationYs[i] + b * mNewRotationYs[i]);
+                        if (mOldAlphas[i] != 0 || mNewAlphas[i] != 0 ||
+                                mOldRotationYs[i] != 0 || mNewRotationYs[i] != 0) {
+                            final CellLayout cl = (CellLayout) getChildAt(i);
+                            cl.setFastRotationY(a * mOldRotationYs[i] + b * mNewRotationYs[i]);
+                        }
                     }
                 }
             });
 
-            mAnimator.playTogether(animWithInterpolator, rotationAnim);
-            mAnimator.setStartDelay(delay);
+            anim.playTogether(animWithInterpolator, rotationAnim);
+            anim.setStartDelay(delay);
             // If we call this when we're not animated, onAnimationEnd is never called on
             // the listener; make sure we only use the listener when we're actually animating
-            mAnimator.addListener(mChangeStateAnimationListener);
-            mAnimator.start();
+            anim.addListener(mChangeStateAnimationListener);
         }
 
         if (stateIsSpringLoaded) {
@@ -1777,6 +1768,7 @@
             animateBackgroundGradient(0f, true);
         }
         syncChildrenLayersEnabledOnVisiblePages();
+        return anim;
     }
 
     /**