Merge "Implement swipe to dismiss recent tasks" into ub-launcher3-master
diff --git a/quickstep/res/drawable/task_thumbnail_background.xml b/quickstep/res/drawable/task_thumbnail_background.xml
index 603380e..f1f48ac 100644
--- a/quickstep/res/drawable/task_thumbnail_background.xml
+++ b/quickstep/res/drawable/task_thumbnail_background.xml
@@ -14,6 +14,5 @@
      limitations under the License.
 -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
-    <solid android:color="#FF000000" />
     <corners android:radius="2dp" />
 </shape>
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index c0fd2cf..a107343 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep;
 
+import android.animation.LayoutTransition;
 import android.animation.TimeInterpolator;
 import android.content.Context;
 import android.graphics.Rect;
@@ -25,6 +26,7 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -58,6 +60,7 @@
 
     private boolean mOverviewStateEnabled;
     private boolean mTaskStackListenerRegistered;
+    private LayoutTransition mLayoutTransition;
 
     private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
         @Override
@@ -87,6 +90,18 @@
         setWillNotDraw(false);
         setPageSpacing((int) getResources().getDimension(R.dimen.recents_page_spacing));
         enableFreeScroll(true);
+        setupLayoutTransition();
+    }
+
+    private void setupLayoutTransition() {
+        // We want to show layout transitions when pages are deleted, to close the gap.
+        mLayoutTransition = new LayoutTransition();
+        mLayoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING);
+        mLayoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
+
+        mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING);
+        mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
+        setLayoutTransition(mLayoutTransition);
     }
 
     @Override
@@ -141,6 +156,7 @@
         // necessary)
         final LayoutInflater inflater = LayoutInflater.from(getContext());
         final ArrayList<Task> tasks = stack.getTasks();
+        setLayoutTransition(null);
         for (int i = getChildCount(); i < tasks.size(); i++) {
             final TaskView taskView = (TaskView) inflater.inflate(R.layout.task, this, false);
             addView(taskView);
@@ -150,6 +166,7 @@
             removeView(taskView);
             loader.unloadTaskData(taskView.getTask());
         }
+        setLayoutTransition(mLayoutTransition);
 
         // Rebind all task views
         for (int i = tasks.size() - 1; i >= 0; i--) {
@@ -248,4 +265,12 @@
             }
         }
     }
+
+    public void onTaskDismissed(TaskView taskView) {
+        ActivityManagerWrapper.getInstance().removeTask(taskView.getTask().key.id);
+        removeView(taskView);
+        if (getChildCount() == 0) {
+            Launcher.getLauncher(getContext()).getStateManager().goToState(LauncherState.NORMAL);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskView.java b/quickstep/src/com/android/quickstep/TaskView.java
index ac9a778..a0ad618 100644
--- a/quickstep/src/com/android/quickstep/TaskView.java
+++ b/quickstep/src/com/android/quickstep/TaskView.java
@@ -16,15 +16,25 @@
 
 package com.android.quickstep;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+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.uioverrides.OverviewState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.touch.SwipeDetector;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskCallbacks;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -39,11 +49,38 @@
 /**
  * A task in the Recents view.
  */
-public class TaskView extends FrameLayout implements TaskCallbacks {
+public class TaskView extends FrameLayout implements TaskCallbacks, SwipeDetector.Listener {
+
+    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 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 Task mTask;
     private TaskThumbnailView mSnapshotView;
     private ImageView mIconView;
+    private SwipeDetector mSwipeDetector;
+    private float mSwipeDistance;
+    private float mSwipeProgress;
+    private Interpolator mAlphaInterpolator;
+    private Interpolator mSwipeAnimInterpolator;
 
     public TaskView(Context context) {
         this(context, null);
@@ -58,6 +95,11 @@
         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
@@ -67,6 +109,15 @@
         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}.
      */
@@ -134,4 +185,78 @@
     public void onTaskWindowingModeChanged() {
         // 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();
+    }
 }