Merge "Using state animation to control all-apps transition" into ub-launcher3-master
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 661ba11..63c232d 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -39,7 +39,6 @@
     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];
 
@@ -90,7 +89,6 @@
      * @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) {
@@ -106,7 +104,6 @@
         this.doNotRestore = (flags & FLAG_DO_NOT_RESTORE) != 0;
 
         this.verticalProgress = verticalProgress;
-        this.hasVerticalSpring = (flags & FLAG_HAS_SPRING) != 0;
 
         this.ordinal = id;
         sAllStates[id] = this;
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index 863cae7..fd94067 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -28,6 +28,7 @@
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimationLayerSet;
 import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.AnimatorPlaybackController;
 
 /**
  * TODO: figure out what kind of tests we can write for this
@@ -133,8 +134,7 @@
 
     private void goToState(LauncherState state, boolean animated, long delay,
             Runnable onCompleteRunnable) {
-        if (mLauncher.isInState(state) && mConfig.mCurrentAnimation == null
-                && !mAllAppsController.isTransitioning()) {
+        if (mLauncher.isInState(state) && mConfig.mCurrentAnimation == null) {
 
             // Run any queued runnable
             if (onCompleteRunnable != null) {
@@ -159,27 +159,42 @@
             return;
         }
 
-        AnimatorSet animation = createAnimationToNewWorkspace(state, onCompleteRunnable);
+        // Since state NORMAL can be reached from multiple states, just assume that the
+        // transition plays in reverse and use the same duration as previous state.
+        mConfig.duration = state == NORMAL ? mState.transitionDuration : state.transitionDuration;
+
+        AnimatorSet animation = createAnimationToNewWorkspaceInternal(state, onCompleteRunnable);
         Runnable runnable = new StartAnimRunnable(animation, state.getFinalFocus(mLauncher));
         if (delay > 0) {
             mUiHandler.postDelayed(runnable, delay);
-        } else if (mConfig.shouldPost) {
-            mUiHandler.post(runnable);
         } else {
-            runnable.run();
+            mUiHandler.post(runnable);
         }
     }
 
-    protected AnimatorSet createAnimationToNewWorkspace(final LauncherState state,
-            final Runnable onCompleteRunnable) {
+    /**
+     * Creates a {@link AnimatorPlaybackController} that can be used for a controlled
+     * state transition.
+     * @param state the final state for the transition.
+     * @param duration intended duration for normal playback. Use higher duration for better
+     *                accuracy.
+     */
+    protected AnimatorPlaybackController createAnimationToNewWorkspace(
+            LauncherState state, long duration) {
         mConfig.reset();
-        mConfig.duration = state == NORMAL ? mState.transitionDuration : state.transitionDuration;
+        mConfig.userControlled = true;
+        mConfig.duration = duration;
+        return AnimatorPlaybackController.wrap(
+                createAnimationToNewWorkspaceInternal(state, null), duration);
+    }
+
+    protected AnimatorSet createAnimationToNewWorkspaceInternal(final LauncherState state,
+            final Runnable onCompleteRunnable) {
 
         final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
         final AnimationLayerSet layerViews = new AnimationLayerSet();
 
-        mAllAppsController.animateToFinalProgress(state.verticalProgress,
-                state.hasVerticalSpring, animation, mConfig);
+        mAllAppsController.animateToFinalProgress(state.verticalProgress, animation, mConfig);
         mLauncher.getWorkspace().setStateWithAnimation(state,
                 layerViews, animation, mConfig);
 
@@ -242,14 +257,14 @@
     }
 
     public static class AnimationConfig extends AnimatorListenerAdapter {
-        public boolean shouldPost;
         public long duration;
+        public boolean userControlled;
 
         private AnimatorSet mCurrentAnimation;
 
         public void reset() {
-            shouldPost = true;
             duration = 0;
+            userControlled = false;
 
             if (mCurrentAnimation != null) {
                 mCurrentAnimation.setDuration(0);
diff --git a/src/com/android/launcher3/PinchToOverviewListener.java b/src/com/android/launcher3/PinchToOverviewListener.java
index d1a2538..27edaf6 100644
--- a/src/com/android/launcher3/PinchToOverviewListener.java
+++ b/src/com/android/launcher3/PinchToOverviewListener.java
@@ -22,18 +22,19 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
 import android.view.ScaleGestureDetector.OnScaleGestureListener;
 
-import com.android.launcher3.compat.AnimatorSetCompat;
+import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.util.TouchController;
 
 /**
  * Detects pinches and animates the Workspace to/from overview mode.
  */
-public class PinchToOverviewListener
-        implements TouchController, OnScaleGestureListener, Runnable {
+public class PinchToOverviewListener extends AnimatorListenerAdapter
+        implements TouchController, OnScaleGestureListener {
 
     private static final float ACCEPT_THRESHOLD = 0.65f;
     /**
@@ -47,7 +48,7 @@
     private Workspace mWorkspace = null;
     private boolean mPinchStarted = false;
 
-    private AnimatorSetCompat mCurrentAnimation;
+    private AnimatorPlaybackController mCurrentAnimation;
     private float mCurrentScale;
     private boolean mShouldGoToFinalState;
 
@@ -100,8 +101,9 @@
         }
 
         mToState = mLauncher.isInState(OVERVIEW) ? NORMAL : OVERVIEW;
-        mCurrentAnimation = AnimatorSetCompat.wrap(mLauncher.getStateManager()
-                .createAnimationToNewWorkspace(mToState, this), OVERVIEW_TRANSITION_MS);
+        mCurrentAnimation = mLauncher.getStateManager()
+                .createAnimationToNewWorkspace(mToState, OVERVIEW_TRANSITION_MS);
+        mCurrentAnimation.getTarget().addListener(this);
         mPinchStarted = true;
         mCurrentScale = 1;
         mShouldGoToFinalState = false;
@@ -111,7 +113,7 @@
     }
 
     @Override
-    public void run() {
+    public void onAnimationEnd(Animator animation) {
         mCurrentAnimation = null;
         mPinchStarted = false;
     }
@@ -121,9 +123,9 @@
         if (mShouldGoToFinalState) {
             mCurrentAnimation.start();
         } else {
-            mCurrentAnimation.addListener(new AnimatorListenerAdapter() {
+            mCurrentAnimation.setEndAction(new Runnable() {
                 @Override
-                public void onAnimationEnd(Animator animation) {
+                public void run() {
                     mLauncher.getStateManager().goToState(
                             mToState == OVERVIEW ? NORMAL : OVERVIEW, false);
                 }
diff --git a/src/com/android/launcher3/VerticalSwipeController.java b/src/com/android/launcher3/VerticalSwipeController.java
new file mode 100644
index 0000000..12c6916
--- /dev/null
+++ b/src/com/android/launcher3/VerticalSwipeController.java
@@ -0,0 +1,282 @@
+/*
+ * 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;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.anim.SpringAnimationHandler.Y_DIRECTION;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.support.animation.SpringAnimation;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.SpringAnimationHandler;
+import com.android.launcher3.touch.SwipeDetector;
+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.TouchController;
+
+import java.util.ArrayList;
+
+/**
+ * Handles vertical touch gesture on the DragLayer
+ */
+public class VerticalSwipeController extends AnimatorListenerAdapter
+        implements TouchController, SwipeDetector.Listener {
+
+    private static final String TAG = "VerticalSwipeController";
+
+    private static final float RECATCH_REJECTION_FRACTION = .0875f;
+    private static final int SINGLE_FRAME_MS = 16;
+
+    // Progress after which the transition is assumed to be a success in case user does not fling
+    private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
+
+    private final Launcher mLauncher;
+    private final SwipeDetector mDetector;
+
+    private boolean mNoIntercept;
+    private int mStartContainerType;
+
+    private AnimatorPlaybackController mCurrentAnimation;
+    private LauncherState mToState;
+
+    private float mStartProgress;
+    // Ratio of transition process [0, 1] to drag displacement (px)
+    private float mProgressMultiplier;
+
+    private SpringAnimationHandler[] mSpringHandlers;
+
+    public VerticalSwipeController(Launcher l) {
+        mLauncher = l;
+        mDetector = new SwipeDetector(l, this, SwipeDetector.VERTICAL);
+    }
+
+    private boolean canInterceptTouch(MotionEvent ev) {
+        if (!mLauncher.isInState(NORMAL) && !mLauncher.isInState(ALL_APPS)) {
+            // Don't listen for the pinch gesture if on all apps, widget picker, -1, etc.
+            return false;
+        }
+        if (mCurrentAnimation != null) {
+            // If we are already animating from a previous state, we can intercept.
+            return true;
+        }
+        if (mLauncher.isInState(ALL_APPS) && !mLauncher.getAppsView().shouldContainerScroll(ev)) {
+            return false;
+        }
+        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public void onAnimationCancel(Animator animation) {
+        if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
+            Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception());
+            mDetector.finishedScrolling();
+            mCurrentAnimation = null;
+        }
+    }
+
+    private void initSprings() {
+        AllAppsContainerView appsView = mLauncher.getAppsView();
+
+        SpringAnimationHandler handler = appsView.getSpringAnimationHandler();
+        if (handler == null) {
+            mSpringHandlers = new SpringAnimationHandler[0];
+            return;
+        }
+
+        ArrayList<SpringAnimationHandler> handlers = new ArrayList<>();
+        handlers.add(handler);
+
+        SpringAnimation searchSpring = appsView.getSearchUiManager().getSpringForFling();
+        if (searchSpring != null) {
+            SpringAnimationHandler searchHandler =
+                    new SpringAnimationHandler(Y_DIRECTION, handler.getFactory());
+            searchHandler.add(searchSpring, true /* setDefaultValues */);
+            handlers.add(searchHandler);
+        }
+
+        mSpringHandlers = handlers.toArray(new SpringAnimationHandler[handlers.size()]);
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mNoIntercept = !canInterceptTouch(ev);
+            if (mNoIntercept) {
+                return false;
+            }
+
+            // Now figure out which direction scroll events the controller will start
+            // calling the callbacks.
+            final int directionsToDetectScroll;
+            boolean ignoreSlopWhenSettling = false;
+
+            if (mCurrentAnimation != null) {
+                if (mCurrentAnimation.getProgressFraction() > 1 - RECATCH_REJECTION_FRACTION) {
+                    directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
+                } else if (mCurrentAnimation.getProgressFraction() < RECATCH_REJECTION_FRACTION ) {
+                    directionsToDetectScroll = SwipeDetector.DIRECTION_NEGATIVE;
+                } else {
+                    directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                    ignoreSlopWhenSettling = true;
+                }
+            } else {
+                if (mLauncher.isInState(ALL_APPS)) {
+                    directionsToDetectScroll = SwipeDetector.DIRECTION_NEGATIVE;
+                    mStartContainerType = ContainerType.ALLAPPS;
+                } else {
+                    directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
+                    mStartContainerType = mLauncher.getDragLayer().isEventOverHotseat(ev) ?
+                            ContainerType.HOTSEAT : ContainerType.WORKSPACE;
+                }
+            }
+
+            mDetector.setDetectableScrollConditions(
+                    directionsToDetectScroll, ignoreSlopWhenSettling);
+
+            if (mSpringHandlers == null) {
+                initSprings();
+            }
+        }
+
+        if (mNoIntercept) {
+            return false;
+        }
+
+        onControllerTouchEvent(ev);
+        return mDetector.isDraggingOrSettling();
+    }
+
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        for (SpringAnimationHandler h : mSpringHandlers) {
+            h.addMovement(ev);
+        }
+        return mDetector.onTouchEvent(ev);
+    }
+
+    @Override
+    public void onDragStart(boolean start) {
+        if (mCurrentAnimation == null) {
+            float range = getShiftRange();
+            long maxAccuracy = (long) (2 * range);
+
+            // Build current animation
+            mToState = mLauncher.isInState(ALL_APPS) ? NORMAL : ALL_APPS;
+            mCurrentAnimation = mLauncher.getStateManager()
+                    .createAnimationToNewWorkspace(mToState, maxAccuracy);
+            mCurrentAnimation.getTarget().addListener(this);
+            mStartProgress = 0;
+            mProgressMultiplier = (mLauncher.isInState(ALL_APPS) ? 1 : -1) / range;
+            mCurrentAnimation.dispatchOnStart();
+        } else {
+            mCurrentAnimation.pause();
+            mStartProgress = mCurrentAnimation.getProgressFraction();
+        }
+
+        for (SpringAnimationHandler h : mSpringHandlers) {
+            h.skipToEnd();
+        }
+    }
+
+    private float getShiftRange() {
+        return mLauncher.mAllAppsController.getShiftRange();
+    }
+
+    @Override
+    public boolean onDrag(float displacement, float velocity) {
+        float deltaProgress = mProgressMultiplier * displacement;
+        mCurrentAnimation.setPlayFraction(deltaProgress + mStartProgress);
+        return true;
+    }
+
+    @Override
+    public void onDragEnd(float velocity, boolean fling) {
+        final long animationDuration;
+        final int logAction;
+        final LauncherState targetState;
+        final float progress = mCurrentAnimation.getProgressFraction();
+
+        if (fling) {
+            logAction = Touch.FLING;
+            if (velocity < 0) {
+                targetState = ALL_APPS;
+                animationDuration = SwipeDetector.calculateDuration(velocity,
+                        mToState == ALL_APPS ? (1 - progress) : progress);
+            } else {
+                targetState = NORMAL;
+                animationDuration = SwipeDetector.calculateDuration(velocity,
+                        mToState == ALL_APPS ? progress : (1 - progress));
+            }
+            // snap to top or bottom using the release velocity
+        } else {
+            logAction = Touch.SWIPE;
+            if (progress > SUCCESS_TRANSITION_PROGRESS) {
+                targetState = mToState;
+                animationDuration = SwipeDetector.calculateDuration(velocity, 1 - progress);
+            } else {
+                targetState = mToState == ALL_APPS ? NORMAL : ALL_APPS;
+                animationDuration = SwipeDetector.calculateDuration(velocity, progress);
+            }
+        }
+
+        if (fling && targetState == ALL_APPS) {
+            for (SpringAnimationHandler h : mSpringHandlers) {
+                // The icons are moving upwards, so we go to 0 from 1. (y-axis 1 is below 0.)
+                h.animateToFinalPosition(0 /* pos */, 1 /* startValue */);
+            }
+        }
+
+        mCurrentAnimation.setEndAction(new Runnable() {
+            @Override
+            public void run() {
+                if (targetState == mToState) {
+                    // Transition complete. log the action
+                    mLauncher.getUserEventDispatcher().logActionOnContainer(logAction,
+                            mToState == ALL_APPS ? Direction.UP : Direction.DOWN,
+                            mStartContainerType, mLauncher.getWorkspace().getCurrentPage());
+                } else {
+                    mLauncher.getStateManager().goToState(
+                            mToState == ALL_APPS ? NORMAL : ALL_APPS, false);
+                }
+                mDetector.finishedScrolling();
+                mCurrentAnimation = null;
+            }
+        });
+
+        float nextFrameProgress = Utilities.boundToRange(
+                progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f);
+
+        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
+        anim.setFloatValues(nextFrameProgress, targetState == mToState ? 1f : 0f);
+        anim.setDuration(animationDuration);
+        anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
+        anim.start();
+    }
+}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index e72f54e..24c4704 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -1582,6 +1582,7 @@
 
         ValueAnimator stepAnimator = ValueAnimator.ofFloat(0, 1);
         stepAnimator.addUpdateListener(listener);
+        stepAnimator.setDuration(config.duration);
         anim.play(stepAnimator);
         anim.addListener(listener);
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index a84172d..9b64043 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -1,25 +1,19 @@
 package com.android.launcher3.allapps;
 
-import static com.android.launcher3.LauncherState.ALL_APPS;
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
 
 import android.animation.Animator;
 import android.animation.AnimatorInflater;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.support.animation.SpringAnimation;
 import android.util.Property;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.animation.Interpolator;
 
-import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
 import com.android.launcher3.R;
@@ -27,16 +21,9 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
-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.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;
-import com.android.launcher3.util.TouchController;
 
 /**
  * Handles AllApps view transition.
@@ -48,8 +35,7 @@
  * If release velocity < THRES1, snap according to either top or bottom depending on whether it's
  * closer to top or closer to the page indicator.
  */
-public class AllAppsTransitionController implements TouchController, SwipeDetector.Listener,
-         SearchUiManager.OnScrollRangeChangeListener {
+public class AllAppsTransitionController implements SearchUiManager.OnScrollRangeChangeListener {
 
     private static final Property<AllAppsTransitionController, Float> PROGRESS =
             new Property<AllAppsTransitionController, Float>(Float.class, "progress") {
@@ -65,24 +51,16 @@
         }
     };
 
-    // 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 = Interpolators.ACCEL_2;
     private final Interpolator mHotseatAccelInterpolator = Interpolators.ACCEL_1_5;
-    private final Interpolator mFastOutSlowInInterpolator = Interpolators.FAST_OUT_SLOW_IN;
 
     private static final float PARALLAX_COEFFICIENT = .125f;
-    private static final int SINGLE_FRAME_MS = 16;
 
     private AllAppsContainerView mAppsView;
     private Workspace mWorkspace;
     private Hotseat mHotseat;
 
     private final Launcher mLauncher;
-    private final SwipeDetector mDetector;
     private final boolean mIsDarkTheme;
 
     // Animation in this class is controlled by a single variable {@link mProgress}.
@@ -91,190 +69,25 @@
 
     // When {@link mProgress} is 0, all apps container is pulled up.
     // When {@link mProgress} is 1, all apps container is pulled down.
-    private float mShiftStart;      // [0, mShiftRange]
     private float mShiftRange;      // changes depending on the orientation
     private float mProgress;        // [0, 1], mShiftRange * mProgress = shiftCurrent
 
-    // Velocity of the container. Unit is in px/ms.
-    private float mContainerVelocity;
-
     private static final float DEFAULT_SHIFT_RANGE = 10;
 
-    private static final float RECATCH_REJECTION_FRACTION = .0875f;
-
-    private long mAnimationDuration;
-
-    private boolean mNoIntercept;
-    private boolean mTouchEventStartedOnHotseat;
-
-    // Used in discovery bounce animation to provide the transition without workspace changing.
     private boolean mIsTranslateWithoutWorkspace = false;
     private Animator mDiscoBounceAnimation;
     private GradientView mGradientView;
 
-    private SpringAnimation mSearchSpring;
-    private SpringAnimationHandler mSpringAnimationHandler;
-
     public AllAppsTransitionController(Launcher l) {
         mLauncher = l;
-        mDetector = new SwipeDetector(l, this, SwipeDetector.VERTICAL);
         mShiftRange = DEFAULT_SHIFT_RANGE;
         mProgress = 1f;
 
         mIsDarkTheme = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark);
     }
 
-    @Override
-    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            mNoIntercept = false;
-            mTouchEventStartedOnHotseat = mLauncher.getDragLayer().isEventOverHotseat(ev);
-            if (!mLauncher.isInState(ALL_APPS) && !mLauncher.isInState(NORMAL)) {
-                mNoIntercept = true;
-            } else if (mLauncher.isInState(ALL_APPS) &&
-                    !mAppsView.shouldContainerScroll(ev)) {
-                mNoIntercept = true;
-            } else if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
-                mNoIntercept = true;
-            } else {
-                // Now figure out which direction scroll events the controller will start
-                // calling the callbacks.
-                int directionsToDetectScroll = 0;
-                boolean ignoreSlopWhenSettling = false;
-
-                if (mDetector.isIdleState()) {
-                    if (mLauncher.isInState(ALL_APPS)) {
-                        directionsToDetectScroll |= SwipeDetector.DIRECTION_NEGATIVE;
-                    } else {
-                        directionsToDetectScroll |= SwipeDetector.DIRECTION_POSITIVE;
-                    }
-                } else {
-                    if (isInDisallowRecatchBottomZone()) {
-                        directionsToDetectScroll |= SwipeDetector.DIRECTION_POSITIVE;
-                    } else if (isInDisallowRecatchTopZone()) {
-                        directionsToDetectScroll |= SwipeDetector.DIRECTION_NEGATIVE;
-                    } else {
-                        directionsToDetectScroll |= SwipeDetector.DIRECTION_BOTH;
-                        ignoreSlopWhenSettling = true;
-                    }
-                }
-                mDetector.setDetectableScrollConditions(directionsToDetectScroll,
-                        ignoreSlopWhenSettling);
-            }
-        }
-
-        if (mNoIntercept) {
-            return false;
-        }
-        mDetector.onTouchEvent(ev);
-        if (mDetector.isSettlingState() && (isInDisallowRecatchBottomZone() || isInDisallowRecatchTopZone())) {
-            return false;
-        }
-        return mDetector.isDraggingOrSettling();
-    }
-
-    @Override
-    public boolean onControllerTouchEvent(MotionEvent ev) {
-        if (hasSpringAnimationHandler()) {
-            mSpringAnimationHandler.addMovement(ev);
-        }
-        return mDetector.onTouchEvent(ev);
-    }
-
-    private boolean isInDisallowRecatchTopZone() {
-        return mProgress < RECATCH_REJECTION_FRACTION;
-    }
-
-    private boolean isInDisallowRecatchBottomZone() {
-        return mProgress > 1 - RECATCH_REJECTION_FRACTION;
-    }
-
-    @Override
-    public void onDragStart(boolean start) {
-        mLauncher.getStateManager().cancelAnimation();
-        cancelDiscoveryAnimation();
-        mShiftStart = mAppsView.getTranslationY();
-        onProgressAnimationStart();
-        if (hasSpringAnimationHandler()) {
-            mSpringAnimationHandler.skipToEnd();
-        }
-    }
-
-    @Override
-    public boolean onDrag(float displacement, float velocity) {
-        if (mAppsView == null) {
-            return false;   // early termination.
-        }
-
-        mContainerVelocity = velocity;
-
-        float shift = Math.min(Math.max(0, mShiftStart + displacement), mShiftRange);
-        setProgress(shift / mShiftRange);
-
-        return true;
-    }
-
-    @Override
-    public void onDragEnd(float velocity, boolean fling) {
-        if (mAppsView == null) {
-            return; // early termination.
-        }
-
-        final int containerType = mTouchEventStartedOnHotseat
-                ? ContainerType.HOTSEAT : ContainerType.WORKSPACE;
-        if (fling) {
-            if (velocity < 0) {
-                calculateDuration(velocity, mAppsView.getTranslationY());
-                if (!mLauncher.isInState(ALL_APPS)) {
-                    logSwipeOnContainer(Touch.FLING, Direction.UP, containerType);
-                }
-                mLauncher.getStateManager().goToState(ALL_APPS);
-                if (hasSpringAnimationHandler()) {
-                    mSpringAnimationHandler.add(mSearchSpring, true /* setDefaultValues */);
-                    // The icons are moving upwards, so we go to 0 from 1. (y-axis 1 is below 0.)
-                    mSpringAnimationHandler.animateToFinalPosition(0 /* pos */, 1 /* startValue */);
-                }
-            } else {
-                calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
-                if (mLauncher.isInState(ALL_APPS)) {
-                    logSwipeOnContainer(Touch.FLING, Direction.DOWN, ContainerType.ALLAPPS);
-                }
-                mLauncher.getStateManager().goToState(NORMAL);
-            }
-            // snap to top or bottom using the release velocity
-        } else {
-            if (mAppsView.getTranslationY() > mShiftRange / 2) {
-                calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY()));
-                if (mLauncher.isInState(ALL_APPS)) {
-                    logSwipeOnContainer(Touch.SWIPE, Direction.DOWN, ContainerType.ALLAPPS);
-                }
-                mLauncher.getStateManager().goToState(NORMAL);
-            } else {
-                calculateDuration(velocity, Math.abs(mAppsView.getTranslationY()));
-                if (!mLauncher.isInState(ALL_APPS)) {
-                    logSwipeOnContainer(Touch.SWIPE, Direction.UP, containerType);
-                }
-                mLauncher.getStateManager().goToState(ALL_APPS);
-            }
-        }
-    }
-
-    /**
-     * Important, make sure that this method is called only when actual launcher state transition
-     * happen and not when user swipes in one direction only to cancel that swipe seconds later.
-     *
-     * @param touchType Swipe or Fling
-     * @param direction Up or Down
-     * @param containerType Workspace or Allapps
-     */
-    private void logSwipeOnContainer(int touchType, int direction, int containerType) {
-        mLauncher.getUserEventDispatcher().logActionOnContainer(
-                touchType, direction, containerType,
-                mLauncher.getWorkspace().getCurrentPage());
-    }
-
-    public boolean isTransitioning() {
-        return mDetector.isDraggingOrSettling();
+    public float getShiftRange() {
+        return mShiftRange;
     }
 
     private void onProgressAnimationStart() {
@@ -310,10 +123,9 @@
      * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace
      *
      * @see #setFinalProgress(float)
-     * @see #animateToFinalProgress(float, boolean, AnimatorSet, AnimationConfig)
+     * @see #animateToFinalProgress(float, AnimatorSet, AnimationConfig)
      */
     public void setProgress(float progress) {
-        float shiftPrevious = mProgress * mShiftRange;
         mProgress = progress;
         float shiftCurrent = progress * mShiftRange;
 
@@ -341,11 +153,6 @@
         mWorkspace.setWorkspaceYTranslationAndAlpha(
                 PARALLAX_COEFFICIENT * (-mShiftRange + shiftCurrent), workspaceAlpha);
 
-        if (!mDetector.isDraggingState()) {
-            mContainerVelocity = mDetector.computeVelocity(shiftCurrent - shiftPrevious,
-                    System.currentTimeMillis());
-        }
-
         updateLightStatusBar(shiftCurrent);
     }
 
@@ -353,10 +160,6 @@
         return mProgress;
     }
 
-    private void calculateDuration(float velocity, float disp) {
-        mAnimationDuration = SwipeDetector.calculateDuration(velocity, disp / mShiftRange);
-    }
-
     /**
      * Sets the vertical transition progress to {@param progress} and updates all the dependent UI
      * accordingly.
@@ -371,34 +174,20 @@
      * 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) {
+    public void animateToFinalProgress(
+            float progress, AnimatorSet animationOut, AnimationConfig outConfig) {
         if (Float.compare(mProgress, progress) == 0) {
             // Fail fast
             onProgressAnimationEnd();
             return;
         }
 
-        outConfig.shouldPost = true;
-        Interpolator interpolator;
-        if (mDetector.isIdleState()) {
-            mAnimationDuration = LauncherAnimUtils.ALL_APPS_TRANSITION_MS;
-            mShiftStart = mAppsView.getTranslationY();
-            interpolator = mFastOutSlowInInterpolator;
-        } else {
-            interpolator = scrollInterpolatorForVelocity(mContainerVelocity);
-            mProgress = Utilities.boundToRange(
-                    mProgress + mContainerVelocity * SINGLE_FRAME_MS / mShiftRange, 0f, 1f);
-            outConfig.shouldPost = false;
-        }
-
-        outConfig.duration = mAnimationDuration;
+        Interpolator interpolator = outConfig.userControlled ? LINEAR : FAST_OUT_SLOW_IN;
         ObjectAnimator anim = ObjectAnimator.ofFloat(this, PROGRESS, mProgress, progress);
-        anim.setDuration(mAnimationDuration);
+        anim.setDuration(outConfig.duration);
         anim.setInterpolator(interpolator);
         anim.addListener(new AnimationSuccessListener() {
             @Override
@@ -413,20 +202,6 @@
         });
 
         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() {
@@ -476,12 +251,6 @@
         mWorkspace = workspace;
         mHotseat.bringToFront();
         mAppsView.getSearchUiManager().addOnScrollRangeChangeListener(this);
-        mSpringAnimationHandler = mAppsView.getSpringAnimationHandler();
-        mSearchSpring = mAppsView.getSearchUiManager().getSpringForFling();
-    }
-
-    private boolean hasSpringAnimationHandler() {
-        return FeatureFlags.LAUNCHER3_PHYSICS && mSpringAnimationHandler != null;
     }
 
     @Override
@@ -503,13 +272,5 @@
             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
index feebc6c..9448632 100644
--- a/src/com/android/launcher3/anim/AnimationSuccessListener.java
+++ b/src/com/android/launcher3/anim/AnimationSuccessListener.java
@@ -24,7 +24,7 @@
  */
 public abstract class AnimationSuccessListener extends AnimatorListenerAdapter {
 
-    private boolean mCancelled = false;
+    protected boolean mCancelled = false;
 
     @Override
     public void onAnimationCancel(Animator animation) {
diff --git a/src/com/android/launcher3/compat/AnimatorSetCompat.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
similarity index 75%
rename from src/com/android/launcher3/compat/AnimatorSetCompat.java
rename to src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 6676725..826a20e 100644
--- a/src/com/android/launcher3/compat/AnimatorSetCompat.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -13,37 +13,32 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.compat;
+package com.android.launcher3.anim;
 
 import android.animation.Animator;
 import android.animation.Animator.AnimatorListener;
 import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
-import android.annotation.TargetApi;
-import android.os.Build;
-
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.AnimationSuccessListener;
-import com.android.launcher3.anim.Interpolators;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
 /**
- * Compat implementation for various new APIs in {@link AnimatorSet}
+ * Helper class to control the playback of an {@link AnimatorSet}, with custom interpolators
+ * and durations.
  *
- * Note: The compat implementation does not support start delays on child animations or
+ * Note: The implementation does not support start delays on child animations or
  * sequential playbacks.
  */
-public abstract class AnimatorSetCompat implements ValueAnimator.AnimatorUpdateListener {
+public abstract class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {
 
-    public static AnimatorSetCompat wrap(AnimatorSet anim, int duration) {
-        if (Utilities.ATLEAST_OREO) {
-            return new AnimatorSetCompatVO(anim, duration);
-        } else {
-            return new AnimatorSetCompatVL(anim, duration);
-        }
+    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
+
+        /**
+         * TODO: use {@link AnimatorSet#setCurrentPlayTime(long)} once b/68382377 is fixed.
+         */
+        return new AnimatorPlaybackControllerVL(anim, duration);
     }
 
     private final ValueAnimator mAnimationPlayer;
@@ -52,23 +47,28 @@
     protected final AnimatorSet mAnim;
 
     protected float mCurrentFraction;
+    private Runnable mEndAction;
 
-    protected AnimatorSetCompat(AnimatorSet anim, int duration) {
+    protected AnimatorPlaybackController(AnimatorSet anim, long duration) {
         mAnim = anim;
         mDuration = duration;
 
         mAnimationPlayer = ValueAnimator.ofFloat(0, 1);
         mAnimationPlayer.setInterpolator(Interpolators.LINEAR);
+        mAnimationPlayer.addListener(new OnAnimationEndDispatcher());
         mAnimationPlayer.addUpdateListener(this);
     }
 
+    public AnimatorSet getTarget() {
+        return mAnim;
+    }
+
     /**
      * Starts playing the animation forward from current position.
      */
     public void start() {
         mAnimationPlayer.setFloatValues(mCurrentFraction, 1);
         mAnimationPlayer.setDuration(clampDuration(1 - mCurrentFraction));
-        mAnimationPlayer.addListener(new OnAnimationEndDispatcher());
         mAnimationPlayer.start();
     }
 
@@ -78,20 +78,38 @@
     public void reverse() {
         mAnimationPlayer.setFloatValues(mCurrentFraction, 0);
         mAnimationPlayer.setDuration(clampDuration(mCurrentFraction));
-        mAnimationPlayer.addListener(new OnAnimationEndDispatcher());
         mAnimationPlayer.start();
     }
 
     /**
+     * Pauses the currently playing animation.
+     */
+    public void pause() {
+        mAnimationPlayer.cancel();
+    }
+
+    /**
+     * Returns the underlying animation used for controlling the set.
+     */
+    public ValueAnimator getAnimationPlayer() {
+        return mAnimationPlayer;
+    }
+
+    /**
      * Sets the current animation position and updates all the child animators accordingly.
      */
     public abstract void setPlayFraction(float fraction);
 
+    public float getProgressFraction() {
+        return mCurrentFraction;
+    }
+
     /**
-     * @see Animator#addListener(AnimatorListener)
+     * Sets the action to be called when the animation is completed. Also clears any
+     * previously set action.
      */
-    public void addListener(Animator.AnimatorListener listener) {
-        mAnimationPlayer.addListener(listener);
+    public void setEndAction(Runnable runnable) {
+        mEndAction = runnable;
     }
 
     @Override
@@ -124,11 +142,11 @@
         }
     }
 
-    public static class AnimatorSetCompatVL extends AnimatorSetCompat {
+    public static class AnimatorPlaybackControllerVL extends AnimatorPlaybackController {
 
         private final ValueAnimator[] mChildAnimations;
 
-        private AnimatorSetCompatVL(AnimatorSet anim, int duration) {
+        private AnimatorPlaybackControllerVL(AnimatorSet anim, long duration) {
             super(anim, duration);
 
             // Build animation list
@@ -164,25 +182,19 @@
 
     }
 
-    @TargetApi(Build.VERSION_CODES.O)
-    private static class AnimatorSetCompatVO extends AnimatorSetCompat {
-
-        private AnimatorSetCompatVO(AnimatorSet anim, int duration) {
-            super(anim, duration);
-        }
+    private class OnAnimationEndDispatcher extends AnimationSuccessListener {
 
         @Override
-        public void setPlayFraction(float fraction) {
-            mCurrentFraction = fraction;
-            mAnim.setCurrentPlayTime(clampDuration(fraction));
+        public void onAnimationStart(Animator animation) {
+            mCancelled = false;
         }
-    }
-
-    private class OnAnimationEndDispatcher extends AnimationSuccessListener {
 
         @Override
         public void onAnimationSuccess(Animator animator) {
             dispatchOnEndRecursively(mAnim);
+            if (mEndAction != null) {
+                mEndAction.run();
+            }
         }
 
         private void dispatchOnEndRecursively(Animator animator) {
diff --git a/src/com/android/launcher3/anim/SpringAnimationHandler.java b/src/com/android/launcher3/anim/SpringAnimationHandler.java
index eec3a48..29a2430 100644
--- a/src/com/android/launcher3/anim/SpringAnimationHandler.java
+++ b/src/com/android/launcher3/anim/SpringAnimationHandler.java
@@ -83,6 +83,10 @@
         mAnimations.add(spring);
     }
 
+    public AnimationFactory<T> getFactory() {
+        return mAnimationFactory;
+    }
+
     /**
      * Adds a new or recycled animation to the list of springs handled by this class.
      *
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 5b1a4dc..0f0b20d 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.VerticalSwipeController;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.config.FeatureFlags;
@@ -96,6 +97,7 @@
 
     // Handles all apps pull up interaction
     private AllAppsTransitionController mAllAppsController;
+    private VerticalSwipeController mVerticalSwipeController;
 
     private TouchController mActiveController;
     /**
@@ -121,6 +123,7 @@
         mLauncher = launcher;
         mDragController = dragController;
         mAllAppsController = allAppsTransitionController;
+        mVerticalSwipeController = new VerticalSwipeController(mLauncher);
 
         boolean isAccessibilityEnabled = ((AccessibilityManager) mLauncher.getSystemService(
                 Context.ACCESSIBILITY_SERVICE)).isEnabled();
@@ -191,8 +194,8 @@
             return true;
         }
 
-        if (mAllAppsController.onControllerInterceptTouchEvent(ev)) {
-            mActiveController = mAllAppsController;
+        if (mVerticalSwipeController.onControllerInterceptTouchEvent(ev)) {
+            mActiveController = mVerticalSwipeController;
             return true;
         }
 
diff --git a/src/com/android/launcher3/states/AllAppsState.java b/src/com/android/launcher3/states/AllAppsState.java
index ee35b4d..ed3023a 100644
--- a/src/com/android/launcher3/states/AllAppsState.java
+++ b/src/com/android/launcher3/states/AllAppsState.java
@@ -32,7 +32,7 @@
 
     public static final String APPS_VIEW_SHOWN = "launcher.apps_view_shown";
 
-    private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY | FLAG_HAS_SPRING;
+    private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY;
 
     public AllAppsState(int id) {
         super(id, ContainerType.ALLAPPS, ALL_APPS_TRANSITION_MS, 0f, STATE_FLAGS);