Initial commit of taskbar stashing

- Added StashedHandleViewController to provide properties such as ViewOutlineProvider to animate the handle that's shown in place of taskbar while it's stashed
- Added TaskbarStashController to coordinate the stashed state, including orchestrating the animation across taskbar controllers
- Added TaskbarStashInput consumer to detect long press in the nav region when taskbar is stashed

Behavior:
- Long pressing taskbar background animates to the stashed state by morphing the TaskbarView into the stashed handle view and offsetting the background offscreen
- We persist the stashed state across app launches and reboot; to unstash, long press the stashed handle
- We also visually unstash when going back home

Test: long press tasbkar background when in an app to stash it, long press the resulting stashed handle to unstash; while stashed, swipe up to home to also unstash until launching another app
Bug: 189503603
Change-Id: I698eff785388dff1ef717c76879719d6af236c2d
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index d61a895..dfa17d6 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -15,6 +15,7 @@
 -->
 <com.android.launcher3.taskbar.TaskbarDragLayer
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/taskbar_container"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
@@ -39,6 +40,7 @@
             android:id="@+id/start_nav_buttons"
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
+            android:orientation="horizontal"
             android:paddingLeft="@dimen/taskbar_nav_buttons_spacing"
             android:paddingRight="@dimen/taskbar_nav_buttons_spacing"
             android:gravity="center_vertical"
@@ -54,4 +56,14 @@
             android:layout_gravity="end"/>
     </FrameLayout>
 
+    <View
+        android:id="@+id/stashed_handle"
+        tools:comment1="The actual size and shape will be set as a ViewOutlineProvider at runtime"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        tools:comment2="TODO: Tint dynamically"
+        android:background="?android:attr/textColorPrimary"
+        android:clipToOutline="true"
+        android:layout_gravity="bottom"/>
+
 </com.android.launcher3.taskbar.TaskbarDragLayer>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index d8899a6..4f62b34 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -153,4 +153,7 @@
     <dimen name="taskbar_folder_margin">16dp</dimen>
     <dimen name="taskbar_nav_buttons_spacing">16dp</dimen>
     <dimen name="taskbar_nav_buttons_size">48dp</dimen>
+    <dimen name="taskbar_stashed_size">24dp</dimen>
+    <dimen name="taskbar_stashed_handle_width">220dp</dimen>
+    <dimen name="taskbar_stashed_handle_height">6dp</dimen>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 7d0afe1..c98bc87 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -50,21 +50,23 @@
     private final TaskbarHotseatController mHotseatController;
 
     private final TaskbarActivityContext mContext;
-    final TaskbarDragLayer mTaskbarDragLayer;
-    final TaskbarView mTaskbarView;
+    private final TaskbarDragLayer mTaskbarDragLayer;
+    private final TaskbarView mTaskbarView;
 
     private final AnimatedFloat mIconAlignmentForResumedState =
             new AnimatedFloat(this::onIconAlignmentRatioChanged);
     private final AnimatedFloat mIconAlignmentForGestureState =
             new AnimatedFloat(this::onIconAlignmentRatioChanged);
 
+    // Initialized in init.
+    private TaskbarControllers mControllers;
     private AnimatedFloat mTaskbarBackgroundAlpha;
     private AlphaProperty mIconAlphaForHome;
-    private boolean mIsAnimatingToLauncher;
+    private boolean mIsAnimatingToLauncherViaResume;
+    private boolean mIsAnimatingToLauncherViaGesture;
     private TaskbarKeyguardController mKeyguardController;
 
     private LauncherState mTargetStateOverride = null;
