diff --git a/quickstep/res/drawable/task_thumbnail_background.xml b/quickstep/res/drawable/task_thumbnail_background.xml index 603380e753..f1f48ac11d 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. --> - diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java index c0fd2cfbc4..a107343d1d 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 android.view.View; 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 @@ public class RecentsView extends PagedView { private boolean mOverviewStateEnabled; private boolean mTaskStackListenerRegistered; + private LayoutTransition mLayoutTransition; private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { @Override @@ -87,6 +90,18 @@ public class RecentsView extends PagedView { 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 @@ public class RecentsView extends PagedView { // necessary) final LayoutInflater inflater = LayoutInflater.from(getContext()); final ArrayList 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 @@ public class RecentsView extends PagedView { 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 class RecentsView extends PagedView { } } } + + 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 ac9a7787a8..a0ad61869a 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 @@ import java.util.List; /** * 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 PROPERTY_SWIPE_PROGRESS = + new Property(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 @@ public class TaskView extends FrameLayout implements TaskCallbacks { 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 @@ public class TaskView extends FrameLayout implements TaskCallbacks { 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 class TaskView extends FrameLayout implements TaskCallbacks { 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(); + } }