Add atomic recents animation while swiping up

State handlers can now specify atomic and non-atomic components of
their animations to states, which can be specified when creating a
new animation. There is now one atomic animation, when going from
NORMAL to OVERVIEW (and in reverse):
- RecentsViewStateController's animation (scale/alpha) is all atomic
- WorkspaceStateTransitionAnimation has atomic and non-atomic:
  - Hotseat and workspace alpha is atomic, as is workspace scale
  - Everything else (scrim, translation, qsb and drag handle alpha) is
    non-atomic
- All apps progress is non-atomic

Also simplified dragging through overview; no longer pulls against you,
so we use an OvershootInterpolator when flinging instead of our custom
interpolator for the spring effect.

Bug: 76449024
Bug: 78089840
Change-Id: Iafac84d0c2b99ee9cf9dd5b30e2218286713b449
diff --git a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
index e31805c..b0f8d99 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
@@ -84,7 +84,7 @@
 
     @Override
     public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) {
-        return new float[] {1f, -0.2f};
+        return new float[] {0.9f, -0.2f};
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java
index 42f6c74..ce8192f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java
@@ -9,6 +9,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager.AnimationComponents;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -56,11 +57,11 @@
     }
 
     @Override
-    protected float initCurrentAnimation() {
+    protected float initCurrentAnimation(@AnimationComponents int animComponent) {
         float range = getShiftRange();
         long maxAccuracy = (long) (2 * range);
-        mCurrentAnimation = mLauncher.getStateManager()
-                .createAnimationToNewWorkspace(mToState, maxAccuracy);
+        mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState,
+                maxAccuracy, animComponent);
         return (mLauncher.getDeviceProfile().isSeascape() ? 2 : -2) / range;
     }
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index 91e1e7f..eaf8aa0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.Workspace;
 import com.android.launcher3.allapps.DiscoveryBounce;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.quickstep.views.RecentsView;