-    private TaskbarControllers mControllers;
 
     public LauncherTaskbarUIController(
             BaseQuickstepLauncher launcher, TaskbarActivityContext context) {
@@ -80,13 +82,14 @@
 
     @Override
     protected void init(TaskbarControllers taskbarControllers) {
-        mTaskbarBackgroundAlpha = taskbarControllers.taskbarDragLayerController
-                .getTaskbarBackgroundAlpha();
-        MultiValueAlpha taskbarIconAlpha = taskbarControllers.taskbarViewController
-                .getTaskbarIconAlpha();
-        mIconAlphaForHome = taskbarIconAlpha.getProperty(ALPHA_INDEX_HOME);
         mControllers = taskbarControllers;
 
+        mTaskbarBackgroundAlpha = mControllers.taskbarDragLayerController
+                .getTaskbarBackgroundAlpha();
+
+        MultiValueAlpha taskbarIconAlpha = mControllers.taskbarViewController.getTaskbarIconAlpha();
+        mIconAlphaForHome = taskbarIconAlpha.getProperty(ALPHA_INDEX_HOME);
+
         mHotseatController.init();
         mLauncher.setTaskbarUIController(this);
         mKeyguardController = taskbarControllers.taskbarKeyguardController;
@@ -109,19 +112,17 @@
 
     @Override
     protected boolean isTaskbarTouchable() {
-        return !mIsAnimatingToLauncher && mTargetStateOverride == null;
+        return !isAnimatingToLauncher() && !mControllers.taskbarStashController.isStashed();
+    }
+
+    private boolean isAnimatingToLauncher() {
+        return mIsAnimatingToLauncherViaResume || mIsAnimatingToLauncherViaGesture;
     }
 
     @Override
     protected void updateContentInsets(Rect outContentInsets) {
-        // TaskbarDragLayer provides insets to other apps based on contentInsets. These
-        // insets should stay consistent even if we expand TaskbarDragLayer's bounds, e.g.
-        // to show a floating view like Folder. Thus, we set the contentInsets to be where
-        // mTaskbarView is, since its position never changes and insets rather than overlays.
-        outContentInsets.left = mTaskbarView.getLeft();
-        outContentInsets.top = mTaskbarView.getTop();
-        outContentInsets.right = mTaskbarDragLayer.getWidth() - mTaskbarView.getRight();
-        outContentInsets.bottom = mTaskbarDragLayer.getHeight() - mTaskbarView.getBottom();
+        int contentHeight = mControllers.taskbarStashController.getContentHeight();
+        outContentInsets.top = mTaskbarDragLayer.getHeight() - contentHeight;
     }
 
     /**
@@ -137,13 +138,20 @@
             }
         }
 
+        long duration = QuickstepTransitionManager.CONTENT_ALPHA_DURATION;
         ObjectAnimator anim = mIconAlignmentForResumedState.animateToValue(
                 getCurrentIconAlignmentRatio(), isResumed ? 1 : 0)
-                .setDuration(QuickstepTransitionManager.CONTENT_ALPHA_DURATION);
+                .setDuration(duration);
 
-        anim.addListener(AnimatorListeners.forEndCallback(() -> mIsAnimatingToLauncher = false));
+        anim.addListener(AnimatorListeners.forEndCallback(
+                () -> mIsAnimatingToLauncherViaResume = false));
         anim.start();
-        mIsAnimatingToLauncher = isResumed;
+        mIsAnimatingToLauncherViaResume = isResumed;
+
+        if (!isResumed) {
+            TaskbarStashController stashController = mControllers.taskbarStashController;
+            stashController.animateToIsStashed(stashController.isStashedInApp(), duration);
+        }
     }
 
     /**
@@ -155,36 +163,48 @@
     public Animator createAnimToLauncher(@NonNull LauncherState toState,
             @NonNull RecentsAnimationCallbacks callbacks,
             long duration) {
+        TaskbarStashController stashController = mControllers.taskbarStashController;
         ObjectAnimator animator = mIconAlignmentForGestureState
-                .animateToValue(mIconAlignmentForGestureState.value, 1)
+                .animateToValue(1)
                 .setDuration(duration);
         animator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
                 mTargetStateOverride = null;
+                animator.removeListener(this);
             }
 
             @Override
             public void onAnimationStart(Animator animation) {
                 mTargetStateOverride = toState;
+                mIsAnimatingToLauncherViaGesture = true;
+                // TODO: base this on launcher state
+                stashController.animateToIsStashed(false, duration);
             }
         });
         callbacks.addListener(new RecentsAnimationListener() {
             @Override
             public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
-                endGestureStateOverride();
+                endGestureStateOverride(true);
             }
 
             @Override
             public void onRecentsAnimationFinished(RecentsAnimationController controller) {
-                endGestureStateOverride();
+                endGestureStateOverride(!controller.getFinishTargetIsLauncher());
             }
 
-            private void endGestureStateOverride() {
+            private void endGestureStateOverride(boolean finishedToApp) {
                 callbacks.removeListener(this);
+                mIsAnimatingToLauncherViaGesture = false;
+
                 mIconAlignmentForGestureState
-                        .animateToValue(mIconAlignmentForGestureState.value, 0)
+                        .animateToValue(0)
                         .start();
+
+                if (finishedToApp) {
+                    // We only need this for the exiting live tile case.
+                    stashController.animateToIsStashed(stashController.isStashedInApp());
+                }
             }
         });
         return animator;
@@ -215,6 +235,11 @@
         }
     }
 
+    @Override
+    public boolean onLongPressToUnstashTaskbar() {
+        return mControllers.taskbarStashController.onLongPressToUnstashTaskbar();
+    }
+
     /**
      * Should be called when one or more items in the Hotseat have changed.
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
new file mode 100644
index 0000000..8c14ff6
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import android.animation.Animator;
+import android.content.res.Resources;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.anim.RevealOutlineAnimation;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.quickstep.AnimatedFloat;
+
+/**
+ * Handles properties/data collection, then passes the results to our stashed handle View to render.
+ */
+public class StashedHandleViewController {
+
+    private final TaskbarActivityContext mActivity;
+    private final View mStashedHandleView;
+    private final int mStashedHandleWidth;
+    private final int mStashedHandleHeight;
+    private final AnimatedFloat mTaskbarStashedHandleAlpha = new AnimatedFloat(
+            this::updateStashedHandleAlpha);
+
+    // Initialized in init.
+    private TaskbarControllers mControllers;
+
+    // The bounds we want to clip to in the settled state when showing the stashed handle.
+    private final Rect mStashedHandleBounds = new Rect();
+    private float mStashedHandleRadius;
+
+    private boolean mIsAtStashedRevealBounds = true;
+
+    public StashedHandleViewController(TaskbarActivityContext activity, View stashedHandleView) {
+        mActivity = activity;
+        mStashedHandleView = stashedHandleView;
+        final Resources resources = mActivity.getResources();
+        mStashedHandleWidth = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width);
+        mStashedHandleHeight = resources.getDimensionPixelSize(
+                R.dimen.taskbar_stashed_handle_height);
+    }
+
+    public void init(TaskbarControllers controllers) {
+        mControllers = controllers;
+        mStashedHandleView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize;
+
+        updateStashedHandleAlpha();
+
+        final int stashedTaskbarHeight = mControllers.taskbarStashController.getStashedHeight();
+        mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                final int stashedCenterX = view.getWidth() / 2;
+                final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
+                mStashedHandleBounds.set(
+                        stashedCenterX - mStashedHandleWidth / 2,
+                        stashedCenterY - mStashedHandleHeight / 2,
+                        stashedCenterX + mStashedHandleWidth / 2,
+                        stashedCenterY + mStashedHandleHeight / 2);
+                mStashedHandleRadius = view.getHeight() / 2f;
+                outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius);
+            }
+        });
+    }
+
+    public AnimatedFloat getStashedHandleAlpha() {
+        return mTaskbarStashedHandleAlpha;
+    }
+
+    /**
+     * Creates and returns a {@link RevealOutlineAnimation} Animator that updates the stashed handle
+     * shape and size. When stashed, the shape is a thin rounded pill. When unstashed, the shape
+     * morphs into the size of where the taskbar icons will be.
+     */
+    public @Nullable Animator createRevealAnimToIsStashed(boolean isStashed) {
+        if (mIsAtStashedRevealBounds == isStashed) {
+            return null;
+        }
+        mIsAtStashedRevealBounds = isStashed;
+        final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
+                mStashedHandleRadius, mStashedHandleRadius,
+                mControllers.taskbarViewController.getIconLayoutBounds(), mStashedHandleBounds);
+        return handleRevealProvider.createRevealAnimator(mStashedHandleView, !isStashed);
+    }
+
+    protected void updateStashedHandleAlpha() {
+        mStashedHandleView.setAlpha(mTaskbarStashedHandleAlpha.value);
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 4142610..f4703d3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -115,6 +115,7 @@
                 R.layout.taskbar, null, false);
         TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view);
         FrameLayout navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
