Merge "Add spring animations for neighboring tasks on dismiss cancel" into main
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index f2f1ebd..05f0695 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -88,6 +88,9 @@
<dimen name="task_thumbnail_header_icon_size">18dp</dimen>
<dimen name="task_thumbnail_header_round_corner_radius">16dp</dimen>
+ <!-- How much a task being dragged for dismissal can undershoot the origin when dragged back to its start position. -->
+ <dimen name="task_dismiss_max_undershoot">25dp</dimen>
+
<dimen name="task_icon_cache_default_icon_size">72dp</dimen>
<item name="overview_modal_max_scale" format="float" type="dimen">1.1</item>
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index 99b962b..77a05c1 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -20,6 +20,7 @@
import androidx.dynamicanimation.animation.SpringAnimation
import com.android.app.animation.Interpolators.DECELERATE
import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.R
import com.android.launcher3.Utilities.EDGE_NAV_BAR
import com.android.launcher3.Utilities.boundToRange
import com.android.launcher3.Utilities.isRtl
@@ -144,7 +145,7 @@
0f,
dismissLength.toFloat(),
0f,
- DISMISS_MAX_UNDERSHOOT,
+ container.resources.getDimension(R.dimen.task_dismiss_max_undershoot),
DECELERATE,
)
taskBeingDragged.secondaryDismissTranslationProperty.setValue(
@@ -207,6 +208,5 @@
companion object {
private const val DISMISS_THRESHOLD_FRACTION = 0.5f
- private const val DISMISS_MAX_UNDERSHOOT = 25f
}
}
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
index ceffbe4..da26622 100644
--- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
+++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
@@ -103,23 +103,28 @@
}
case DIRECTION_RIGHT: {
int boundedIndex =
- cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex) : Math.max(
- nextIndex, 0);
+ cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex)
+ : Math.max(nextIndex, 0);
boolean inOriginalTop = mOriginalTopRowIds.contains(currentPageTaskViewId);
return inOriginalTop ? mTopRowIds.get(boundedIndex)
: mBottomRowIds.get(boundedIndex);
}
case DIRECTION_TAB: {
int boundedIndex =
- cycle ? nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize : Math.min(
- nextIndex, maxSize - 1);
+ cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize)
+ : Math.min(nextIndex, maxSize - 1);
if (delta >= 0) {
return inTop && mTopRowIds.get(index) != mBottomRowIds.get(index)
? mBottomRowIds.get(index)
: mTopRowIds.get(boundedIndex);
} else {
if (mTopRowIds.contains(currentPageTaskViewId)) {
- return mBottomRowIds.get(boundedIndex);
+ if (boundedIndex < 0) {
+ // If no cycling, always return the first task.
+ return mTopRowIds.get(0);
+ } else {
+ return mBottomRowIds.get(boundedIndex);
+ }
} else {
// Go up to top if there is task above
return mTopRowIds.get(index) != mBottomRowIds.get(index)
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index a76ebdb..51980f0 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -6379,7 +6379,7 @@
}
/**
- * @return true if the task in on the top of the grid
+ * @return true if the task in on the bottom of the grid
*/
public boolean isOnGridBottomRow(TaskView taskView) {
return showAsGrid()
@@ -6942,7 +6942,8 @@
* Creates the spring animations which run as a task settles back into its place in overview.
*
* <p>When a task dismiss is cancelled, the task will return to its original position via a
- * spring animation.
+ * spring animation. As it passes the threshold of its settling state, its neighbors will
+ * spring in response to the perceived impact of the settling task.
*/
public SpringAnimation createTaskDismissSettlingSpringAnimation(TaskView draggedTaskView,
float velocity, boolean isDismissing, SingleAxisSwipeDetector detector,
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index f610335..d37a3f9 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -20,6 +20,7 @@
import android.view.View
import androidx.core.view.children
import androidx.dynamicanimation.animation.FloatPropertyCompat
+import androidx.dynamicanimation.animation.FloatValueHolder
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.launcher3.Flags.enableLargeDesktopWindowingTile
@@ -29,6 +30,7 @@
import com.android.launcher3.util.DynamicResource
import com.android.launcher3.util.IntArray
import com.android.quickstep.util.GroupTask
+import com.android.quickstep.util.TaskGridNavHelper
import com.android.quickstep.util.isExternalDisplay
import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
import com.android.systemui.shared.recents.model.ThumbnailData
@@ -305,7 +307,8 @@
* Creates the spring animations which run when a dragged task view in overview is released.
*
* <p>When a task dismiss is cancelled, the task will return to its original position via a
- * spring animation.
+ * spring animation. As it passes the threshold of its settling state, its neighbors will spring
+ * in response to the perceived impact of the settling task.
*/
fun createTaskDismissSettlingSpringAnimation(
draggedTaskView: TaskView?,
@@ -320,37 +323,181 @@
FloatPropertyCompat.createFloatPropertyCompat(
draggedTaskView.secondaryDismissTranslationProperty
)
- val rp = DynamicResource.provider(recentsView.mContainer)
- return SpringAnimation(draggedTaskView, taskDismissFloatProperty)
- .setSpring(
- SpringForce()
- .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
- .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness))
- )
- .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
- .addUpdateListener { animation, value, _ ->
- if (isDismissing && abs(value) >= abs(dismissLength)) {
- // TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
- draggedTaskView.alpha = 0f
- animation.cancel()
- } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
- recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
- remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
- taskDismissFloatProperty.getValue(draggedTaskView)
+ // Animate dragged task towards dismissal or rest state.
+ val draggedTaskViewSpringAnimation =
+ SpringAnimation(draggedTaskView, taskDismissFloatProperty)
+ .setSpring(createExpressiveDismissSpringForce())
+ .setStartVelocity(if (detector.isFling(velocity)) velocity else 0f)
+ .addUpdateListener { animation, value, _ ->
+ if (isDismissing && abs(value) >= abs(dismissLength)) {
+ // TODO(b/393553524): Remove 0 alpha, instead animate task fully off screen.
+ draggedTaskView.alpha = 0f
+ animation.cancel()
+ } else if (draggedTaskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+ recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+ remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+ taskDismissFloatProperty.getValue(draggedTaskView)
+ }
+ recentsView.redrawLiveTile()
}
- recentsView.redrawLiveTile()
}
+ .addEndListener { _, _, _, _ ->
+ if (isDismissing) {
+ recentsView.dismissTask(
+ draggedTaskView,
+ /* animateTaskView = */ false,
+ /* removeTask = */ true,
+ )
+ } else {
+ recentsView.onDismissAnimationEnds()
+ }
+ onEndRunnable()
+ }
+ if (!isDismissing) {
+ addNeighboringSpringAnimationsForDismissCancel(
+ draggedTaskView,
+ draggedTaskViewSpringAnimation,
+ recentsView.pageCount,
+ )
+ }
+ return draggedTaskViewSpringAnimation
+ }
+
+ private fun addNeighboringSpringAnimationsForDismissCancel(
+ draggedTaskView: TaskView,
+ draggedTaskViewSpringAnimation: SpringAnimation,
+ taskCount: Int,
+ ) {
+ // Empty spring animation exists for conditional start, and to drive neighboring springs.
+ val neighborsToSettle =
+ SpringAnimation(FloatValueHolder()).setSpring(createExpressiveDismissSpringForce())
+ var lastPosition = 0f
+ var startSettling = false
+ draggedTaskViewSpringAnimation.addUpdateListener { _, value, velocity ->
+ // Start the settling animation the first time the dragged task passes the origin (from
+ // negative displacement to positive displacement). We do not check for an exact value
+ // to compare to, as the update listener does not necessarily hit every value (e.g. a
+ // value of zero). Do not check again once it has started settling, as a spring can
+ // bounce past the origin multiple times depending on the stifness and damping ratio.
+ if (startSettling) return@addUpdateListener
+ if (lastPosition < 0 && value >= 0) {
+ startSettling = true
}
- .addEndListener { _, _, _, _ ->
- if (isDismissing) {
- recentsView.dismissTask(
- draggedTaskView,
- /* animateTaskView = */ false,
- /* removeTask = */ true,
+ lastPosition = value
+ if (startSettling) {
+ neighborsToSettle.setStartVelocity(velocity).animateToFinalPosition(0f)
+ }
+ }
+
+ // Add tasks before dragged index, fanning out from the dragged task.
+ // The order they are added matters, as each spring drives the next.
+ var previousNeighbor = neighborsToSettle
+ getTasksAdjacentToDraggedTask(draggedTaskView, towardsStart = true).forEach {
+ previousNeighbor = createNeighboringTaskViewSpringAnimation(it, previousNeighbor)
+ }
+ // Add tasks after dragged index, fanning out from the dragged task.
+ // The order they are added matters, as each spring drives the next.
+ previousNeighbor = neighborsToSettle
+ getTasksAdjacentToDraggedTask(draggedTaskView, towardsStart = false).forEach {
+ previousNeighbor = createNeighboringTaskViewSpringAnimation(it, previousNeighbor)
+ }
+ }
+
+ /** Gets adjacent tasks either before or after the dragged task in visual order. */
+ private fun getTasksAdjacentToDraggedTask(
+ draggedTaskView: TaskView,
+ towardsStart: Boolean,
+ ): Sequence<TaskView> {
+ if (recentsView.showAsGrid()) {
+ return gridTaskViewInTabOrderSequence(draggedTaskView, towardsStart)
+ } else {
+ val taskViewList = taskViews.toList()
+ val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
+
+ return if (towardsStart) {
+ taskViewList.take(draggedTaskViewIndex).reversed().asSequence()
+ } else {
+ taskViewList.takeLast(taskViewList.size - draggedTaskViewIndex - 1).asSequence()
+ }
+ }
+ }
+
+ /**
+ * Returns a sequence of TaskViews in the grid, ordered according to tab navigation, starting
+ * from the dragged TaskView, in the direction of the provided delta.
+ *
+ * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
+ * negative value moves backward towards the beginning.
+ */
+ private fun gridTaskViewInTabOrderSequence(
+ draggedTaskView: TaskView,
+ towardsStart: Boolean,
+ ): Sequence<TaskView> = sequence {
+ val taskGridNavHelper =
+ TaskGridNavHelper(
+ recentsView.topRowIdArray,
+ recentsView.bottomRowIdArray,
+ getLargeTaskViewIds(),
+ /* hasAddDesktopButton= */ false,
+ )
+ var nextTaskView: TaskView? = draggedTaskView
+ var previousTaskView: TaskView? = null
+ while (nextTaskView != previousTaskView && nextTaskView != null) {
+ previousTaskView = nextTaskView
+ nextTaskView =
+ recentsView.getTaskViewFromTaskViewId(
+ taskGridNavHelper.getNextGridPage(
+ nextTaskView.taskViewId,
+ if (towardsStart) -1 else 1,
+ TaskGridNavHelper.DIRECTION_TAB,
+ /* cycle = */ false,
)
- }
- onEndRunnable()
+ )
+ if (nextTaskView != null && nextTaskView != previousTaskView) {
+ yield(nextTaskView)
}
+ }
+ }
+
+ /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
+ private fun createNeighboringTaskViewSpringAnimation(
+ taskView: TaskView,
+ previousNeighborSpringAnimation: SpringAnimation,
+ ): SpringAnimation {
+ val neighboringTaskViewSpringAnimation =
+ SpringAnimation(
+ taskView,
+ FloatPropertyCompat.createFloatPropertyCompat(
+ taskView.secondaryDismissTranslationProperty
+ ),
+ )
+ .setSpring(createExpressiveDismissSpringForce())
+ // Update live tile on spring animation.
+ if (taskView.isRunningTask && recentsView.enableDrawingLiveTile) {
+ neighboringTaskViewSpringAnimation.addUpdateListener { _, _, _ ->
+ recentsView.runActionOnRemoteHandles { remoteTargetHandle ->
+ remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value =
+ taskView.secondaryDismissTranslationProperty.get(taskView)
+ }
+ recentsView.redrawLiveTile()
+ }
+ }
+ // Drive current neighbor's spring with the previous neighbor's.
+ previousNeighborSpringAnimation.addUpdateListener { _, value, _ ->
+ neighboringTaskViewSpringAnimation.animateToFinalPosition(value)
+ }
+ return neighboringTaskViewSpringAnimation
+ }
+
+ private fun createExpressiveDismissSpringForce(): SpringForce {
+ val resourceProvider = DynamicResource.provider(recentsView.mContainer)
+ return SpringForce()
+ .setDampingRatio(
+ resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_damping_ratio)
+ )
+ .setStiffness(
+ resourceProvider.getFloat(R.dimen.expressive_dismiss_task_trans_y_stiffness)
+ )
}
companion object {
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
index 7066d21..f2fa0c5 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
@@ -447,6 +447,37 @@
}
/*
+ 5 3 [1]
+ CLEAR_ALL
+ 6 4 2
+ */
+ @Test
+ fun equalLengthRows_noFocused_onTop_pressTabWithShift_noCycle_staysOnTop() {
+ assertThat(
+ getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = -1, cycle = false)
+ )
+ .isEqualTo(1)
+ }
+
+ /*
+ 5 3 1
+ [CLEAR_ALL]
+ 6 4 2
+ */
+ @Test
+ fun equalLengthRows_noFocused_onClearAll_pressTab_noCycle_staysOnClearAll() {
+ assertThat(
+ getNextGridPage(
+ currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
+ DIRECTION_TAB,
+ delta = 1,
+ cycle = false,
+ )
+ )
+ .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
+ }
+
+ /*
5 3 1
CLEAR_ALL FOCUSED_TASK←--DESKTOP
6 4 2
@@ -783,10 +814,11 @@
bottomIds: IntArray = IntArray.wrap(2, 4, 6),
largeTileIds: List<Int> = emptyList(),
hasAddDesktopButton: Boolean = false,
+ cycle: Boolean = true,
): Int {
val taskGridNavHelper =
TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton)
- return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, true)
+ return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle)
}
private companion object {
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index e0560e2..79d3c19 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -539,6 +539,24 @@
}
}
+ @Test
+ @PortraitLandscape
+ public void testDismissCancel() throws Exception {
+ startTestAppsWithCheck();
+ Overview overview = mLauncher.goHome().switchToOverview();
+ assertIsInState("Launcher internal state didn't switch to Overview",
+ ExpectedState.OVERVIEW);
+ final Integer numTasks = getFromRecentsView(RecentsView::getTaskViewCount);
+ OverviewTask task = overview.getCurrentTask();
+ assertNotNull("overview.getCurrentTask() returned null (2)", task);
+
+ task.dismissCancel();
+
+ runOnRecentsView(recentsView -> assertEquals(
+ "Canceling dismissing a task removed a task from Overview",
+ numTasks == null ? 0 : numTasks, recentsView.getTaskViewCount()));
+ }
+
private void startTestAppsWithCheck() throws Exception {
startTestApps();
expectLaunchedAppState();
diff --git a/res/values/config.xml b/res/values/config.xml
index a545f0c..07f97bc 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -117,6 +117,10 @@
<item name="swipe_up_rect_y_damping_ratio" type="dimen" format="float">0.95</item>
<item name="swipe_up_rect_y_stiffness" type="dimen" format="float">400</item>
+ <!-- Expressive Dismiss -->
+ <item name="expressive_dismiss_task_trans_y_damping_ratio" type="dimen" format="float">0.6</item>
+ <item name="expressive_dismiss_task_trans_y_stiffness" type="dimen" format="float">900</item>
+
<!-- Taskbar -->
<!-- This is a float because it is converted to dp later in DeviceProfile -->
<item name="taskbar_icon_size" type="dimen" format="float">0</item>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c48f140..c3cb31d 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -480,6 +480,7 @@
<dimen name="task_thumbnail_icon_drawable_size_grid">0dp</dimen>
<dimen name="task_thumbnail_icon_menu_drawable_touch_size">0dp</dimen>
<dimen name="task_menu_edge_padding">0dp</dimen>
+ <dimen name="task_dismiss_max_undershoot">0dp</dimen>
<dimen name="overview_task_margin">0dp</dimen>
<dimen name="overview_actions_height">0dp</dimen>
<dimen name="overview_actions_button_spacing">0dp</dimen>
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 2431ef5..1158521 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -211,6 +211,36 @@
}
/**
+ * Starts dismissing the task by swiping up, then cancels, and task springs back to start.
+ */
+ public void dismissCancel() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to start dismissing an overview task then cancel")) {
+ verifyActiveContainer();
+ int taskCountBeforeDismiss = mOverview.getTaskCount();
+ mLauncher.assertNotEquals("Unable to find a task", 0, taskCountBeforeDismiss);
+
+ final Rect taskBounds = mLauncher.getVisibleBounds(mTask);
+ final int centerX = taskBounds.centerX();
+ final int centerY = taskBounds.bottom - 1;
+ final int endCenterY = centerY - (taskBounds.height() / 4);
+ mLauncher.executeAndWaitForLauncherEvent(
+ // Set slowDown to true so we do not fling the task at the end of the drag, as
+ // we want it to cancel and return back to the origin. We use 30 steps to
+ // perform the gesture slowly as well, to avoid flinging.
+ () -> mLauncher.linearGesture(centerX, centerY, centerX, endCenterY,
+ /* steps= */ 30, /* slowDown= */ true,
+ LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER),
+ event -> TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE.equals(
+ event.getClassName()),
+ () -> "Canceling swipe to dismiss did not end with task at origin.",
+ "cancel swiping to dismiss");
+
+ }
+ }
+
+ /**
* Clicks the task.
*/
public LaunchedAppState open() {