@@ -47,8 +48,12 @@
 
     @Override
     public float[] getWorkspaceScaleAndTranslation(Launcher launcher) {
-        // TODO: provide a valid value
-        return new float[]{1, 0, -launcher.getDeviceProfile().hotseatBarSizePx / 2};
+        RecentsView recentsView = launcher.getOverviewPanel();
+        Workspace workspace = launcher.getWorkspace();
+        recentsView.getTaskSize(sTempRect);
+        float scale = (float) sTempRect.width() / workspace.getWidth();
+        float parallaxFactor = 0.4f;
+        return new float[]{scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor};
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
index 2f0bdc6..514c0e8 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
@@ -18,20 +18,20 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATION;
 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS;
-import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager.AnimationComponents;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.Interpolators;
@@ -52,43 +52,11 @@
 
     private static final String TAG = "PortraitStatesTouchCtrl";
 
-    private static final float TOTAL_DISTANCE_MULTIPLIER = 3f;
-    private static final float LINEAR_SCALE_LIMIT = 1 / TOTAL_DISTANCE_MULTIPLIER;
-
-    // Must be greater than LINEAR_SCALE_LIMIT;
-    private static final float MAXIMUM_DISTANCE_FACTOR = 0.9f;
-
-    // Maximum amount to overshoot.
-    private static final float MAX_OVERSHOOT = 0.3f;
-
-    private static final double PI_BY_2 = Math.PI / 2;
-
     private InterpolatorWrapper mAllAppsInterpolatorWrapper = new InterpolatorWrapper();
 
     // If true, we will finish the current animation instantly on second touch.
     private boolean mFinishFastOnSecondTouch;
 
-    private final Interpolator mAllAppsDampedInterpolator = new Interpolator() {
-
-        private final double mAngleMultiplier = Math.PI /
-                (2 * (MAXIMUM_DISTANCE_FACTOR - LINEAR_SCALE_LIMIT));
-
-        @Override
-        public float getInterpolation(float v) {
-            if (v <= LINEAR_SCALE_LIMIT) {
-                return v * TOTAL_DISTANCE_MULTIPLIER;
-            }
-            float overshoot = (v - LINEAR_SCALE_LIMIT);
-            return (float) (1 + MAX_OVERSHOOT * Math.sin(overshoot * mAngleMultiplier));
-        }
-    };
-
-    private final Interpolator mOverviewBoundInterpolator = (v) -> {
-            if (v >= MAXIMUM_DISTANCE_FACTOR) {
-                return 1;
-            }
-            return FAST_OUT_SLOW_IN.getInterpolation(v / MAXIMUM_DISTANCE_FACTOR);
-    };
 
     public PortraitStatesTouchController(Launcher l) {
         super(l, SwipeDetector.VERTICAL);
@@ -144,17 +112,16 @@
     }
 
     private AnimatorSetBuilder getNormalToOverviewAnimation() {
-        mAllAppsInterpolatorWrapper.baseInterpolator = mAllAppsDampedInterpolator;
+        mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR;
 
         AnimatorSetBuilder builder = new AnimatorSetBuilder();
         builder.setInterpolator(ANIM_VERTICAL_PROGRESS, mAllAppsInterpolatorWrapper);
 
-        builder.setInterpolator(ANIM_OVERVIEW_TRANSLATION, mOverviewBoundInterpolator);
         return builder;
     }
 
     @Override
-    protected float initCurrentAnimation() {
+    protected float initCurrentAnimation(@AnimationComponents int animComponents) {
         float range = getShiftRange();
         long maxAccuracy = (long) (2 * range);
 
@@ -167,7 +134,6 @@
 
         if (mFromState == NORMAL && mToState == OVERVIEW && totalShift != 0) {
             builder = getNormalToOverviewAnimation();
-            totalShift = totalShift * TOTAL_DISTANCE_MULTIPLIER;
         } else {
             builder = new AnimatorSetBuilder();
         }
@@ -190,7 +156,8 @@
             mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation);
         } else {
             mCurrentAnimation = mLauncher.getStateManager()
-                    .createAnimationToNewWorkspace(mToState, builder, maxAccuracy, this::clearState);
+                    .createAnimationToNewWorkspace(mToState, builder, maxAccuracy, this::clearState,
+                            animComponents);
         }
 
         if (totalShift == 0) {
@@ -210,9 +177,9 @@
     @Override
     protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
             LauncherState targetState, float velocity, boolean isFling) {
-        handleFirstSwipeToOverview(animator, expectedDuration, targetState, velocity, isFling);
         super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState,
                 velocity, isFling);
+        handleFirstSwipeToOverview(animator, expectedDuration, targetState, velocity, isFling);
     }
 
     private void handleFirstSwipeToOverview(final ValueAnimator animator,
@@ -220,62 +187,22 @@
             final boolean isFling) {
         if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) {
             mFinishFastOnSecondTouch = true;
-
-            // Update all apps interpolator
-            float currentFraction = mCurrentAnimation.getProgressFraction();
-            float absVelocity = Math.abs(velocity);
-            float currentValue = mAllAppsDampedInterpolator.getInterpolation(currentFraction);
-
-            if (isFling && absVelocity > 1 && currentFraction < LINEAR_SCALE_LIMIT) {
-
-                // TODO: Clean up these magic calculations
-                // Linearly interpolate the max value based on the velocity.
-                float maxValue = Math.max(absVelocity > 4 ? 1 + MAX_OVERSHOOT :
-                                1 + (absVelocity - 1) * MAX_OVERSHOOT / 3,
-                        currentValue);
-                double angleToPeak = PI_BY_2 - Math.asin(currentValue / maxValue);
-
-                if (expectedDuration != 0 && angleToPeak != 0) {
-
-                    float distanceLeft = 1 - currentFraction;
-                    mAllAppsInterpolatorWrapper.baseInterpolator = (f) -> {
-                        float scaledF = (f - currentFraction) / distanceLeft;
-
-                        if (scaledF < 0.5f) {
-                            double angle = PI_BY_2 - angleToPeak + scaledF * angleToPeak / 0.5f;
-                            return (float) (maxValue * Math.sin(angle));
-                        }
-
-                        scaledF = ((scaledF - .5f) / .5f);
-                        double angle = PI_BY_2 + 3 * scaledF * PI_BY_2;
-                        float amplitude = (1 - scaledF) * (1 - scaledF) * (maxValue - 1);
-                        return 1 + (float) (amplitude * Math.sin(angle));
-                    };
-
-                    animator.setDuration(expectedDuration).setInterpolator(LINEAR);
-                    return;
-                }
+            if (isFling && expectedDuration != 0) {
+                // Update all apps interpolator to add a bit of overshoot starting from currFraction
+                final float currFraction = mCurrentAnimation.getProgressFraction();
+                mAllAppsInterpolatorWrapper.baseInterpolator
+                        = new OvershootInterpolator(Math.min(Math.abs(velocity) / 3, 3f)) {
+                    @Override
+                    public float getInterpolation(float t) {
+                        return super.getInterpolation(t) + ((1 - t) * currFraction);
+                    }
+                };
+                animator.setFloatValues(0, 1);
+                animator.setDuration(Math.max(expectedDuration, 300)).setInterpolator(LINEAR);
             }
-
-            if (currentFraction < LINEAR_SCALE_LIMIT) {
-                mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR;
-                return;
-            }
-            float extraValue = mAllAppsDampedInterpolator.getInterpolation(currentFraction) - 1;
-            float distanceLeft = 1 - currentFraction;
-
-            animator.setFloatValues(currentFraction, 1);
-            mAllAppsInterpolatorWrapper.baseInterpolator = (f) -> {
-                float scaledF = (f - currentFraction) / distanceLeft;
-
-                double angle = scaledF * 1.5 * Math.PI;
-                float amplitude = (1 - scaledF) * (1 - scaledF) * extraValue;
-                return 1 + (float) (amplitude * Math.sin(angle));
-            };
-            animator.setDuration(200).setInterpolator(LINEAR);
-            return;
+        } else {
+            mFinishFastOnSecondTouch = false;
         }
-        mFinishFastOnSecondTouch = false;
     }
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 2579bc2..febe360 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -15,7 +15,6 @@
  */
 package com.android.launcher3.uioverrides;
 