+        View stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle);
 
         // Construct controllers.
         mControllers = new TaskbarControllers(this,
@@ -125,7 +126,9 @@
                         R.color.popup_color_primary_light),
                 new TaskbarDragLayerController(this, mDragLayer),
                 new TaskbarViewController(this, taskbarView),
-                new TaskbarKeyguardController(this));
+                new TaskbarKeyguardController(this),
+                new StashedHandleViewController(this, stashedHandleView),
+                new TaskbarStashController(this));
 
         Display display = windowContext.getDisplay();
         Context c = display.getDisplayId() == Display.DEFAULT_DISPLAY
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index c48c28b..8279a47 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -32,6 +32,8 @@
     public final TaskbarDragLayerController taskbarDragLayerController;
     public final TaskbarViewController taskbarViewController;
     public final TaskbarKeyguardController taskbarKeyguardController;
+    public final StashedHandleViewController stashedHandleViewController;
+    public final TaskbarStashController taskbarStashController;
 
     /** Do not store this controller, as it may change at runtime. */
     @NonNull public TaskbarUIController uiController = TaskbarUIController.DEFAULT;
@@ -43,7 +45,9 @@
             RotationButtonController rotationButtonController,
             TaskbarDragLayerController taskbarDragLayerController,
             TaskbarViewController taskbarViewController,
