Add spring animations for neighboring tasks on dismiss cancel
Fix: 389083088 Test: TaplTestsQuickstep#testDismissCancel Test: TaskGridNavHelperTest Flag: com.android.launcher3.enable_expressive_dismiss_task_motion Change-Id: I1240519eea499b5839d64e537588bde3cb0a424b
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <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,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <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 @@ 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<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 {
|
||||
|
||||
+33
-1
@@ -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<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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user