-import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATION;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.quickstep.views.LauncherRecentsView.TRANSLATION_Y_FACTOR;
@@ -63,12 +62,14 @@
     @Override
     public void setStateWithAnimation(final LauncherState toState,
             AnimatorSetBuilder builder, AnimationConfig config) {
+        if (!config.playAtomicComponent()) {
+            // The entire recents animation is played atomically.
+            return;
+        }
         PropertySetter setter = config.getPropertySetter(builder);
         float[] scaleTranslationYFactor = toState.getOverviewScaleAndTranslationYFactor(mLauncher);
-        setter.setFloat(mRecentsView, ADJACENT_SCALE, scaleTranslationYFactor[0],
-                builder.getInterpolator(ANIM_OVERVIEW_TRANSLATION, LINEAR));
-        setter.setFloat(mRecentsView, TRANSLATION_Y_FACTOR, scaleTranslationYFactor[1],
-                builder.getInterpolator(ANIM_OVERVIEW_TRANSLATION, LINEAR));
+        setter.setFloat(mRecentsView, ADJACENT_SCALE, scaleTranslationYFactor[0], LINEAR);
+        setter.setFloat(mRecentsView, TRANSLATION_Y_FACTOR, scaleTranslationYFactor[1], LINEAR);
         setter.setFloat(mRecentsViewContainer, CONTENT_ALPHA, toState.overviewUi ? 1 : 0,
                 AGGRESSIVE_EASE_IN_OUT);
 
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index f548095..6790b7f 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -21,6 +21,7 @@
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.states.RotationHelper.REQUEST_NONE;
 
+import android.graphics.Rect;
 import android.view.View;
 import android.view.animation.Interpolator;
 
@@ -86,6 +87,8 @@
     public static final LauncherState FAST_OVERVIEW = new FastOverviewState(3);
     public static final LauncherState ALL_APPS = new AllAppsState(4);
 
+    protected static final Rect sTempRect = new Rect();
+
     public final int ordinal;
 
     /**
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index 1b9ac21..d04401e 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -24,6 +24,7 @@
 import android.animation.AnimatorSet;
 import android.os.Handler;
 import android.os.Looper;
+import android.support.annotation.IntDef;
 import android.view.View;
 
 import com.android.launcher3.anim.AnimationSuccessListener;
@@ -33,6 +34,8 @@
 import com.android.launcher3.anim.PropertySetter.AnimatedPropertySetter;
 import com.android.launcher3.uioverrides.UiFactory;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 
 /**
@@ -80,6 +83,21 @@
 
     public static final String TAG = "StateManager";
 
+    // We separate the state animations into "atomic" and "non-atomic" components. The atomic
+    // components may be run atomically - that is, all at once, instead of user-controlled. However,
+    // atomic components are not restricted to this purpose; they can be user-controlled alongside
+    // non atomic components as well.
+    @IntDef(flag = true, value = {
+            NON_ATOMIC_COMPONENT,
+            ATOMIC_COMPONENT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AnimationComponents {}
+    public static final int NON_ATOMIC_COMPONENT = 1 << 0;
+    public static final int ATOMIC_COMPONENT = 1 << 1;
+
+    public static final int ANIM_ALL = NON_ATOMIC_COMPONENT | ATOMIC_COMPONENT;
+
     private final AnimationConfig mConfig = new AnimationConfig();
     private final Handler mUiHandler;
     private final Launcher mLauncher;
@@ -238,13 +256,21 @@
      */
     public AnimatorPlaybackController createAnimationToNewWorkspace(
             LauncherState state, long duration) {
-        return createAnimationToNewWorkspace(state, new AnimatorSetBuilder(), duration, null);
+        return createAnimationToNewWorkspace(state, duration, LauncherStateManager.ANIM_ALL);
+    }
+
+    public AnimatorPlaybackController createAnimationToNewWorkspace(
+            LauncherState state, long duration, @AnimationComponents int animComponents) {
+        return createAnimationToNewWorkspace(state, new AnimatorSetBuilder(), duration, null,
+                animComponents);
     }
 
     public AnimatorPlaybackController createAnimationToNewWorkspace(LauncherState state,
-            AnimatorSetBuilder builder, long duration, Runnable onCancelRunnable) {
+            AnimatorSetBuilder builder, long duration, Runnable onCancelRunnable,
+            @AnimationComponents int animComponents) {
         mConfig.reset();
         mConfig.userControlled = true;
+        mConfig.animComponents = animComponents;
         mConfig.duration = duration;
         mConfig.playbackController = AnimatorPlaybackController.wrap(
                 createAnimationToNewWorkspaceInternal(state, builder, null), duration,
@@ -425,6 +451,7 @@
         public long duration;
         public boolean userControlled;
         public AnimatorPlaybackController playbackController;
+        public @AnimationComponents int animComponents = ANIM_ALL;
         private PropertySetter mPropertySetter;
 
         private AnimatorSet mCurrentAnimation;
@@ -436,6 +463,7 @@
         public void reset() {
             duration = 0;
             userControlled = false;
+            animComponents = ANIM_ALL;
             mPropertySetter = null;
             mTargetState = null;
 
@@ -471,6 +499,14 @@
             mTargetState = targetState;
             mCurrentAnimation.addListener(this);
         }
+
+        public boolean playAtomicComponent() {
+            return (animComponents & ATOMIC_COMPONENT) != 0;
+        }
+
+        public boolean playNonAtomicComponent() {
+            return (animComponents & NON_ATOMIC_COMPONENT) != 0;
+        }
     }
 
     public interface StateHandler {
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index bd7e6eb..720c574 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -82,7 +82,6 @@
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
 import com.android.launcher3.touch.ItemLongClickListener;
 import com.android.launcher3.touch.WorkspaceTouchListener;
-import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 8d3d459..55968db 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
 
 import android.view.View;
+import android.view.animation.Interpolator;
 
 import com.android.launcher3.LauncherState.PageAlphaProvider;
 import com.android.launcher3.LauncherStateManager.AnimationConfig;
@@ -48,12 +49,12 @@
     }
 
     public void setState(LauncherState toState) {
-        setWorkspaceProperty(toState, NO_ANIM_PROPERTY_SETTER);
+        setWorkspaceProperty(toState, NO_ANIM_PROPERTY_SETTER, new AnimationConfig());
     }
 
     public void setStateWithAnimation(LauncherState toState, AnimatorSetBuilder builder,
             AnimationConfig config) {
-        setWorkspaceProperty(toState, config.getPropertySetter(builder));
+        setWorkspaceProperty(toState, config.getPropertySetter(builder), config);
     }
 
     public float getFinalScale() {
@@ -63,28 +64,40 @@
     /**
      * Starts a transition animation for the workspace.
      */
-    private void setWorkspaceProperty(LauncherState state, PropertySetter propertySetter) {
+    private void setWorkspaceProperty(LauncherState state, PropertySetter propertySetter,
+            AnimationConfig config) {
         float[] scaleAndTranslation = state.getWorkspaceScaleAndTranslation(mLauncher);
         mNewScale = scaleAndTranslation[0];
         PageAlphaProvider pageAlphaProvider = state.getWorkspacePageAlphaProvider(mLauncher);
         final int childCount = mWorkspace.getChildCount();
         for (int i = 0; i < childCount; i++) {
             applyChildState(state, (CellLayout) mWorkspace.getChildAt(i), i, pageAlphaProvider,
-                    propertySetter);
+                    propertySetter, config);
         }
 
-        propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, Interpolators.ZOOM_OUT);
-        propertySetter.setFloat(mWorkspace, View.TRANSLATION_X,
-                scaleAndTranslation[1], Interpolators.ZOOM_OUT);
-        propertySetter.setFloat(mWorkspace, View.TRANSLATION_Y,
-                scaleAndTranslation[2], Interpolators.ZOOM_OUT);
 
         int elements = state.getVisibleElements(mLauncher);
-        float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
-        propertySetter.setViewAlpha(mLauncher.getHotseat().getLayout(), hotseatIconsAlpha,
-                pageAlphaProvider.interpolator);
-        propertySetter.setViewAlpha(mLauncher.getWorkspace().getPageIndicator(),
-                hotseatIconsAlpha, pageAlphaProvider.interpolator);
+        boolean playAtomicComponent = config.playAtomicComponent();
+        if (playAtomicComponent) {
+            propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, Interpolators.ZOOM_OUT);
+            float hotseatIconsAlpha = (elements & HOTSEAT_ICONS) != 0 ? 1 : 0;
+            propertySetter.setViewAlpha(mLauncher.getHotseat().getLayout(), hotseatIconsAlpha,
+                    pageAlphaProvider.interpolator);
+            propertySetter.setViewAlpha(mLauncher.getWorkspace().getPageIndicator(),
+                    hotseatIconsAlpha, pageAlphaProvider.interpolator);
+        }
+
+        if (!config.playNonAtomicComponent()) {
+            // Only the alpha and scale, handled above, are included in the atomic animation.
+            return;
+        }
+
+        Interpolator translationInterpolator = !playAtomicComponent ? Interpolators.LINEAR
+                : Interpolators.ZOOM_OUT;
+        propertySetter.setFloat(mWorkspace, View.TRANSLATION_X,
+                scaleAndTranslation[1], translationInterpolator);
+        propertySetter.setFloat(mWorkspace, View.TRANSLATION_Y,
+                scaleAndTranslation[2], translationInterpolator);
 
         propertySetter.setViewAlpha(mLauncher.getHotseatSearchBox(),
                 (elements & HOTSEAT_SEARCH_BOX) != 0 ? 1 : 0,
@@ -101,17 +114,22 @@
 
     public void applyChildState(LauncherState state, CellLayout cl, int childIndex) {
         applyChildState(state, cl, childIndex, state.getWorkspacePageAlphaProvider(mLauncher),
-                NO_ANIM_PROPERTY_SETTER);
+                NO_ANIM_PROPERTY_SETTER, new AnimationConfig());
     }
 
     private void applyChildState(LauncherState state, CellLayout cl, int childIndex,
-            PageAlphaProvider pageAlphaProvider, PropertySetter propertySetter) {
+            PageAlphaProvider pageAlphaProvider, PropertySetter propertySetter,
+            AnimationConfig config) {
         float pageAlpha = pageAlphaProvider.getPageAlpha(childIndex);
         int drawableAlpha = Math.round(pageAlpha * (state.hasWorkspacePageBackground ? 255 : 0));
 
-        propertySetter.setInt(cl.getScrimBackground(),
-                DRAWABLE_ALPHA, drawableAlpha, Interpolators.ZOOM_OUT);
-        propertySetter.setFloat(cl.getShortcutsAndWidgets(), View.ALPHA,
-                pageAlpha, pageAlphaProvider.interpolator);
+        if (config.playNonAtomicComponent()) {
+            propertySetter.setInt(cl.getScrimBackground(),
+                    DRAWABLE_ALPHA, drawableAlpha, Interpolators.ZOOM_OUT);
+        }
+        if (config.playAtomicComponent()) {
+            propertySetter.setFloat(cl.getShortcutsAndWidgets(), View.ALPHA,
+                    pageAlpha, pageAlphaProvider.interpolator);
+        }
     }
 }
\ No newline at end of file
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 6d70a08..4a0b622 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -168,6 +168,11 @@
             return;
         }
 
