Moving all-apps state logic to an independent class
Unifying all the paths for state change to a single flow

Bug: 67678570
Change-Id: I0773c0f59ae1ef324c507bc1aae188d8c059dea4
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index b06081b..775dad2 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -117,6 +117,7 @@
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.states.AllAppsState;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -202,9 +203,7 @@
     // Type: SparseArray<Parcelable>
     private static final String RUNTIME_STATE_WIDGET_PANEL = "launcher.widget_panel";
 
-    static final String APPS_VIEW_SHOWN = "launcher.apps_view_shown";
-
-    @Thunk LauncherStateTransitionAnimation mStateTransitionAnimation;
+    private LauncherStateTransitionAnimation mStateTransitionAnimation;
 
     private boolean mIsSafeModeEnabled;
 
@@ -431,6 +430,10 @@
         recreate();
     }
 
+    public LauncherStateTransitionAnimation getStateTransition() {
+        return mStateTransitionAnimation;
+    }
+
     protected void overrideTheme(boolean isDark, boolean supportsDarkText) {
         if (isDark) {
             setTheme(R.style.LauncherThemeDark);
@@ -2441,8 +2444,7 @@
         boolean changed = !isInState(LauncherState.NORMAL);
         if (changed || mAllAppsController.isTransitioning()) {
             mWorkspace.setVisibility(View.VISIBLE);
-            mStateTransitionAnimation.startAnimationToWorkspace(
-                    LauncherState.NORMAL, animated, onCompleteRunnable);
+            mStateTransitionAnimation.goToState(LauncherState.NORMAL, animated, onCompleteRunnable);
 
             // Set focus to the AppsCustomize button
             if (mAllAppsButton != null) {
@@ -2483,8 +2485,7 @@
             };
         }
         mWorkspace.setVisibility(View.VISIBLE);
-        mStateTransitionAnimation.startAnimationToWorkspace(
-                LauncherState.OVERVIEW, animated, postAnimRunnable);
+        mStateTransitionAnimation.goToState(LauncherState.OVERVIEW, animated, postAnimRunnable);
 
         // If animated from long press, then don't allow any of the controller in the drag
         // layer to intercept any remaining touch.
@@ -2499,8 +2500,6 @@
     // TODO: calling method should use the return value so that when {@code false} is returned
     // the workspace transition doesn't fall into invalid state.
     public boolean showAppsView(boolean animated) {
-        markAppsViewShown();
-
         if (!(isInState(LauncherState.NORMAL) ||
                 (isInState(LauncherState.ALL_APPS) && mAllAppsController.isTransitioning()))) {
             return false;
@@ -2512,7 +2511,7 @@
             mExitSpringLoadedModeRunnable = null;
         }
 
-        mStateTransitionAnimation.startAnimationToAllApps(animated);
+        mStateTransitionAnimation.goToState(LauncherState.ALL_APPS, animated, null);
 
         // Change the state *after* we've called all the transition code
         AbstractFloatingView.closeAllOpenViews(this);
@@ -2529,10 +2528,7 @@
         if (isInState(LauncherState.SPRING_LOADED)) {
             return;
         }
-
-        mStateTransitionAnimation.startAnimationToWorkspace(
-                LauncherState.SPRING_LOADED, true /* animated */,
-                null /* onCompleteRunnable */);
+        mStateTransitionAnimation.goToState(LauncherState.SPRING_LOADED, true, null);
     }
 
     public void exitSpringLoadedDragMode(int delay) {
@@ -3307,15 +3303,9 @@
         return mRotationEnabled;
     }
 
-    private void markAppsViewShown() {
-        if (mSharedPrefs.getBoolean(APPS_VIEW_SHOWN, false)) {
-            return;
-        }
-        mSharedPrefs.edit().putBoolean(APPS_VIEW_SHOWN, true).apply();
-    }
-
     private boolean shouldShowDiscoveryBounce() {
-        return isInState(LauncherState.NORMAL) && !mSharedPrefs.getBoolean(APPS_VIEW_SHOWN, false)
+        return isInState(LauncherState.NORMAL)
+                && !mSharedPrefs.getBoolean(AllAppsState.APPS_VIEW_SHOWN, false)
                 && !UserManagerCompat.getInstance(this).isDemoUser();
     }
 
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 0ac27e5..9d01ed8 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -20,6 +20,9 @@
 
 import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
 
+import android.view.View;
+
+import com.android.launcher3.states.AllAppsState;
 import com.android.launcher3.states.OverviewState;
 import com.android.launcher3.states.SpringLoadedState;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -28,7 +31,7 @@
 
 
 /**
- * Various states for launcher
+ * Base state for various states used for the Launcher
  */
 public class LauncherState {
 
@@ -37,14 +40,14 @@
     protected static final int FLAG_HIDE_HOTSEAT = 1 << 2;
     protected static final int FLAG_DISABLE_ACCESSIBILITY = 1 << 3;
     protected static final int FLAG_DO_NOT_RESTORE = 1 << 4;
+    protected static final int FLAG_HAS_SPRING = 1 << 5;
 
     private static final LauncherState[] sAllStates = new LauncherState[4];
 
     public static final LauncherState NORMAL = new LauncherState(0, ContainerType.WORKSPACE,
-            0, FLAG_DO_NOT_RESTORE);
+            0, 1f, FLAG_DO_NOT_RESTORE);
 
-    public static final LauncherState ALL_APPS = new LauncherState(1, ContainerType.ALLAPPS,
-            ALL_APPS_TRANSITION_MS, FLAG_DISABLE_ACCESSIBILITY);
+    public static final LauncherState ALL_APPS = new AllAppsState(1);
 
     public static final LauncherState SPRING_LOADED = new SpringLoadedState(2);
 
@@ -73,12 +76,25 @@
      */
     public final int workspaceAccessibilityFlag;
 
-    // Properties related to state transition animation.
+    /**
+     * Properties related to state transition animation
+     *
+     * @see WorkspaceStateTransitionAnimation
+     */
     public final boolean hasScrim;
     public final boolean hideHotseat;
     public final int transitionDuration;
 
-    public LauncherState(int id, int containerType, int transitionDuration, int flags) {
+    /**
+     * Fraction shift in the vertical translation UI and related properties
+     *
+     * @see com.android.launcher3.allapps.AllAppsTransitionController
+     */
+    public final float verticalProgress;
+    public final boolean hasVerticalSpring;
+
+    public LauncherState(int id, int containerType, int transitionDuration, float verticalProgress,
+            int flags) {
         this.containerType = containerType;
         this.transitionDuration = transitionDuration;
 
@@ -90,6 +106,9 @@
                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO;
         this.doNotRestore = (flags & FLAG_DO_NOT_RESTORE) != 0;
 
+        this.verticalProgress = verticalProgress;
+        this.hasVerticalSpring = (flags & FLAG_HAS_SPRING) != 0;
+
         this.ordinal = id;
         sAllStates[id] = this;
     }
@@ -105,4 +124,8 @@
     public void onStateEnabled(Launcher launcher) { }
 
     public void onStateDisabled(Launcher launcher) { }
+
+    public View getFinalFocus(Launcher launcher) {
+        return launcher.getWorkspace();
+    }
 }
diff --git a/src/com/android/launcher3/LauncherStateTransitionAnimation.java b/src/com/android/launcher3/LauncherStateTransitionAnimation.java
index 0e6cead..eec8b31 100644
--- a/src/com/android/launcher3/LauncherStateTransitionAnimation.java
+++ b/src/com/android/launcher3/LauncherStateTransitionAnimation.java
@@ -19,14 +19,13 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.util.Log;
+import android.os.Handler;
+import android.os.Looper;
 import android.view.View;
 
-import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimationLayerSet;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.util.Thunk;
+import com.android.launcher3.anim.AnimationSuccessListener;
 
 /**
  * TODO: figure out what kind of tests we can write for this
@@ -74,223 +73,75 @@
     public static final String TAG = "LSTAnimation";
 
     private final AnimationConfig mConfig = new AnimationConfig();
-    @Thunk Launcher mLauncher;
-    @Thunk AnimatorSet mCurrentAnimation;
-    AllAppsTransitionController mAllAppsController;
+    private final Handler mUiHandler;
+    private final Launcher mLauncher;
+    private final AllAppsTransitionController mAllAppsController;
 
-    public LauncherStateTransitionAnimation(Launcher l, AllAppsTransitionController allAppsController) {
+    public LauncherStateTransitionAnimation(
+            Launcher l, AllAppsTransitionController allAppsController) {
+        mUiHandler = new Handler(Looper.getMainLooper());
         mLauncher = l;
         mAllAppsController = allAppsController;
     }
 
-    /**
-     * Starts an animation to the apps view.
-     */
-    public void startAnimationToAllApps(boolean animated) {
-        final AllAppsContainerView toView = mLauncher.getAppsView();
-
-        // If for some reason our views aren't initialized, don't animate
-        animated = animated && (toView != null);
-
-        final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
-
-        final AnimationLayerSet layerViews = new AnimationLayerSet();
-
+    public void goToState(LauncherState state, boolean animated, Runnable onCompleteRunnable) {
         // Cancel the current animation
-        cancelAnimation();
-
-        if (!animated) {
-            mLauncher.getWorkspace().setState(LauncherState.ALL_APPS);
-
-            mAllAppsController.finishPullUp();
-            toView.setTranslationX(0.0f);
-            toView.setTranslationY(0.0f);
-            toView.setScaleX(1.0f);
-            toView.setScaleY(1.0f);
-            toView.setAlpha(1.0f);
-            toView.setVisibility(View.VISIBLE);
-
-            mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
-            return;
-        }
-
-        if (!FeatureFlags.LAUNCHER3_PHYSICS) {
-            // We are animating the content view alpha, so ensure we have a layer for it.
-            layerViews.addView(toView);
-        }
-
-        animation.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                cleanupAnimation();
-                mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
-            }
-        });
-
         mConfig.reset();
-        mAllAppsController.animateToAllApps(animation, mConfig);
-        mLauncher.getWorkspace().setStateWithAnimation(LauncherState.ALL_APPS,
-                layerViews, animation, mConfig);
-
-        Runnable startAnimRunnable = new StartAnimRunnable(animation, toView);
-        mCurrentAnimation = animation;
-        mCurrentAnimation.addListener(layerViews);
-        if (mConfig.shouldPost) {
-            toView.post(startAnimRunnable);
-        } else {
-            startAnimRunnable.run();
-        }
-    }
-
-    /**
-     * Starts an animation to the workspace from the current overlay view.
-     */
-    public void startAnimationToWorkspace(final LauncherState toWorkspaceState,
-            final boolean animated, final Runnable onCompleteRunnable) {
-        if (toWorkspaceState != LauncherState.NORMAL &&
-                toWorkspaceState != LauncherState.SPRING_LOADED &&
-                toWorkspaceState != LauncherState.OVERVIEW) {
-            Log.e(TAG, "Unexpected call to startAnimationToWorkspace");
-        }
-
-        if (mLauncher.isInState(LauncherState.ALL_APPS) || mAllAppsController.isTransitioning()) {
-            startAnimationToWorkspaceFromAllApps(mLauncher.getWorkspace().getState(),
-                    toWorkspaceState, animated, onCompleteRunnable);
-        } else {
-            startAnimationToNewWorkspaceState(toWorkspaceState, animated, onCompleteRunnable);
-        }
-    }
-
-    /**
-     * Starts an animation to the workspace from the apps view.
-     */
-    private void startAnimationToWorkspaceFromAllApps(final LauncherState fromWorkspaceState,
-            final LauncherState toWorkspaceState, boolean animated,
-            final Runnable onCompleteRunnable) {
-        final AllAppsContainerView fromView = mLauncher.getAppsView();
-        // If for some reason our views aren't initialized, don't animate
-        animated = animated & (fromView != null);
-
-        final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
-        final AnimationLayerSet layerViews = new AnimationLayerSet();
-
-        // Cancel the current animation
-        cancelAnimation();
 
         if (!animated) {
-            if (fromWorkspaceState == LauncherState.ALL_APPS) {
-                mAllAppsController.finishPullDown();
-            }
-            fromView.setVisibility(View.GONE);
-            mLauncher.getWorkspace().setState(toWorkspaceState);
+            mAllAppsController.setFinalProgress(state.verticalProgress);
+            mLauncher.getWorkspace().setState(state);
             mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
 
-            // Run any queued runnables
+            // Run any queued runnable
             if (onCompleteRunnable != null) {
                 onCompleteRunnable.run();
             }
             return;
         }
 
-        animation.addListener(new AnimatorListenerAdapter() {
-            boolean canceled = false;
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                canceled = true;
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (canceled) return;
-                // Run any queued runnables
-                if (onCompleteRunnable != null) {
-                    onCompleteRunnable.run();
-                }
-
-                cleanupAnimation();
-                mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
-            }
-
-        });
-
-        mConfig.reset();
-        mAllAppsController.animateToWorkspace(animation, mConfig);
-        mLauncher.getWorkspace().setStateWithAnimation(toWorkspaceState, layerViews, animation,
-                mConfig);
-
-        Runnable startAnimRunnable = new StartAnimRunnable(animation, mLauncher.getWorkspace());
-        mCurrentAnimation = animation;
-        mCurrentAnimation.addListener(layerViews);
+        AnimatorSet animation = createAnimationToNewWorkspace(state, onCompleteRunnable);
+        Runnable runnable = new StartAnimRunnable(animation, state.getFinalFocus(mLauncher));
         if (mConfig.shouldPost) {
-            fromView.post(startAnimRunnable);
+            mUiHandler.post(runnable);
         } else {
-            startAnimRunnable.run();
+            runnable.run();
         }
     }
 