-            TaskbarKeyguardController taskbarKeyguardController) {
+            TaskbarKeyguardController taskbarKeyguardController,
+            StashedHandleViewController stashedHandleViewController,
+            TaskbarStashController taskbarStashController) {
         this.taskbarActivityContext = taskbarActivityContext;
         this.taskbarDragController = taskbarDragController;
         this.navButtonController = navButtonController;
@@ -52,6 +56,8 @@
         this.taskbarDragLayerController = taskbarDragLayerController;
         this.taskbarViewController = taskbarViewController;
         this.taskbarKeyguardController = taskbarKeyguardController;
+        this.stashedHandleViewController = stashedHandleViewController;
+        this.taskbarStashController = taskbarStashController;
     }
 
     /**
@@ -67,6 +73,8 @@
         taskbarDragLayerController.init(this);
         taskbarViewController.init(this);
         taskbarKeyguardController.init(navbarButtonsViewController);
+        stashedHandleViewController.init(this);
+        taskbarStashController.init(this);
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index ac121ab..cd1baf7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -40,10 +40,11 @@
 public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> {
 
     private final Paint mTaskbarBackgroundPaint;
+    private final OnComputeInsetsListener mTaskbarInsetsComputer = this::onComputeTaskbarInsets;
 
     private TaskbarDragLayerController.TaskbarDragLayerCallbacks mControllerCallbacks;
 
-    private final OnComputeInsetsListener mTaskbarInsetsComputer = this::onComputeTaskbarInsets;
+    private float mTaskbarBackgroundOffset;
 
     public TaskbarDragLayer(@NonNull Context context) {
         this(context, null);
@@ -118,8 +119,10 @@
 
     @Override
     protected void dispatchDraw(Canvas canvas) {
-        canvas.drawRect(0, canvas.getHeight() - mControllerCallbacks.getTaskbarBackgroundHeight(),
-                canvas.getWidth(), canvas.getHeight(), mTaskbarBackgroundPaint);
+        float backgroundHeight = mControllerCallbacks.getTaskbarBackgroundHeight()
+                * (1f - mTaskbarBackgroundOffset);
+        canvas.drawRect(0, canvas.getHeight() - backgroundHeight, canvas.getWidth(),
+                canvas.getHeight(), mTaskbarBackgroundPaint);
         super.dispatchDraw(canvas);
     }
 
@@ -132,6 +135,15 @@
         invalidate();
     }
 
+    /**
+     * Sets the translation of the background color behind all the Taskbar contents.
+     * @param offset 0 is fully onscreen, 1 is fully offscreen.
+     */
+    protected void setTaskbarBackgroundOffset(float offset) {
+        mTaskbarBackgroundOffset = offset;
+        invalidate();
+    }
+
     @Override
     public boolean dispatchTouchEvent(MotionEvent ev) {
         TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index db5c387..e15e9ff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -39,6 +39,8 @@
     // Alpha properties for taskbar background.
     private final AnimatedFloat mBgTaskbar = new AnimatedFloat(this::updateBackgroundAlpha);
     private final AnimatedFloat mBgNavbar = new AnimatedFloat(this::updateBackgroundAlpha);
+    // Translation property for taskbar background.
+    private final AnimatedFloat mBgOffset = new AnimatedFloat(this::updateBackgroundOffset);
 
     // Initialized in init.
     private TaskbarControllers mControllers;
@@ -78,10 +80,18 @@
         return mBgNavbar;
     }
 
+    public AnimatedFloat getTaskbarBackgroundOffset() {
+        return mBgOffset;
+    }
+
     private void updateBackgroundAlpha() {
         mTaskbarDragLayer.setTaskbarBackgroundAlpha(Math.max(mBgNavbar.value, mBgTaskbar.value));
     }
 