+        if (!config.playNonAtomicComponent()) {
+            // There is no atomic component for the all apps transition, so just return early.
+            return;
+        }
+
         Interpolator interpolator = config.userControlled ? LINEAR : FAST_OUT_SLOW_IN;
         ObjectAnimator anim =
                 ObjectAnimator.ofFloat(this, ALL_APPS_PROGRESS, mProgress, targetProgress);
diff --git a/src/com/android/launcher3/anim/AnimatorSetBuilder.java b/src/com/android/launcher3/anim/AnimatorSetBuilder.java
index b209a2d..dae2dbc 100644
--- a/src/com/android/launcher3/anim/AnimatorSetBuilder.java
+++ b/src/com/android/launcher3/anim/AnimatorSetBuilder.java
@@ -32,7 +32,6 @@
 public class AnimatorSetBuilder {
 
     public static final int ANIM_VERTICAL_PROGRESS = 0;
-    public static final int ANIM_OVERVIEW_TRANSLATION = 1;
 
     protected final ArrayList<Animator> mAnims = new ArrayList<>();
 
@@ -56,9 +55,9 @@
         AnimatorSet anim = LauncherAnimUtils.createAnimatorSet();
         anim.playTogether(mAnims);
         if (!mOnFinishRunnables.isEmpty()) {
-            anim.addListener(new AnimatorListenerAdapter() {
+            anim.addListener(new AnimationSuccessListener() {
                 @Override
-                public void onAnimationEnd(Animator animation) {
+                public void onAnimationSuccess(Animator animation) {
                     for (Runnable onFinishRunnable : mOnFinishRunnables) {
                         onFinishRunnable.run();
                     }
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index bad4976..977572e 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -18,16 +18,26 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherStateManager.ANIM_ALL;
+import static com.android.launcher3.LauncherStateManager.ATOMIC_COMPONENT;
+import static com.android.launcher3.LauncherStateManager.NON_ATOMIC_COMPONENT;
 import static com.android.launcher3.Utilities.SINGLE_FRAME_MS;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.view.MotionEvent;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager.AnimationComponents;
+import com.android.launcher3.LauncherStateManager.AnimationConfig;
+import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.AnimatorSetBuilder;
 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;
@@ -41,11 +51,17 @@
         implements TouchController, SwipeDetector.Listener {
 
     private static final String TAG = "ASCTouchController";
-    public static final float RECATCH_REJECTION_FRACTION = .0875f;
 
     // Progress after which the transition is assumed to be a success in case user does not fling
     public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
 
+    /**
+     * Play an atomic recents animation when the progress from NORMAL to OVERVIEW reaches this.
+     */
+    public static final float ATOMIC_OVERVIEW_ANIM_THRESHOLD = 0.5f;
+    private static final long ATOMIC_NORMAL_TO_OVERVIEW_DURATION = 120;
+    private static final long ATOMIC_OVERVIEW_TO_NORMAL_DURATION = 200;
+
     protected final Launcher mLauncher;
     protected final SwipeDetector mDetector;
 
@@ -62,6 +78,11 @@
     private float mProgressMultiplier;
     private float mDisplacementShift;
 
+    private AnimatorSet mAtomicAnim;
+    private boolean mPassedOverviewAtomicThreshold;
+    private boolean mCanBlockFling;
+    private boolean mBlockFling;
+
     public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) {
         mLauncher = l;
         mDetector = new SwipeDetector(l, this, dir);
@@ -83,14 +104,8 @@
             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;
-                }
+                directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                ignoreSlopWhenSettling = true;
             } else {
                 directionsToDetectScroll = getSwipeDirection();
                 if (directionsToDetectScroll == 0) {
@@ -138,7 +153,7 @@
     protected abstract LauncherState getTargetState(LauncherState fromState,
             boolean isDragTowardPositive);
 
-    protected abstract float initCurrentAnimation();
+    protected abstract float initCurrentAnimation(@AnimationComponents int animComponents);
 
     /**
      * Returns the container that the touch started from when leaving NORMAL state.
@@ -169,14 +184,28 @@
         mToState = newToState;
 
         mStartProgress = 0;
+        mPassedOverviewAtomicThreshold = false;
+        if (mAtomicAnim != null) {
+            // Most likely the animation is finished by now, but just in case it's not,
+            // make sure the next state animation starts from the expected state.
+            mAtomicAnim.end();
+        }
         if (mCurrentAnimation != null) {
             mCurrentAnimation.setOnCancelRunnable(null);
         }
-        mProgressMultiplier = initCurrentAnimation();
+        int animComponents = goingBetweenNormalAndOverview(mFromState, mToState)
+                ? NON_ATOMIC_COMPONENT : ANIM_ALL;
+        mProgressMultiplier = initCurrentAnimation(animComponents);
         mCurrentAnimation.dispatchOnStart();
         return true;
     }
 
+    private boolean goingBetweenNormalAndOverview(LauncherState fromState, LauncherState toState) {
+        return (fromState == NORMAL || fromState == OVERVIEW)
+                && (toState == NORMAL || toState == OVERVIEW)
+                && mPendingAnimation == null;
+    }
+
     @Override
     public void onDragStart(boolean start) {
         if (mCurrentAnimation == null) {
@@ -187,6 +216,8 @@
             mCurrentAnimation.pause();
             mStartProgress = mCurrentAnimation.getProgressFraction();
         }
+        mCanBlockFling = mFromState == NORMAL;
+        mBlockFling = false;
     }
 
     @Override
@@ -198,17 +229,61 @@
         if (progress <= 0) {
             if (reinitCurrentAnimation(false, isDragTowardPositive)) {
                 mDisplacementShift = displacement;
+                mBlockFling = mCanBlockFling;
             }
         } else if (progress >= 1) {
             if (reinitCurrentAnimation(true, isDragTowardPositive)) {
                 mDisplacementShift = displacement;
+                mBlockFling = mCanBlockFling;
             }
+        } else if (Math.abs(velocity) < SwipeDetector.RELEASE_VELOCITY_PX_MS) {
+            // We prevent flinging after passing a state, but allow it if the user pauses briefly.
+            mBlockFling = false;
         }
+
         return true;
     }
 
     protected void updateProgress(float fraction) {
         mCurrentAnimation.setPlayFraction(fraction);
+        maybeUpdateAtomicAnim(mFromState, mToState, fraction);
+    }
+
+    /**
+     * When going between normal and overview states, see if we passed the overview threshold and
+     * play the appropriate atomic animation if so.
+     */
+    private void maybeUpdateAtomicAnim(LauncherState fromState, LauncherState toState,
+            float progress) {
+        if (!goingBetweenNormalAndOverview(fromState, toState)) {
+            return;
+        }
+        float threshold = toState == OVERVIEW ? ATOMIC_OVERVIEW_ANIM_THRESHOLD
+                : 1f - ATOMIC_OVERVIEW_ANIM_THRESHOLD;
+        boolean passedThreshold = progress >= threshold;
+        if (passedThreshold != mPassedOverviewAtomicThreshold) {
+            LauncherState targetState = passedThreshold ? toState : fromState;
+            mPassedOverviewAtomicThreshold = passedThreshold;
+            if (mAtomicAnim != null) {
+                mAtomicAnim.cancel();
+            }
+            AnimatorSetBuilder builder = new AnimatorSetBuilder();
+            AnimationConfig config = new AnimationConfig();
+            config.animComponents = ATOMIC_COMPONENT;
+            config.duration = targetState == OVERVIEW ? ATOMIC_NORMAL_TO_OVERVIEW_DURATION
+                    : ATOMIC_OVERVIEW_TO_NORMAL_DURATION;
+            for (StateHandler handler : mLauncher.getStateManager().getStateHandlers()) {
+                handler.setStateWithAnimation(targetState, builder, config);
+            }
+            mAtomicAnim = builder.build();
+            mAtomicAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mAtomicAnim = null;
+                }
+            });
+            mAtomicAnim.start();
+        }
     }
 
     @Override
@@ -217,6 +292,11 @@
         final LauncherState targetState;
         final float progress = mCurrentAnimation.getProgressFraction();
 
+        boolean blockedFling = fling && mBlockFling;
+        if (blockedFling) {
+            fling = false;
+        }
+
         if (fling) {
             logAction = Touch.FLING;
             targetState =
@@ -231,6 +311,8 @@
         final float endProgress;
         final float startProgress;
         final long duration;
+        // Increase the duration if we prevented the fling, as we are going against a high velocity.
+        final long durationMultiplier = blockedFling && targetState == mFromState ? 6 : 1;
 
         if (targetState == mToState) {
             endProgress = 1;
@@ -241,7 +323,7 @@
                 startProgress = Utilities.boundToRange(
                         progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f);
                 duration = SwipeDetector.calculateDuration(velocity,
-                        endProgress - Math.max(progress, 0));
+                        endProgress - Math.max(progress, 0)) * durationMultiplier;
             }
         } else {
             // Let the state manager know that the animation didn't go to the target state,
@@ -259,18 +341,35 @@
                 startProgress = Utilities.boundToRange(
                         progress + velocity * SINGLE_FRAME_MS * mProgressMultiplier, 0f, 1f);
                 duration = SwipeDetector.calculateDuration(velocity,
-                        Math.min(progress, 1) - endProgress);
+                        Math.min(progress, 1) - endProgress) * durationMultiplier;
             }
         }
 
         mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction));
         ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
         anim.setFloatValues(startProgress, endProgress);
-        updateSwipeCompleteAnimation(anim, duration, targetState, velocity, fling);
+        maybeUpdateAtomicAnim(mFromState, targetState, targetState == mToState ? 1f : 0f);
+        updateSwipeCompleteAnimation(anim, Math.max(duration, getRemainingAtomicDuration()),
+                targetState, velocity, fling);
         mCurrentAnimation.dispatchOnStart();
         anim.start();
     }
 
