Adding swipe gestures in overview screen

> When on home time, swiping up goes to all_apps, and swiping down goes to normal
> When on a recents tile, swiping up the tile dismisses it, swiping down launches it
> When on a recents tile, swiping up on the hotseat opens allApps.

Change-Id: I59f8c02f5c5d9cb88c0585a083fbc33d33b1c806
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java
new file mode 100644
index 0000000..335077a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2018 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.uioverrides;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.util.TouchController;
+import com.android.quickstep.RecentsView;
+import com.android.quickstep.TaskView;
+
+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.Interpolators.DEACCEL_1_5;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+
+/**
+ * Touch controller for swipe interaction in Overview state
+ */
+public class OverviewSwipeController extends AnimatorListenerAdapter
+        implements TouchController, SwipeDetector.Listener {
+
+    private static final String TAG = "OverviewSwipeController";
+
+    private static final float ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS = 0.1f;
+    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 final RecentsView mRecentsView;
+    private final int[] mTempCords = new int[2];
+
+    private AnimatorPlaybackController mCurrentAnimation;
+    private boolean mCurrentAnimationIsGoingUp;
+
+    private boolean mNoIntercept;
+    private boolean mSwipeDownEnabled;
+
+    private float mDisplacementShift;
+    private float mProgressMultiplier;
+    private float mEndDisplacement;
+
+    private TaskView mTaskBeingDragged;
+
+    public OverviewSwipeController(Launcher launcher) {
+        mLauncher = launcher;
+        mRecentsView = launcher.getOverviewPanel();
+        mDetector = new SwipeDetector(launcher, this, SwipeDetector.VERTICAL);
+    }
+
+    private boolean canInterceptTouch() {
+        if (mCurrentAnimation != null) {
+            // If we are already animating from a previous state, we can intercept.
+            return true;
+        }
+        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+            return false;
+        }
+        return mLauncher.isInState(OVERVIEW);
+    }
+
+    private boolean isEventOverHotseat(MotionEvent ev) {
+        if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
+            return ev.getY() >
+                    mLauncher.getDragLayer().getHeight() * OVERVIEW.getVerticalProgress(mLauncher);
+        } else {
+            return mLauncher.getDragLayer().isEventOverHotseat(ev);
+        }
+    }
+
+    @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;
+        }
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mNoIntercept = !canInterceptTouch();
+            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) {
+                directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                ignoreSlopWhenSettling = true;
+            } else {
+                mTaskBeingDragged = null;
+                mSwipeDownEnabled = true;
+
+                int currentPage = mRecentsView.getCurrentPage();
+                if (currentPage == 0) {
+                    // User is on home tile
+                    directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                } else {
+                    View view = mRecentsView.getChildAt(currentPage);
+                    if (mLauncher.getDragLayer().isEventOverView(view, ev) &&
+                            view instanceof TaskView) {
+                        // The tile can be dragged down to open the task.
+                        mTaskBeingDragged = (TaskView) view;
+                        directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                    } else if (isEventOverHotseat(ev)) {
+                        // The hotseat is being dragged
+                        directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
+                        mSwipeDownEnabled = false;
+                    } else {
+                        mNoIntercept = true;
+                        return false;
+                    }
+                }
+            }
+
+            mDetector.setDetectableScrollConditions(
+                    directionsToDetectScroll, ignoreSlopWhenSettling);
+        }
+
+        if (mNoIntercept) {
+            return false;
+        }
+
+        onControllerTouchEvent(ev);
+        return mDetector.isDraggingOrSettling();
+    }
+
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        return mDetector.onTouchEvent(ev);
+    }
+
+    private void reinitAnimationController(boolean goingUp) {
+        if (!goingUp && !mSwipeDownEnabled) {
+            goingUp = true;
+        }
+        if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) {
+            // No need to init
+            return;
+        }
+        if (mCurrentAnimation != null) {
+            mCurrentAnimation.setPlayFraction(0);
+        }
+        mCurrentAnimationIsGoingUp = goingUp;
+        float range = mLauncher.getAllAppsController().getShiftRange();
+        long maxDuration = (long) (2 * range);
+        DragLayer dl = mLauncher.getDragLayer();
+
+        if (mTaskBeingDragged == null) {
+            // User is either going to all apps or home
+            mCurrentAnimation = mLauncher.getStateManager()
+                    .createAnimationToNewWorkspace(goingUp ? ALL_APPS : NORMAL, maxDuration);
+            if (goingUp) {
+                mEndDisplacement = -range;
+            } else {
+                View ws = mLauncher.getWorkspace();
+                mTempCords[1] = ws.getHeight() - ws.getPaddingBottom();
+                dl.getDescendantCoordRelativeToSelf(ws, mTempCords);
+
+                float distance = mTempCords[1];
+                if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
+                    mTempCords[1] = 0;
+                    dl.getDescendantCoordRelativeToSelf(mLauncher.getHotseat(), mTempCords);
+                    distance = mTempCords[1] - distance;
+                } else {
+                    distance = dl.getHeight() - distance;
+                }
+
+                mEndDisplacement = distance;
+            }
+        } else {
+            if (goingUp) {
+                AnimatorSet anim = new AnimatorSet();
+                ObjectAnimator translate = ObjectAnimator.ofFloat(
+                        mTaskBeingDragged, View.TRANSLATION_Y, -mTaskBeingDragged.getBottom());
+                translate.setInterpolator(LINEAR);
+                translate.setDuration(maxDuration);
+                anim.play(translate);
+
+                ObjectAnimator alpha = ObjectAnimator.ofFloat(mTaskBeingDragged, View.ALPHA, 0);
+                alpha.setInterpolator(DEACCEL_1_5);
+                alpha.setDuration(maxDuration);
+                anim.play(alpha);
+                mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration);
+                mEndDisplacement = -mTaskBeingDragged.getBottom();
+            } else {
+                AnimatorSet anim = new AnimatorSet();
+                // TODO: Setup a zoom animation
+                mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration);
+
+                mTempCords[1] = mTaskBeingDragged.getHeight();
+                dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords);
+                mEndDisplacement = dl.getHeight() - mTempCords[1];
+            }
+        }
+
+        mCurrentAnimation.getTarget().addListener(this);
+        mCurrentAnimation.dispatchOnStart();
+        mProgressMultiplier = 1 / mEndDisplacement;
+    }
+
+    @Override
+    public void onDragStart(boolean start) {
+        if (mCurrentAnimation == null) {
+            reinitAnimationController(mDetector.wasInitialTouchPositive());
+            mDisplacementShift = 0;
+        } else {
+            mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
+            mCurrentAnimation.pause();
+        }
+    }
+
+    @Override
+    public boolean onDrag(float displacement, float velocity) {
+        float totalDisplacement = displacement + mDisplacementShift;
+        boolean isGoingUp =
+                totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0;
+        if (isGoingUp != mCurrentAnimationIsGoingUp) {
+            reinitAnimationController(isGoingUp);
+        }
+        mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier);
+        return true;
+    }
+
+    @Override
+    public void onDragEnd(float velocity, boolean fling) {
+        final boolean goingToEnd;
+
+        if (fling) {
+            boolean goingUp = velocity < 0;
+            if (!goingUp && !mSwipeDownEnabled) {
+                goingToEnd = false;
+            } else if (goingUp != mCurrentAnimationIsGoingUp) {
+                // In case the fling is in opposite direction, make sure if is close enough
+                // from the start position
+                if (mCurrentAnimation.getProgressFraction()
+                        >= ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS) {
+                    // Not allowed
+                    goingToEnd = false;
+                } else {
+                    reinitAnimationController(goingUp);
+                    goingToEnd = true;
+                }
+            } else {
+                goingToEnd = true;
+            }
+        } else {
+            goingToEnd = mCurrentAnimation.getProgressFraction() > SUCCESS_TRANSITION_PROGRESS;
+        }
+
+        float progress = mCurrentAnimation.getProgressFraction();
+        long animationDuration = SwipeDetector.calculateDuration(
+                velocity, goingToEnd ? (1 - progress) : progress);
+
+        float nextFrameProgress = Utilities.boundToRange(
+                progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f);
+
+
+        mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd));
+
+        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
+        anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
+        anim.setDuration(animationDuration);
+        anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
+        anim.start();
+    }
+
+    private void onCurrentAnimationEnd(boolean wasSuccess) {
+        // TODO: Might be a good time to log something.
+        if (mTaskBeingDragged == null) {
+            LauncherState state = wasSuccess ?
+                    (mCurrentAnimationIsGoingUp ? ALL_APPS : NORMAL) : OVERVIEW;
+            mLauncher.getStateManager().goToState(state, false);
+        } else if (wasSuccess) {
+            if (mCurrentAnimationIsGoingUp) {
+                mRecentsView.onTaskDismissed(mTaskBeingDragged);
+            } else {
+                mTaskBeingDragged.launchTask(false);
+            }
+        }
+        mDetector.finishedScrolling();
+        mTaskBeingDragged = null;
+        mCurrentAnimation = null;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index bd443aa..0e539ee 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -41,11 +41,11 @@
             return new TouchController[]{
                     new EdgeSwipeController(launcher),
                     new TwoStepSwipeController(launcher),
-                    new OverviewSwipeUpController(launcher)};
+                    new OverviewSwipeController(launcher)};
         } else {
             return new TouchController[]{
                     new TwoStepSwipeController(launcher),
-                    new OverviewSwipeUpController(launcher)};
+                    new OverviewSwipeController(launcher)};
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/TaskView.java b/quickstep/src/com/android/quickstep/TaskView.java
index 94d85ee..3f733ca 100644
--- a/quickstep/src/com/android/quickstep/TaskView.java
+++ b/quickstep/src/com/android/quickstep/TaskView.java
@@ -16,29 +16,17 @@
 
 package com.android.quickstep;
 