+    private void updateBackgroundOffset() {
+        mTaskbarDragLayer.setTaskbarBackgroundOffset(mBgOffset.value);
+    }
+
     /**
      * Callbacks for {@link TaskbarDragLayer} to interact with its controller.
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
new file mode 100644
index 0000000..57600d7
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2021 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.taskbar;
+
+import static android.view.HapticFeedbackConstants.LONG_PRESS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.annotation.Nullable;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.quickstep.AnimatedFloat;
+
+/**
+ * Coordinates between controllers such as TaskbarViewController and StashedHandleViewController to
+ * create a cohesive animation between stashed/unstashed states.
+ */
+public class TaskbarStashController {
+
+    /**
+     * How long to stash/unstash when manually invoked via long press.
+     */
+    private static final long TASKBAR_STASH_DURATION = 300;
+
+    /**
+     * The scale TaskbarView animates to when being stashed.
+     */
+    private static final float STASHED_TASKBAR_SCALE = 0.5f;
+
+    /**
+     * The SharedPreferences key for whether user has manually stashed the taskbar.
+     */
+    private static final String SHARED_PREFS_STASHED_KEY = "taskbar_is_stashed";
+
+    /**
+     * Whether taskbar should be stashed out of the box.
+     */
+    private static final boolean DEFAULT_STASHED_PREF = false;
+
+    private final TaskbarActivityContext mActivity;
+    private final SharedPreferences mPrefs;
+    private final int mStashedHeight;
+    private final int mUnstashedHeight;
+
+    // Initialized in init.
+    private TaskbarControllers mControllers;
+    // Taskbar background properties.
+    private AnimatedFloat mTaskbarBackgroundOffset;
+    // TaskbarView icon properties.
+    private AlphaProperty mIconAlphaForStash;
+    private AnimatedFloat mIconScaleForStash;
+    private AnimatedFloat mIconTranslationYForStash;
+    // Stashed handle properties.
+    private AnimatedFloat mTaskbarStashedHandleAlpha;
+
+    /** Whether the user has manually invoked taskbar stashing, which we persist. */
+    private boolean mIsStashedInApp;
+    /** Whether we are currently visually stashed (might change based on launcher state). */
+    private boolean mIsStashed = false;
+
+    private @Nullable AnimatorSet mAnimator;
+
+    public TaskbarStashController(TaskbarActivityContext activity) {
+        mActivity = activity;
+        mPrefs = Utilities.getPrefs(mActivity);
+        final Resources resources = mActivity.getResources();
+        mStashedHeight = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size);
+        mUnstashedHeight = mActivity.getDeviceProfile().taskbarSize;
+    }
+
+    public void init(TaskbarControllers controllers) {
+        mControllers = controllers;
+
+        TaskbarDragLayerController dragLayerController = controllers.taskbarDragLayerController;
+        mTaskbarBackgroundOffset = dragLayerController.getTaskbarBackgroundOffset();
+
+        TaskbarViewController taskbarViewController = controllers.taskbarViewController;
+        mIconAlphaForStash = taskbarViewController.getTaskbarIconAlpha().getProperty(
+                TaskbarViewController.ALPHA_INDEX_STASH);
+        mIconScaleForStash = taskbarViewController.getTaskbarIconScaleForStash();
+        mIconTranslationYForStash = taskbarViewController.getTaskbarIconTranslationYForStash();
+
+        StashedHandleViewController stashedHandleController =
+                controllers.stashedHandleViewController;
+        mTaskbarStashedHandleAlpha = stashedHandleController.getStashedHandleAlpha();
+
+        mIsStashedInApp = supportsStashing()
+                && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF);
+    }
+
+    /**
+     * Returns whether the user can manually stash the taskbar based on the current device state.
+     */
+    private boolean supportsStashing() {
+        return !mActivity.isThreeButtonNav();
+    }
+
+    /**
+     * Returns whether the taskbar is currently visually stashed.
+     */
+    public boolean isStashed() {
+        return mIsStashed;
+    }
+
+    /**
+     * Returns whether the user has manually stashed the taskbar in apps.
+     */
+    public boolean isStashedInApp() {
+        return mIsStashedInApp;
+    }
+
+    public int getContentHeight() {
+        return isStashed() ? mStashedHeight : mUnstashedHeight;
+    }
+
+    public int getStashedHeight() {
+        return mStashedHeight;
+    }
+
+    /**
+     * Should be called when long pressing the nav region when taskbar is present.
+     * @return Whether taskbar was stashed and now is unstashed.
+     */
+    public boolean onLongPressToUnstashTaskbar() {
+        if (!isStashed()) {
+            // We only listen for long press on the nav region to unstash the taskbar. To stash the
+            // taskbar, we use an OnLongClickListener on TaskbarView instead.
+            return false;
+        }
+        if (updateAndAnimateIsStashedInApp(false)) {
+            mControllers.taskbarActivityContext.getDragLayer().performHapticFeedback(LONG_PRESS);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Updates whether we should stash the taskbar when in apps, and animates to the changed state.
+     * @return Whether we started an animation to either be newly stashed or unstashed.
+     */
+    public boolean updateAndAnimateIsStashedInApp(boolean isStashedInApp) {
+        if (!supportsStashing()) {
+            return false;
+        }
+        if (mIsStashedInApp != isStashedInApp) {
+            boolean wasStashed = mIsStashedInApp;
+            mIsStashedInApp = isStashedInApp;
+            mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_KEY, mIsStashedInApp).apply();
+            boolean isStashed = mIsStashedInApp;
+            if (wasStashed != isStashed) {
+                createAnimToIsStashed(isStashed, TASKBAR_STASH_DURATION).start();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Starts an animation to the new stashed state with a default duration.
+     */
+    public void animateToIsStashed(boolean isStashed) {
+        animateToIsStashed(isStashed, TASKBAR_STASH_DURATION);
+    }
+
+    /**
+     * Starts an animation to the new stashed state with the specified duration.
+     */
+    public void animateToIsStashed(boolean isStashed, long duration) {
+        createAnimToIsStashed(isStashed, duration).start();
+    }
+
+    private Animator createAnimToIsStashed(boolean isStashed, long duration) {
+        AnimatorSet fullLengthAnimatorSet = new AnimatorSet();
+        // Not exactly half and may overlap. See [first|second]HalfDurationScale below.
+        AnimatorSet firstHalfAnimatorSet = new AnimatorSet();
+        AnimatorSet secondHalfAnimatorSet = new AnimatorSet();
+
+        final float firstHalfDurationScale;
+        final float secondHalfDurationScale;
+
+        if (isStashed) {
+            firstHalfDurationScale = 0.75f;
+            secondHalfDurationScale = 0.5f;
+            final float stashTranslation = (mUnstashedHeight - mStashedHeight) / 2f;
+
+            fullLengthAnimatorSet.playTogether(
+                    mTaskbarBackgroundOffset.animateToValue(1),
+                    mIconTranslationYForStash.animateToValue(stashTranslation)
+            );
+            firstHalfAnimatorSet.playTogether(
+                    mIconAlphaForStash.animateToValue(0),
+                    mIconScaleForStash.animateToValue(STASHED_TASKBAR_SCALE)
+            );
+            secondHalfAnimatorSet.playTogether(
+                    mTaskbarStashedHandleAlpha.animateToValue(1)
+            );
+        } else  {
+            firstHalfDurationScale = 0.5f;
+            secondHalfDurationScale = 0.75f;
+
+            fullLengthAnimatorSet.playTogether(
+                    mTaskbarBackgroundOffset.animateToValue(0),
+                    mIconScaleForStash.animateToValue(1),
+                    mIconTranslationYForStash.animateToValue(0)
+            );
+            firstHalfAnimatorSet.playTogether(
+                    mTaskbarStashedHandleAlpha.animateToValue(0)
+            );
+            secondHalfAnimatorSet.playTogether(
+                    mIconAlphaForStash.animateToValue(1)
+            );
+        }
+
+        Animator stashedHandleRevealAnim = mControllers.stashedHandleViewController
+                .createRevealAnimToIsStashed(isStashed);
+        if (stashedHandleRevealAnim != null) {
+            fullLengthAnimatorSet.play(stashedHandleRevealAnim);
+        }
+
+        fullLengthAnimatorSet.setDuration(duration);
+        firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale));
+        secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale));
+        secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale)));
+
+        if (mAnimator != null) {
+            mAnimator.cancel();
+        }
+        mAnimator = new AnimatorSet();
+        mAnimator.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet,
+                secondHalfAnimatorSet);
+        mAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mIsStashed = isStashed;
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mAnimator = null;
+            }
+        });
+        return mAnimator;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 260cedc..6d0e3c6 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -33,4 +33,8 @@
     }
 
     protected void updateContentInsets(Rect outContentInsets) { }