+    private long getRemainingAtomicDuration() {
+        if (mAtomicAnim == null) {
+            return 0;
+        }
+        if (Utilities.ATLEAST_OREO) {
+            return mAtomicAnim.getTotalDuration() - mAtomicAnim.getCurrentPlayTime();
+        } else {
+            long remainingDuration = 0;
+            for (Animator anim : mAtomicAnim.getChildAnimations()) {
+                remainingDuration = Math.max(remainingDuration, anim.getDuration());
+            }
+            return remainingDuration;
+        }
+    }
+
     protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration,
             LauncherState targetState, float velocity, boolean isFling) {
         animator.setDuration(expectedDuration)
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
index 860be5f..80c2485 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsSwipeController.java
@@ -8,6 +8,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager.AnimationComponents;
 import com.android.launcher3.touch.AbstractStateChangeTouchController;
 import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -62,11 +63,11 @@
     }
 
     @Override
-    protected float initCurrentAnimation() {
+    protected float initCurrentAnimation(@AnimationComponents int animComponents) {
         float range = getShiftRange();
         long maxAccuracy = (long) (2 * range);
         mCurrentAnimation = mLauncher.getStateManager()
-                .createAnimationToNewWorkspace(mToState, maxAccuracy);
+                .createAnimationToNewWorkspace(mToState, maxAccuracy, animComponents);
         float startVerticalShift = mFromState.getVerticalProgress(mLauncher) * range;
         float endVerticalShift = mToState.getVerticalProgress(mLauncher) * range;
         float totalShift = endVerticalShift - startVerticalShift;