-import static com.android.quickstep.RecentsView.SCROLL_TYPE_TASK;
-import static com.android.quickstep.RecentsView.SCROLL_TYPE_WORKSPACE;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
 import android.app.ActivityOptions;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.util.Property;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.touch.SwipeDetector;
 import com.android.quickstep.RecentsView.PageCallbacks;
 import com.android.quickstep.RecentsView.ScrollState;
 import com.android.systemui.shared.recents.model.Task;
@@ -52,11 +40,13 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import static com.android.quickstep.RecentsView.SCROLL_TYPE_TASK;
+import static com.android.quickstep.RecentsView.SCROLL_TYPE_WORKSPACE;
+
 /**
  * A task in the Recents view.
  */
-public class TaskView extends FrameLayout implements TaskCallbacks, SwipeDetector.Listener,
-        PageCallbacks {
+public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks {
 
     /** Designates how "curvy" the carousel is from 0 to 1, where 0 is a straight line. */
     private static final float CURVE_FACTOR = 0.25f;
@@ -70,30 +60,8 @@
      */
     private static final float MAX_PAGE_SCRIM_ALPHA = 0.8f;
 
-    private static final int SWIPE_DIRECTIONS = SwipeDetector.DIRECTION_POSITIVE;
-
-    /**
-     * The task will appear fully dismissed when the distance swiped
-     * reaches this percentage of the card height.
-     */
-    private static final float SWIPE_DISTANCE_HEIGHT_PERCENTAGE = 0.38f;
-
     private static final long SCALE_ICON_DURATION = 120;
 
-    private static final Property<TaskView, Float> PROPERTY_SWIPE_PROGRESS =
-            new Property<TaskView, Float>(Float.class, "swipe_progress") {
-
-                @Override
-                public Float get(TaskView taskView) {
-                    return taskView.mSwipeProgress;
-                }
-
-                @Override
-                public void set(TaskView taskView, Float progress) {
-                    taskView.setSwipeProgress(progress);
-                }
-            };
-
     private static final Property<TaskView, Float> SCALE_ICON_PROPERTY =
             new Property<TaskView, Float>(Float.TYPE, "scale_icon") {
                 @Override
@@ -110,11 +78,6 @@
     private Task mTask;
     private TaskThumbnailView mSnapshotView;
     private ImageView mIconView;
-    private SwipeDetector mSwipeDetector;
-    private float mSwipeDistance;
-    private float mSwipeProgress;
-    private Interpolator mAlphaInterpolator;
-    private Interpolator mSwipeAnimInterpolator;
     private float mIconScale = 1f;
 
     public TaskView(Context context) {
@@ -130,11 +93,6 @@
         setOnClickListener((view) -> {
             launchTask(true /* animate */);
         });
-
-        mSwipeDetector = new SwipeDetector(getContext(), this, SwipeDetector.VERTICAL);
-        mSwipeDetector.setDetectableScrollConditions(SWIPE_DIRECTIONS, false);
-        mAlphaInterpolator = Interpolators.ACCEL_1_5;
-        mSwipeAnimInterpolator = Interpolators.SCROLL_CUBIC;
     }
 
     @Override
@@ -144,15 +102,6 @@
         mIconView = findViewById(R.id.icon);
     }
 
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
-        View p = (View) getParent();
-        mSwipeDistance = (getMeasuredHeight() - p.getPaddingTop() - p.getPaddingBottom())
-                * SWIPE_DISTANCE_HEIGHT_PERCENTAGE;
-    }
-
     /**
      * Updates this task view to the given {@param task}.
      */
@@ -223,80 +172,6 @@
         // Do nothing
     }
 
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        mSwipeDetector.onTouchEvent(ev);
-        return super.onInterceptTouchEvent(ev);
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        mSwipeDetector.onTouchEvent(event);
-        return mSwipeDetector.isDraggingOrSettling() || super.onTouchEvent(event);
-    }
-
-    // Swipe detector methods
-
-    @Override
-    public void onDragStart(boolean start) {
-        getParent().requestDisallowInterceptTouchEvent(true);
-    }
-
-    @Override
-    public boolean onDrag(float displacement, float velocity) {
-        setSwipeProgress(Utilities.boundToRange(displacement / mSwipeDistance,
-                allowsSwipeUp() ? -1 : 0, allowsSwipeDown() ? 1 : 0));
-        return true;
-    }
-
-    /**
-     * Indicates the page is being removed.
-     * @param progress Ranges from -1 (fading upwards) to 1 (fading downwards).
-     */
-    private void setSwipeProgress(float progress) {
-        mSwipeProgress = progress;
-        float translationY = mSwipeProgress * mSwipeDistance;
-        float alpha = 1f - mAlphaInterpolator.getInterpolation(Math.abs(mSwipeProgress));
-        // Only change children to avoid changing our properties while dragging.
-        mIconView.setTranslationY(translationY);
-        mSnapshotView.setTranslationY(translationY);
-        mIconView.setAlpha(alpha);
-        mSnapshotView.setAlpha(alpha);
-    }
-
-    private boolean allowsSwipeUp() {
-        return (SWIPE_DIRECTIONS & SwipeDetector.DIRECTION_POSITIVE) != 0;
-    }
-
-    private boolean allowsSwipeDown() {
-        return (SWIPE_DIRECTIONS & SwipeDetector.DIRECTION_NEGATIVE) != 0;
-    }
-
-    @Override
-    public void onDragEnd(float velocity, boolean fling) {
-        boolean movingAwayFromCenter = velocity < 0 == mSwipeProgress < 0;
-        boolean flingAway = fling && movingAwayFromCenter
-                && (allowsSwipeUp() && velocity < 0 || allowsSwipeDown() && velocity > 0);
-        final boolean shouldRemove = flingAway || (!fling && Math.abs(mSwipeProgress) > 0.5f);
-        float fromProgress = mSwipeProgress;
-        float toProgress = !shouldRemove ? 0f : mSwipeProgress < 0 ? -1f : 1f;
-        ValueAnimator swipeAnimator = ObjectAnimator.ofFloat(this, PROPERTY_SWIPE_PROGRESS,
-                fromProgress, toProgress);
-        swipeAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (shouldRemove) {
-                    ((RecentsView) getParent()).onTaskDismissed(TaskView.this);
-                }
-                mSwipeDetector.finishedScrolling();
-            }
-        });
-        swipeAnimator.setDuration(SwipeDetector.calculateDuration(velocity,
-                Math.abs(toProgress - fromProgress)));
-        swipeAnimator.setInterpolator(mSwipeAnimInterpolator);
-        swipeAnimator.start();
-    }
-
     public void animateIconToScale(float scale) {
         ObjectAnimator.ofFloat(this, SCALE_ICON_PROPERTY, scale)
                 .setDuration(SCALE_ICON_DURATION).start();
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 6819386..68e9847 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -33,6 +33,12 @@
  */
 public abstract class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {
 
+    /**
+     * Creates an animation controller for the provided animation.
+     * The actual duration does not matter as the animation is manually controlled. It just
+     * needs to be larger than the total number of pixels so that we don't have jittering due
+     * to float (animation-fraction * total duration) to int conversion.
+     */
     public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
 
         /**
diff --git a/src/com/android/launcher3/touch/SwipeDetector.java b/src/com/android/launcher3/touch/SwipeDetector.java
index ff5f64c..df34885 100644
--- a/src/com/android/launcher3/touch/SwipeDetector.java
+++ b/src/com/android/launcher3/touch/SwipeDetector.java
@@ -286,6 +286,16 @@
         }
     }
 
+    /**
+     * Returns if the start drag was towards the positive direction or negative.
+     *
+     * @see #setDetectableScrollConditions(int, boolean)
+     * @see #DIRECTION_BOTH
+     */
+    public boolean wasInitialTouchPositive() {
+        return mSubtractDisplacement < 0;
+    }
+
     private boolean reportDragging() {
         if (mDisplacement != mLastDisplacement) {
             if (DBG) {