diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml index f2f1ebde51..05f0695b7c 100644 --- a/quickstep/res/values/dimens.xml +++ b/quickstep/res/values/dimens.xml @@ -88,6 +88,9 @@ 18dp 16dp + + 25dp + 72dp 1.1 diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt index 99b962bc78..77a05c1e86 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 android.view.MotionEvent 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 @@ CONTAINER : RecentsViewContainer { 0f, dismissLength.toFloat(), 0f, - DISMISS_MAX_UNDERSHOOT, + container.resources.getDimension(R.dimen.task_dismiss_max_undershoot), DECELERATE, ) taskBeingDragged.secondaryDismissTranslationProperty.setValue( @@ -207,6 +208,5 @@ CONTAINER : RecentsViewContainer { 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 ceffbe4a81..da266220ca 100644 --- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java +++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java @@ -103,23 +103,28 @@ public class TaskGridNavHelper { } 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 a76ebdb1de..51980f0240 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -6379,7 +6379,7 @@ public abstract class RecentsView< } /** - * @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 @@ public abstract class RecentsView< * Creates the spring animations which run as a task settles back into its place in overview. * *

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 f610335c02..d37a3f9d31 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.graphics.Rect 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.touch.SingleAxisSwipeDetector 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 @@ class RecentsViewUtils(private val recentsView: RecentsView<*, *>) { * Creates the spring animations which run when a dragged task view in overview is released. * *

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 @@ class RecentsViewUtils(private val recentsView: RecentsView<*, *>) { 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 { + 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. + * + *

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 = 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 7066d21b0f..f2fa0c570d 100644 --- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt @@ -446,6 +446,37 @@ class TaskGridNavHelperTest { .isEqualTo(1) } + /* + 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 @@ -783,10 +814,11 @@ class TaskGridNavHelperTest { bottomIds: IntArray = IntArray.wrap(2, 4, 6), largeTileIds: List = 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 e0560e259a..79d3c19a0f 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java @@ -539,6 +539,24 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest { } } + @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 a545f0c20e..07f97bc4b7 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -117,6 +117,10 @@ 0.95 400 + + 0.6 + 900 + 0 diff --git a/res/values/dimens.xml b/res/values/dimens.xml index c48f14095d..c3cb31db5c 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -480,6 +480,7 @@ 0dp 0dp 0dp + 0dp 0dp 0dp 0dp diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java index 2431ef5aec..1158521db2 100644 --- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java +++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java @@ -210,6 +210,36 @@ public final class OverviewTask { : List.of(mOverview.getCurrentTask().getTaskCenterX()); } + /** + * 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. */