+
+    protected boolean onLongPressToUnstashTaskbar() {
+        return false;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 7753f96..820d40a 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -94,8 +94,10 @@
 
     protected void init(TaskbarViewController.TaskbarViewCallbacks callbacks) {
         mControllerCallbacks = callbacks;
-        mIconClickListener = mControllerCallbacks.getOnClickListener();
-        mIconLongClickListener = mControllerCallbacks.getOnLongClickListener();
+        mIconClickListener = mControllerCallbacks.getIconOnClickListener();
+        mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener();
+
+        setOnLongClickListener(mControllerCallbacks.getBackgroundOnLongClickListener());
     }
 
     private void removeAndRecycle(View view) {
@@ -235,6 +237,10 @@
         return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
     }
 
+    public Rect getIconLayoutBounds() {
+        return mIconLayoutBounds;
+    }
+
     // FolderIconParent implemented methods.
 
     @Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index c7ac4a4..50c26b3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -17,8 +17,8 @@
 
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.AnimatedFloat.VALUE;
 
 import android.graphics.Rect;
 import android.view.View;
@@ -28,6 +28,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.util.MultiValueAlpha;
+import com.android.quickstep.AnimatedFloat;
 
 /**
  * Handles properties/data collection, then passes the results to TaskbarView to render.
@@ -38,10 +39,16 @@
     public static final int ALPHA_INDEX_HOME = 0;
     public static final int ALPHA_INDEX_IME = 1;
     public static final int ALPHA_INDEX_KEYGUARD = 2;
+    public static final int ALPHA_INDEX_STASH = 3;
 
     private final TaskbarActivityContext mActivity;
     private final TaskbarView mTaskbarView;
     private final MultiValueAlpha mTaskbarIconAlpha;
+    private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale);
+    private final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat(
+            this::updateTranslationY);
+    private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat(
+            this::updateTranslationY);
 
     // Initialized in init.
     private TaskbarControllers mControllers;
@@ -54,7 +61,7 @@
     public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) {
         mActivity = activity;
         mTaskbarView = taskbarView;
-        mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, 3);
+        mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, 4);
         mTaskbarIconAlpha.setUpdateVisibility(true);
     }
 
@@ -62,6 +69,8 @@
         mControllers = controllers;
         mTaskbarView.init(new TaskbarViewCallbacks());
         mTaskbarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize;
+
+        mTaskbarIconScaleForStash.updateValue(1f);
     }
 
     public boolean areIconsVisible() {
@@ -86,6 +95,32 @@
         mTaskbarView.setClickAndLongClickListenersForIcon(icon);
     }
 
+    public Rect getIconLayoutBounds() {
+        return mTaskbarView.getIconLayoutBounds();
+    }
+
+    public AnimatedFloat getTaskbarIconScaleForStash() {
+        return mTaskbarIconScaleForStash;
+    }
+
+    public AnimatedFloat getTaskbarIconTranslationYForStash() {
+        return mTaskbarIconTranslationYForStash;
+    }
+
+    /**
+     * Applies scale properties for the entire TaskbarView (rather than individual icons).
+     */
+    private void updateScale() {
+        float scale = mTaskbarIconScaleForStash.value;
+        mTaskbarView.setScaleX(scale);
+        mTaskbarView.setScaleY(scale);
+    }
+
+    private void updateTranslationY() {
+        mTaskbarView.setTranslationY(mTaskbarIconTranslationYForHome.value
+                + mTaskbarIconTranslationYForStash.value);
+    }
+
     /**
      * Sets the taskbar icon alignment relative to Launcher hotseat icons
      * @param alignmentRatio [0, 1]
@@ -116,7 +151,7 @@
                         / launcherDp.numShownHotseatIcons;
 
         int offsetY = launcherDp.getTaskbarOffsetY();
-        setter.setFloat(mTaskbarView, VIEW_TRANSLATE_Y, -offsetY, LINEAR);
+        setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, LINEAR);
 
         int collapsedHeight = mActivity.getDeviceProfile().taskbarSize;
         int expandedHeight = collapsedHeight + offsetY;
@@ -144,12 +179,16 @@
      * Callbacks for {@link TaskbarView} to interact with its controller.
      */
     public class TaskbarViewCallbacks {
-        public View.OnClickListener getOnClickListener() {
+        public View.OnClickListener getIconOnClickListener() {
             return mActivity::onTaskbarIconClicked;
         }
 
-        public View.OnLongClickListener getOnLongClickListener() {
+        public View.OnLongClickListener getIconOnLongClickListener() {
             return mControllers.taskbarDragController::startDragOnLongClick;
         }
+
+        public View.OnLongClickListener getBackgroundOnLongClickListener() {
+            return view -> mControllers.taskbarStashController.updateAndAnimateIsStashedInApp(true);
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/AnimatedFloat.java b/quickstep/src/com/android/quickstep/AnimatedFloat.java
index f7e8781..95c8710 100644
--- a/quickstep/src/com/android/quickstep/AnimatedFloat.java
+++ b/quickstep/src/com/android/quickstep/AnimatedFloat.java
@@ -53,6 +53,16 @@
         mUpdateCallback = updateCallback;
     }
 
+    /**
+     * Returns an animation from the current value to the given value.
+     */
+    public ObjectAnimator animateToValue(float end) {
+        return animateToValue(value, end);
+    }
+
+    /**
+     * Returns an animation from the given start value to the given end value.
+     */
     public ObjectAnimator animateToValue(float start, float end) {
         cancelAnimation();
         mValueAnimator = ObjectAnimator.ofFloat(this, VALUE, start, end);
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 2699b07..1412b1a 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -364,6 +364,14 @@
     }
 
     /**
+     * Called when we detect a long press in the nav region before passing the gesture slop.
+     * @return Whether taskbar handled the long press, and thus should cancel the gesture.
+     */
+    public boolean onLongPressToUnstashTaskbar() {
+        return false;
+    }
+
+    /**
      * Returns the color of the scrim behind overview when at rest in this state.
      * Return {@link Color#TRANSPARENT} for no scrim.
      */
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 0b2a057..3580ee5 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -39,6 +39,7 @@
     int TYPE_OVERSCROLL = 1 << 9;
     int TYPE_SYSUI_OVERLAY = 1 << 10;
     int TYPE_ONE_HANDED = 1 << 11;
+    int TYPE_TASKBAR_STASH = 1 << 12;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -53,6 +54,7 @@
             "TYPE_OVERSCROLL",              // 9
             "TYPE_SYSUI_OVERLAY",           // 10
             "TYPE_ONE_HANDED",              // 11
+            "TYPE_TASKBAR_STASH",           // 12
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 799a4c2..09474a1 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -303,6 +303,15 @@
     }
 
     @Override
+    public boolean onLongPressToUnstashTaskbar() {
+        LauncherTaskbarUIController taskbarController = getTaskbarController();
+        if (taskbarController == null) {
+            return super.onLongPressToUnstashTaskbar();
+        }
+        return taskbarController.onLongPressToUnstashTaskbar();
+    }
+
+    @Override
     protected int getOverviewScrimColorForState(BaseQuickstepLauncher launcher,
             LauncherState state) {
         return state.getWorkspaceScrimColor(launcher);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 0ebe13b..9e69ef9 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -46,6 +46,8 @@
     private boolean mUseLauncherSysBarFlags = false;
     private boolean mSplitScreenMinimized = false;
     private boolean mFinishRequested = false;
+    // Only valid when mFinishRequested == true.
+    private boolean mFinishTargetIsLauncher;
     private RunnableList mPendingFinishCallbacks = new RunnableList();
 
     public RecentsAnimationController(RecentsAnimationControllerCompat controller,
@@ -145,6 +147,7 @@
 
         // Finish not yet requested
         mFinishRequested = true;
+        mFinishTargetIsLauncher = toRecents;
         mOnFinishedListener.accept(this);
         mPendingFinishCallbacks.add(callback);
         UI_HELPER_EXECUTOR.execute(() -> {
@@ -201,4 +204,12 @@
     public RecentsAnimationControllerCompat getController() {
         return mController;
     }
+
+    /**
+     * RecentsAnimationListeners can check this in onRecentsAnimationFinished() to determine whether
+     * the animation was finished to launcher vs an app.
+     */
+    public boolean getFinishTargetIsLauncher() {
+        return mFinishTargetIsLauncher;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 47ca3d2..5d701d4 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -95,6 +95,7 @@
 import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
 import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
+import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AssistantUtilities;
 import com.android.quickstep.util.ProtoTracer;
@@ -673,6 +674,14 @@
                         mDeviceState, event);
             }
 
+            // If Taskbar is present, we listen for long press to unstash it.
+            BaseActivityInterface activityInterface = newGestureState.getActivityInterface();
+            StatefulActivity activity = activityInterface.getCreatedActivity();
+            if (activity != null && activity.getDeviceProfile().isTaskbarPresent) {
+                base = new TaskbarStashInputConsumer(this, base, mInputMonitorCompat,
+                        activityInterface);
+            }
+
             if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()) {
                 OverscrollPlugin plugin = null;
                 if (FeatureFlags.FORCE_LOCAL_OVERSCROLL_PLUGIN.get()) {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
new file mode 100644
index 0000000..83f689f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021 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.quickstep.inputconsumers;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+import com.android.quickstep.BaseActivityInterface;
+import com.android.quickstep.InputConsumer;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Listens for a long press, and cancels the current gesture if that causes Taskbar to be unstashed.
+ */
+public class TaskbarStashInputConsumer extends DelegateInputConsumer {
+
+    private final BaseActivityInterface mActivityInterface;
+    private final GestureDetector mLongPressDetector;
+
+    public TaskbarStashInputConsumer(Context context, InputConsumer delegate,
+            InputMonitorCompat inputMonitor, BaseActivityInterface activityInterface) {
+        super(delegate, inputMonitor);
+        mActivityInterface = activityInterface;
+
+        mLongPressDetector = new GestureDetector(context, new SimpleOnGestureListener() {
+            @Override
+            public void onLongPress(MotionEvent motionEvent) {
+                onLongPressDetected(motionEvent);
+            }
+        });
+    }
+
+    @Override
+    public int getType() {
+        return TYPE_TASKBAR_STASH | mDelegate.getType();
+    }
+
+    @Override
+    public void onMotionEvent(MotionEvent ev) {
+        mLongPressDetector.onTouchEvent(ev);
+        if (mState != STATE_ACTIVE) {
+            mDelegate.onMotionEvent(ev);
+        }
+    }
+
+    private void onLongPressDetected(MotionEvent motionEvent) {
+        if (mActivityInterface.onLongPressToUnstashTaskbar()) {
+            setActive(motionEvent);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index 5be9529..8591872 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.util;
 
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
 import android.util.FloatProperty;
 import android.view.View;
 
@@ -121,5 +123,12 @@
         public String toString() {
             return Float.toString(mValue);
         }
+
+        /**
+         * Creates and returns an Animator from the current value to the given value.
+         */
+        public Animator animateToValue(float value) {
+            return ObjectAnimator.ofFloat(this, VALUE, value);
+        }
     }
 }