-    /**
-     * Starts an animation to the workspace from another workspace state, e.g. normal to overview.
-     */
-    private void startAnimationToNewWorkspaceState(
-            final LauncherState toWorkspaceState, final boolean animated,
-            final Runnable onCompleteRunnable) {
-        // Cancel the current animation
-        cancelAnimation();
-        mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
-
-        if (!animated) {
-            mLauncher.getWorkspace().setState(toWorkspaceState);
-            // Run any queued runnables
-            if (onCompleteRunnable != null) {
-                onCompleteRunnable.run();
-            }
-            return;
-        }
-
-        final AnimatorSet animation =
-                createAnimationToNewWorkspace(toWorkspaceState, onCompleteRunnable);
-        mLauncher.getWorkspace().post(new StartAnimRunnable(animation, null));
-        mCurrentAnimation = animation;
-    }
-
     protected AnimatorSet createAnimationToNewWorkspace(LauncherState state,
             final Runnable onCompleteRunnable) {
-        cancelAnimation();
-
-        final AnimationLayerSet layerViews = new AnimationLayerSet();
-        final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
         mConfig.reset();
-        mLauncher.getWorkspace().setStateWithAnimation(state, layerViews, animation, mConfig);
 
-        animation.addListener(new AnimatorListenerAdapter() {
+        final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
+        final AnimationLayerSet layerViews = new AnimationLayerSet();
+
+        mAllAppsController.animateToFinalProgress(state.verticalProgress,
+                state.hasVerticalSpring, animation, mConfig);
+        mLauncher.getWorkspace().setStateWithAnimation(state,
+                layerViews, animation, mConfig);
+
+        animation.addListener(layerViews);
+        animation.addListener(new AnimationSuccessListener() {
             @Override
-            public void onAnimationEnd(Animator animation) {
+            public void onAnimationSuccess(Animator animator) {
                 // Run any queued runnables
                 if (onCompleteRunnable != null) {
                     onCompleteRunnable.run();
                 }
 
-                // This can hold unnecessary references to views.
-                cleanupAnimation();
+                mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
             }
         });
-        animation.addListener(layerViews);
-        return animation;
+        mConfig.setAnimation(animation);
+        return mConfig.mCurrentAnimation;
     }
 
     /**
      * Cancels the current animation.
      */
-    private void cancelAnimation() {
-        if (mCurrentAnimation != null) {
-            mCurrentAnimation.setDuration(0);
-            mCurrentAnimation.cancel();
-            mCurrentAnimation = null;
-        }
-    }
-
-    @Thunk void cleanupAnimation() {
-        mCurrentAnimation = null;
+    public void cancelAnimation() {
+        mConfig.reset();
     }
 
     private class StartAnimRunnable implements Runnable {
@@ -305,7 +156,7 @@
 
         @Override
         public void run() {
-            if (mCurrentAnimation != mAnim) {
+            if (mConfig.mCurrentAnimation != mAnim) {
                 return;
             }
             if (mViewToFocus != null) {
@@ -315,14 +166,21 @@
         }
     }
 
-    public static class AnimationConfig {
+    public static class AnimationConfig extends AnimatorListenerAdapter {
         public boolean shouldPost;
 
         private long mOverriddenDuration = -1;
+        private AnimatorSet mCurrentAnimation;
 
         public void reset() {
             shouldPost = false;
             mOverriddenDuration = -1;
+
+            if (mCurrentAnimation != null) {
+                mCurrentAnimation.setDuration(0);
+                mCurrentAnimation.cancel();
+                mCurrentAnimation = null;
+            }
         }
 
         public void overrideDuration(long duration) {
@@ -332,5 +190,17 @@
         public long getDuration(long defaultDuration) {
             return mOverriddenDuration >= 0 ? mOverriddenDuration : defaultDuration;
         }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            if (mCurrentAnimation == animation) {
+                mCurrentAnimation = null;
+            }
+        }
+
+        public void setAnimation(AnimatorSet animation) {
+            mCurrentAnimation = animation;
+            mCurrentAnimation.addListener(this);
+        }
     }
 }
diff --git a/src/com/android/launcher3/PinchToOverviewListener.java b/src/com/android/launcher3/PinchToOverviewListener.java
index 47113c9..407f0b5 100644
--- a/src/com/android/launcher3/PinchToOverviewListener.java
+++ b/src/com/android/launcher3/PinchToOverviewListener.java
@@ -107,7 +107,7 @@
 
         mToState = mLauncher.isInState(LauncherState.OVERVIEW)
                 ? LauncherState.NORMAL : LauncherState.OVERVIEW;
-        mCurrentAnimation = mLauncher.mStateTransitionAnimation
+        mCurrentAnimation = mLauncher.getStateTransition()
                 .createAnimationToNewWorkspace(mToState, this);
         mPinchStarted = true;
         mCurrentScale = 1;
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 9247c54..35dfa81 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -4,10 +4,11 @@
 import android.animation.AnimatorInflater;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.animation.ArgbEvaluator;
 import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
 import android.support.animation.SpringAnimation;
 import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.util.Property;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.animation.AccelerateInterpolator;
@@ -22,12 +23,13 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.SpringAnimationHandler;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.GradientView;
 import com.android.launcher3.touch.SwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.SystemUiController;
 import com.android.launcher3.util.Themes;
@@ -46,6 +48,25 @@
 public class AllAppsTransitionController implements TouchController, SwipeDetector.Listener,
          SearchUiManager.OnScrollRangeChangeListener {
 
+    private static final Property<AllAppsTransitionController, Float> PROGRESS =
+            new Property<AllAppsTransitionController, Float>(Float.class, "progress") {
+
+        @Override
+        public Float get(AllAppsTransitionController controller) {
+            return controller.mProgress;
+        }
+
+        @Override
+        public void set(AllAppsTransitionController controller, Float progress) {
+            controller.setProgress(progress);
+        }
+    };
+
+    // Spring values used when the user has not flung all apps.
+    private static final float SPRING_MAX_RELEASE_VELOCITY = 10000;
+    // The delay (as a % of the animation duration) to start the springs.
+    private static final float SPRING_DELAY = 0.3f;
+
     private final Interpolator mWorkspaceAccelnterpolator = new AccelerateInterpolator(2f);
     private final Interpolator mHotseatAccelInterpolator = new AccelerateInterpolator(1.5f);
     private final Interpolator mFastOutSlowInInterpolator = new FastOutSlowInInterpolator();
@@ -61,11 +82,8 @@
 
     private AllAppsCaretController mCaretController;
 
-    private float mStatusBarHeight;
-
     private final Launcher mLauncher;
     private final SwipeDetector mDetector;
-    private final ArgbEvaluator mEvaluator;
     private final boolean mIsDarkTheme;
 
     // Animation in this class is controlled by a single variable {@link mProgress}.
@@ -87,7 +105,6 @@
 
     private long mAnimationDuration;
 
-    private AnimatorSet mCurrentAnimation;
     private boolean mNoIntercept;
     private boolean mTouchEventStartedOnHotseat;
 
@@ -105,7 +122,6 @@
         mShiftRange = DEFAULT_SHIFT_RANGE;
         mProgress = 1f;
 
-        mEvaluator = new ArgbEvaluator();
         mIsDarkTheme = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark);
     }
 
@@ -177,10 +193,10 @@
     @Override
     public void onDragStart(boolean start) {
         mCaretController.onDragStart();
-        cancelAnimation();
-        mCurrentAnimation = LauncherAnimUtils.createAnimatorSet();
+        mLauncher.getStateTransition().cancelAnimation();
+        cancelDiscoveryAnimation();
         mShiftStart = mAppsView.getTranslationY();
-        preparePull(start);
+        onProgressAnimationStart();
         if (hasSpringAnimationHandler()) {
             mSpringAnimationHandler.skipToEnd();
         }
@@ -263,16 +279,10 @@
         return mDetector.isDraggingOrSettling();
     }
 
-    /**
-     * @param start {@code true} if start of new drag.
-     */
-    public void preparePull(boolean start) {
-        if (start) {
-            // Initialize values that should not change until #onDragEnd
-            mStatusBarHeight = mLauncher.getDragLayer().getInsets().top;
-            mHotseat.setVisibility(View.VISIBLE);
-            mAppsView.setVisibility(View.VISIBLE);
-        }
+    private void onProgressAnimationStart() {
+        // Initialize values that should not change until #onDragEnd
+        mHotseat.setVisibility(View.VISIBLE);
+        mAppsView.setVisibility(View.VISIBLE);
     }
 
     private void updateLightStatusBar(float shift) {
@@ -296,7 +306,13 @@
     }
 
     /**
-     * @param progress       value between 0 and 1, 0 shows all apps and 1 shows workspace
+     * Note this method should not be called outside this class. This is public because it is used
+     * in xml-based animations which also handle updating the appropriate UI.
+     *
+     * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace
+     *
+     * @see #setFinalProgress(float)
+     * @see #animateToFinalProgress(float, boolean, AnimatorSet, AnimationConfig)
      */
     public void setProgress(float progress) {
         float shiftPrevious = mProgress * mShiftRange;
@@ -344,74 +360,77 @@
         mAnimationDuration = SwipeDetector.calculateDuration(velocity, disp / mShiftRange);
     }
 
-    public void animateToAllApps(AnimatorSet animationOut, AnimationConfig outConfig) {
-        outConfig.shouldPost = true;
-        if (animationOut == null) {
+    /**
+     * Sets the vertical transition progress to {@param progress} and updates all the dependent UI
+     * accordingly.
+     */
+    public void setFinalProgress(float progress) {
+        setProgress(progress);
+        onProgressAnimationEnd();
+    }
+
+    /**
+     * Creates an animation which updates the vertical transition progress and updates all the
+     * dependent UI using various animation events
+     *
+     * @param progress the final vertical progress at the end of the animation
+     * @param addSpring should there be an addition spring animation for the sub-views
+     * @param animationOut the target AnimatorSet where this animation should be added
+     * @param outConfig an in/out configuration which can be shared with other animations
+     */
+    public void animateToFinalProgress(float progress, boolean addSpring,
+            AnimatorSet animationOut, AnimationConfig outConfig) {
+        if (Float.compare(mProgress, progress) == 0) {
+            // Fail fast
+            onProgressAnimationEnd();
             return;
         }
+
+        outConfig.shouldPost = true;
         Interpolator interpolator;
         if (mDetector.isIdleState()) {
-            preparePull(true);
             mAnimationDuration = LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
             mShiftStart = mAppsView.getTranslationY();
             interpolator = mFastOutSlowInInterpolator;
         } else {
             mScrollInterpolator.setVelocityAtZero(Math.abs(mContainerVelocity));
             interpolator = mScrollInterpolator;
-            float nextFrameProgress = mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange;
-            if (nextFrameProgress >= 0f) {
-                mProgress = nextFrameProgress;
-            }
+            mProgress = Utilities.boundToRange(
+                    mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange, 0f, 1f);
             outConfig.shouldPost = false;
         }
 
         outConfig.overrideDuration(mAnimationDuration);
-        ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
-                mProgress, 0f);
-        driftAndAlpha.setDuration(mAnimationDuration);
-        driftAndAlpha.setInterpolator(interpolator);
-        animationOut.play(driftAndAlpha);
-
-        animationOut.addListener(new AnimatorListenerAdapter() {
-            // Spring values used when the user has not flung all apps.
-            private final float MAX_RELEASE_VELOCITY = 10000;
-            // The delay (as a % of the animation duration) to start the springs.
-            private final float DELAY = 0.3f;
-
-            boolean canceled = false;
-
+        ObjectAnimator anim = ObjectAnimator.ofFloat(this, PROGRESS, mProgress, progress);
+        anim.setDuration(mAnimationDuration);
+        anim.setInterpolator(interpolator);
+        anim.addListener(new AnimationSuccessListener() {
             @Override
-            public void onAnimationCancel(Animator animation) {
-                canceled = true;
+            public void onAnimationSuccess(Animator animator) {
+                onProgressAnimationEnd();
             }
 
             @Override
             public void onAnimationStart(Animator animation) {
-                // Add springs for cases where the user has not flung.
-                // ie. clicking on the caret, releasing all apps so it snaps up.
-                mAppsView.postDelayed(new Runnable() {
-                    @Override
-                    public void run() {
-                        if (!canceled && !mSpringAnimationHandler.isRunning()) {
-                            float velocity = mProgress * MAX_RELEASE_VELOCITY;
-                            mSpringAnimationHandler.animateToPositionWithVelocity(0, 1, velocity);
-                        }
-                    }
-                }, (long) (mAnimationDuration * DELAY));
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (canceled) {
-                    return;
-                } else {
-                    finishPullUp();
-                    cleanUpAnimation();
-                    mDetector.finishedScrolling();
-                }
+                onProgressAnimationStart();
             }
         });
-        mCurrentAnimation = animationOut;
+
+        animationOut.play(anim);
+        if (addSpring) {
+            ValueAnimator springAnim = ValueAnimator.ofFloat(0, 1);
+            springAnim.setDuration((long) (mAnimationDuration * SPRING_DELAY));
+            springAnim.addListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    if (!mSpringAnimationHandler.isRunning()) {
+                        float velocity = mProgress * SPRING_MAX_RELEASE_VELOCITY;
+                        mSpringAnimationHandler.animateToPositionWithVelocity(0, 1, velocity);
+                    }
+                }
+            });
+            animationOut.play(anim);
+        }
     }
 
     public void showDiscoveryBounce() {
@@ -425,12 +444,12 @@
             @Override
             public void onAnimationStart(Animator animator) {
                 mIsTranslateWithoutWorkspace = true;
-                preparePull(true);
+                onProgressAnimationStart();
             }
 
             @Override
             public void onAnimationEnd(Animator animator) {
-                finishPullDown();
+                onProgressAnimationEnd();
                 mDiscoBounceAnimation = null;
                 mIsTranslateWithoutWorkspace = false;
             }
@@ -447,83 +466,6 @@
         });
     }
 
-    public void animateToWorkspace(AnimatorSet animationOut, AnimationConfig outconfig) {
-        outconfig.shouldPost = true;
-        if (animationOut == null) {
-            return;
-        }
-        Interpolator interpolator;
-        if (mDetector.isIdleState()) {
-            preparePull(true);
-            mAnimationDuration = LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
-            mShiftStart = mAppsView.getTranslationY();
-            interpolator = mFastOutSlowInInterpolator;
-        } else {
-            mScrollInterpolator.setVelocityAtZero(Math.abs(mContainerVelocity));
-            interpolator = mScrollInterpolator;
-            float nextFrameProgress = mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange;
-            if (nextFrameProgress <= 1f) {
-                mProgress = nextFrameProgress;
-            }
-            outconfig.shouldPost = false;
-        }
-
-        outconfig.overrideDuration(mAnimationDuration);
-        ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
-                mProgress, 1f);
-        driftAndAlpha.setDuration(mAnimationDuration);
-        driftAndAlpha.setInterpolator(interpolator);
-        animationOut.play(driftAndAlpha);
-
-        animationOut.addListener(new AnimatorListenerAdapter() {
-            boolean canceled = false;
-
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                canceled = true;
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (canceled) {
-                    return;
-                } else {
-                    finishPullDown();
-                    cleanUpAnimation();
-                    mDetector.finishedScrolling();
-                }
-            }
-        });
-        mCurrentAnimation = animationOut;
-    }
-
-    public void finishPullUp() {
-        mHotseat.setVisibility(View.INVISIBLE);
-        if (hasSpringAnimationHandler()) {
-            mSpringAnimationHandler.remove(mSearchSpring);
-            mSpringAnimationHandler.reset();
-        }
-        setProgress(0f);
-    }
-
-    public void finishPullDown() {
-        mAppsView.setVisibility(View.INVISIBLE);
-        mHotseat.setVisibility(View.VISIBLE);
-        mAppsView.reset();
-        if (hasSpringAnimationHandler()) {
-            mSpringAnimationHandler.reset();
-        }
-        setProgress(1f);
-    }
-
-    private void cancelAnimation() {
-        if (mCurrentAnimation != null) {
-            mCurrentAnimation.cancel();
-            mCurrentAnimation = null;
-        }
-        cancelDiscoveryAnimation();
-    }
-
     public void cancelDiscoveryAnimation() {
         if (mDiscoBounceAnimation == null) {
             return;
@@ -532,10 +474,6 @@
         mDiscoBounceAnimation = null;
     }
 
-    private void cleanUpAnimation() {
-        mCurrentAnimation = null;
-    }
-
     public void setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace) {
         mAppsView = appsView;
         mHotseat = hotseat;
@@ -557,4 +495,27 @@
         mShiftRange = scrollRange;
         setProgress(mProgress);
     }
+
+    /**
+     * Set the final view states based on the progress.
+     * TODO: This logic should go in {@link LauncherState}
+     */
+    private void onProgressAnimationEnd() {
+        if (Float.compare(mProgress, 1f) == 0) {
+            mAppsView.setVisibility(View.INVISIBLE);
+            mHotseat.setVisibility(View.VISIBLE);
+            mAppsView.reset();
+        } else if (Float.compare(mProgress, 0f) == 0) {
+            mHotseat.setVisibility(View.INVISIBLE);
+            mAppsView.setVisibility(View.VISIBLE);
+        }
+        if (hasSpringAnimationHandler()) {
+            mSpringAnimationHandler.remove(mSearchSpring);
+            mSpringAnimationHandler.reset();
+        }
+
+        // TODO: This call should no longer be needed once caret stops animating.
+        setProgress(mProgress);
+        mDetector.finishedScrolling();
+    }
 }
diff --git a/src/com/android/launcher3/anim/AnimationSuccessListener.java b/src/com/android/launcher3/anim/AnimationSuccessListener.java
new file mode 100644
index 0000000..feebc6c
--- /dev/null
+++ b/src/com/android/launcher3/anim/AnimationSuccessListener.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 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.launcher3.anim;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+
+/**
+ * Extension of {@link AnimatorListenerAdapter} for listening for non-cancelled animations
+ */
+public abstract class AnimationSuccessListener extends AnimatorListenerAdapter {
+
+    private boolean mCancelled = false;
+
+    @Override
+    public void onAnimationCancel(Animator animation) {
+        mCancelled = true;
+    }
+
+    @Override
+    public void onAnimationEnd(Animator animation) {
+        if (!mCancelled) {
+            onAnimationSuccess(animation);
+        }
+    }
+
+    public abstract void onAnimationSuccess(Animator animator);
+}
diff --git a/src/com/android/launcher3/states/AllAppsState.java b/src/com/android/launcher3/states/AllAppsState.java
new file mode 100644
index 0000000..9922d99
--- /dev/null
+++ b/src/com/android/launcher3/states/AllAppsState.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 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.launcher3.states;
+
+import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
+
+import android.view.View;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+
+/**
+ * Definition for AllApps state
+ */
+public class AllAppsState extends LauncherState {
+
+    public static final String APPS_VIEW_SHOWN = "launcher.apps_view_shown";
+
+    private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY | FLAG_HAS_SPRING;
+
+    public AllAppsState(int id) {
+        super(id, ContainerType.ALLAPPS, ALL_APPS_TRANSITION_MS, 0f, STATE_FLAGS);
+    }
+
+    @Override
+    public void onStateEnabled(Launcher launcher) {
+        if (!launcher.getSharedPrefs().getBoolean(APPS_VIEW_SHOWN, false)) {
+            launcher.getSharedPrefs().edit().putBoolean(APPS_VIEW_SHOWN, true).apply();
+        }
+    }
+
+    @Override
+    public View getFinalFocus(Launcher launcher) {
+        return launcher.getAppsView();
+    }
+}
diff --git a/src/com/android/launcher3/states/OverviewState.java b/src/com/android/launcher3/states/OverviewState.java
index 911cec6..57f023c 100644
--- a/src/com/android/launcher3/states/OverviewState.java
+++ b/src/com/android/launcher3/states/OverviewState.java
@@ -19,6 +19,7 @@
 import static com.android.launcher3.Utilities.isAccessibilityEnabled;
 
 import android.graphics.Rect;
+import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import com.android.launcher3.DeviceProfile;
@@ -39,7 +40,7 @@
             FLAG_DO_NOT_RESTORE;
 
     public OverviewState(int id) {
-        super(id, ContainerType.WORKSPACE, OVERVIEW_TRANSITION_MS, STATE_FLAGS);
+        super(id, ContainerType.WORKSPACE, OVERVIEW_TRANSITION_MS, 1f, STATE_FLAGS);
     }
 
     @Override
@@ -75,4 +76,9 @@
     public void onStateDisabled(Launcher launcher) {
         launcher.getWorkspace().setPageRearrangeEnabled(false);
     }
+
+    @Override
+    public View getFinalFocus(Launcher launcher) {
+        return launcher.getOverviewPanel();
+    }
 }
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index f60ef0c..3864e3a 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -20,6 +20,7 @@
 import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
 import android.os.Handler;
+import android.view.View;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.InstallShortcutReceiver;
@@ -41,7 +42,7 @@
     private static final int RESTORE_SCREEN_ORIENTATION_DELAY = 500;
 
     public SpringLoadedState(int id) {
-        super(id, ContainerType.OVERVIEW, SPRING_LOADED_TRANSITION_MS, STATE_FLAGS);
+        super(id, ContainerType.OVERVIEW, SPRING_LOADED_TRANSITION_MS, 1f, STATE_FLAGS);
     }
 
     @Override
@@ -105,4 +106,9 @@
         InstallShortcutReceiver.disableAndFlushInstallQueue(
                 InstallShortcutReceiver.FLAG_DRAG_AND_DROP, launcher);
     }
+
+    @Override
+    public View getFinalFocus(Launcher launcher) {
+        return null;
+    }
 }