From c824545d07d49f3360a74230d63b4b72cdecaea4 Mon Sep 17 00:00:00 2001 From: Pat Manning Date: Wed, 29 Jan 2025 15:08:34 +0000 Subject: [PATCH] Refactor TaskViewTouchController to separately handle dismiss and launch. Remove use of PendingAnimation for task dismiss. Fix: 389080698 Test: Manual. Flag: com.android.launcher3.enable_expressive_dismiss_task_motion Change-Id: Ifdfd7d8ef78bba5d10ff6f157635430637805584 --- .../uioverrides/QuickstepLauncher.java | 6 +- .../TaskViewDismissTouchController.kt | 212 ++++++++++ .../TaskViewLaunchTouchController.kt | 201 +++++++++ .../TaskViewTouchController.kt | 394 ------------------ .../quickstep/fallback/RecentsDragLayer.java | 21 +- .../orientation/LandscapePagedViewHandler.kt | 4 + .../orientation/PortraitPagedViewHandler.java | 6 + .../RecentsPagedOrientationHandler.kt | 3 + .../orientation/SeascapePagedViewHandler.kt | 4 + .../android/quickstep/views/RecentsView.java | 27 ++ .../quickstep/views/RecentsViewUtils.kt | 59 +++ .../com/android/quickstep/views/TaskView.kt | 2 +- 12 files changed, 536 insertions(+), 403 deletions(-) create mode 100644 quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt create mode 100644 quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt delete mode 100644 quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.kt diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index f672840fcd..82f475fd81 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -150,8 +150,9 @@ import com.android.launcher3.uioverrides.touchcontrollers.NoButtonQuickSwitchTou import com.android.launcher3.uioverrides.touchcontrollers.PortraitStatesTouchController; import com.android.launcher3.uioverrides.touchcontrollers.QuickSwitchTouchController; import com.android.launcher3.uioverrides.touchcontrollers.StatusBarTouchController; +import com.android.launcher3.uioverrides.touchcontrollers.TaskViewDismissTouchController; +import com.android.launcher3.uioverrides.touchcontrollers.TaskViewLaunchTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TaskViewRecentsTouchContext; -import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchControllerDeprecated; import com.android.launcher3.uioverrides.touchcontrollers.TransposedQuickSwitchTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TwoButtonNavbarTouchController; @@ -688,7 +689,8 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer, } if (enableExpressiveDismissTaskMotion()) { - list.add(new TaskViewTouchController<>(this, mTaskViewRecentsTouchContext)); + list.add(new TaskViewLaunchTouchController<>(this, mTaskViewRecentsTouchContext)); + list.add(new TaskViewDismissTouchController<>(this, mTaskViewRecentsTouchContext)); } else { list.add(new TaskViewTouchControllerDeprecated<>(this, mTaskViewRecentsTouchContext)); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt new file mode 100644 index 0000000000..99b962bc78 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.uioverrides.touchcontrollers + +import android.content.Context +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.Utilities.EDGE_NAV_BAR +import com.android.launcher3.Utilities.boundToRange +import com.android.launcher3.Utilities.isRtl +import com.android.launcher3.Utilities.mapToRange +import com.android.launcher3.touch.SingleAxisSwipeDetector +import com.android.launcher3.util.TouchController +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskView +import kotlin.math.abs +import kotlin.math.sign + +/** Touch controller for handling task view card dismiss swipes */ +class TaskViewDismissTouchController( + private val container: CONTAINER, + private val taskViewRecentsTouchContext: TaskViewRecentsTouchContext, +) : TouchController, SingleAxisSwipeDetector.Listener where +CONTAINER : Context, +CONTAINER : RecentsViewContainer { + private val recentsView: RecentsView<*, *> = container.getOverviewPanel() + private val detector: SingleAxisSwipeDetector = + SingleAxisSwipeDetector( + container as Context, + this, + recentsView.pagedOrientationHandler.upDownSwipeDirection, + ) + private val isRtl = isRtl(container.resources) + + private var taskBeingDragged: TaskView? = null + private var springAnimation: SpringAnimation? = null + private var dismissLength: Int = 0 + private var verticalFactor: Int = 0 + private var initialDisplacement: Float = 0f + + private fun canInterceptTouch(ev: MotionEvent): Boolean = + when { + // Don't intercept swipes on the nav bar, as user might be trying to go home during a + // task dismiss animation. + (ev.edgeFlags and EDGE_NAV_BAR) != 0 -> { + false + } + + // Floating views that a TouchController should not try to intercept touches from. + AbstractFloatingView.getTopOpenViewWithType( + container, + AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT, + ) != null -> false + + // Disable swiping if the task overlay is modal. + taskViewRecentsTouchContext.isRecentsModal -> { + false + } + + else -> taskViewRecentsTouchContext.isRecentsInteractive + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { + if ((ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_CANCEL)) { + clearState() + } + if (ev.action == MotionEvent.ACTION_DOWN) { + if (!onActionDown(ev)) { + return false + } + } + + onControllerTouchEvent(ev) + return detector.isDraggingState && detector.wasInitialTouchPositive() + } + + override fun onControllerTouchEvent(ev: MotionEvent?): Boolean = detector.onTouchEvent(ev) + + private fun onActionDown(ev: MotionEvent): Boolean { + springAnimation?.cancel() + if (!canInterceptTouch(ev)) { + return false + } + + taskBeingDragged = + recentsView.taskViews + .firstOrNull { + recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev) + } + ?.also { + dismissLength = recentsView.pagedOrientationHandler.getSecondaryDimension(it) + verticalFactor = + recentsView.pagedOrientationHandler.secondaryTranslationDirectionFactor + } + + detector.setDetectableScrollConditions( + recentsView.pagedOrientationHandler.getUpDirection(isRtl), + /* ignoreSlop = */ false, + ) + + return true + } + + override fun onDragStart(start: Boolean, startDisplacement: Float) { + val taskBeingDragged = taskBeingDragged ?: return + + initialDisplacement = + taskBeingDragged.secondaryDismissTranslationProperty.get(taskBeingDragged) + + // Add a tiny bit of translation Z, so that it draws on top of other views. This is relevant + // (e.g.) when we dismiss a task by sliding it upward: if there is a row of icons above, we + // want the dragged task to stay above all other views. + taskBeingDragged.translationZ = 0.1f + } + + override fun onDrag(displacement: Float): Boolean { + val taskBeingDragged = taskBeingDragged ?: return false + val currentDisplacement = displacement + initialDisplacement + val boundedDisplacement = + boundToRange(abs(currentDisplacement), 0f, dismissLength.toFloat()) + // When swiping below origin, allow slight undershoot to simulate resisting the movement. + val totalDisplacement = + if (isDisplacementPositiveDirection(currentDisplacement)) + boundedDisplacement * sign(currentDisplacement) + else + mapToRange( + boundedDisplacement, + 0f, + dismissLength.toFloat(), + 0f, + DISMISS_MAX_UNDERSHOOT, + DECELERATE, + ) + taskBeingDragged.secondaryDismissTranslationProperty.setValue( + taskBeingDragged, + totalDisplacement, + ) + if (taskBeingDragged.isRunningTask && recentsView.enableDrawingLiveTile) { + recentsView.runActionOnRemoteHandles { remoteTargetHandle -> + remoteTargetHandle.taskViewSimulator.taskSecondaryTranslation.value = + totalDisplacement + } + recentsView.redrawLiveTile() + } + return true + } + + override fun onDragEnd(velocity: Float) { + val taskBeingDragged = taskBeingDragged ?: return + + val currentDisplacement = + taskBeingDragged.secondaryDismissTranslationProperty.get(taskBeingDragged) + if (currentDisplacement == 0f) { + clearState() + return + } + val isBeyondDismissThreshold = + abs(currentDisplacement) > abs(DISMISS_THRESHOLD_FRACTION * dismissLength) + val isFlingingTowardsDismiss = detector.isFling(velocity) && velocity < 0 + val isFlingingTowardsRestState = detector.isFling(velocity) && velocity > 0 + val isDismissing = + isFlingingTowardsDismiss || (isBeyondDismissThreshold && !isFlingingTowardsRestState) + springAnimation = + recentsView + .createTaskDismissSettlingSpringAnimation( + taskBeingDragged, + velocity, + isDismissing, + detector, + dismissLength, + this::clearState, + ) + .apply { + animateToFinalPosition( + if (isDismissing) (dismissLength * verticalFactor).toFloat() else 0f + ) + } + } + + // Returns if the current task being dragged is towards "positive" (e.g. dismissal). + private fun isDisplacementPositiveDirection(displacement: Float): Boolean = + sign(displacement) == sign(verticalFactor.toFloat()) + + private fun clearState() { + detector.finishedScrolling() + detector.setDetectableScrollConditions(0, false) + taskBeingDragged?.translationZ = 0f + taskBeingDragged = null + springAnimation = null + } + + companion object { + private const val DISMISS_THRESHOLD_FRACTION = 0.5f + private const val DISMISS_MAX_UNDERSHOOT = 25f + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt new file mode 100644 index 0000000000..c740dadbcd --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.uioverrides.touchcontrollers + +import android.content.Context +import android.graphics.Rect +import android.view.MotionEvent +import com.android.app.animation.Interpolators.ZOOM_IN +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.LauncherAnimUtils +import com.android.launcher3.Utilities.EDGE_NAV_BAR +import com.android.launcher3.Utilities.boundToRange +import com.android.launcher3.Utilities.isRtl +import com.android.launcher3.anim.AnimatorPlaybackController +import com.android.launcher3.touch.BaseSwipeDetector +import com.android.launcher3.touch.SingleAxisSwipeDetector +import com.android.launcher3.util.DisplayController +import com.android.launcher3.util.FlingBlockCheck +import com.android.launcher3.util.TouchController +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskView +import kotlin.math.abs + +/** Touch controller which handles dragging task view cards for launch. */ +class TaskViewLaunchTouchController( + private val container: CONTAINER, + private val taskViewRecentsTouchContext: TaskViewRecentsTouchContext, +) : TouchController, SingleAxisSwipeDetector.Listener where +CONTAINER : Context, +CONTAINER : RecentsViewContainer { + private val tempRect = Rect() + private val flingBlockCheck = FlingBlockCheck() + private val recentsView: RecentsView<*, *> = container.getOverviewPanel() + private val detector: SingleAxisSwipeDetector = + SingleAxisSwipeDetector( + container as Context, + this, + recentsView.pagedOrientationHandler.upDownSwipeDirection, + ) + private val isRtl = isRtl(container.resources) + + private var taskBeingDragged: TaskView? = null + private var launchEndDisplacement: Float = 0f + private var playbackController: AnimatorPlaybackController? = null + private var verticalFactor: Int = 0 + + private fun canTaskLaunchTaskView(taskView: TaskView?) = + taskView != null && + taskView === recentsView.currentPageTaskView && + DisplayController.getNavigationMode(container).hasGestures && + (!recentsView.showAsGrid() || taskView.isLargeTile) && + recentsView.isTaskInExpectedScrollPosition(taskView) + + private fun canInterceptTouch(ev: MotionEvent): Boolean = + when { + // Don't intercept swipes on the nav bar, as user might be trying to go home during a + // task dismiss animation. + (ev.edgeFlags and EDGE_NAV_BAR) != 0 -> { + false + } + + // Floating views that a TouchController should not try to intercept touches from. + AbstractFloatingView.getTopOpenViewWithType( + container, + AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT, + ) != null -> { + false + } + + // Disable swiping if the task overlay is modal. + taskViewRecentsTouchContext.isRecentsModal -> { + false + } + + else -> taskViewRecentsTouchContext.isRecentsInteractive + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { + if ( + (ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_CANCEL) && + playbackController == null + ) { + clearState() + } + if (ev.action == MotionEvent.ACTION_DOWN) { + if (!onActionDown(ev)) { + clearState() + return false + } + } + onControllerTouchEvent(ev) + return detector.isDraggingState && !detector.wasInitialTouchPositive() + } + + override fun onControllerTouchEvent(ev: MotionEvent) = detector.onTouchEvent(ev) + + private fun onActionDown(ev: MotionEvent): Boolean { + if (!canInterceptTouch(ev)) { + return false + } + taskBeingDragged = + recentsView.taskViews + .firstOrNull { + recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev) + } + ?.also { + verticalFactor = + recentsView.pagedOrientationHandler.secondaryTranslationDirectionFactor + } + if (!canTaskLaunchTaskView(taskBeingDragged)) { + return false + } + detector.setDetectableScrollConditions( + recentsView.pagedOrientationHandler.getDownDirection(isRtl), + /* ignoreSlop = */ false, + ) + return true + } + + override fun onDragStart(start: Boolean, startDisplacement: Float) { + val taskBeingDragged = taskBeingDragged ?: return + + val secondaryLayerDimension: Int = + recentsView.pagedOrientationHandler.getSecondaryDimension(container.getDragLayer()) + val maxDuration = 2L * secondaryLayerDimension + recentsView.clearPendingAnimation() + val pendingAnimation = + recentsView.createTaskLaunchAnimation(taskBeingDragged, maxDuration, ZOOM_IN) + // Since the thumbnail is what is filling the screen, based the end displacement on it. + taskBeingDragged.getThumbnailBounds(tempRect, /* relativeToDragLayer= */ true) + launchEndDisplacement = (secondaryLayerDimension - tempRect.bottom).toFloat() + playbackController = + pendingAnimation.createPlaybackController()?.apply { + taskViewRecentsTouchContext.onUserControlledAnimationCreated(this) + dispatchOnStart() + } + } + + override fun onDrag(displacement: Float): Boolean { + playbackController?.setPlayFraction( + boundToRange(displacement / launchEndDisplacement, 0f, 1f) + ) + return true + } + + override fun onDragEnd(velocity: Float) { + val playbackController = playbackController ?: return + + val isBeyondLaunchThreshold = + abs(playbackController.progressFraction) > abs(LAUNCH_THRESHOLD_FRACTION) + val isFlingingTowardsLaunch = detector.isFling(velocity) && velocity > 0 + val isFlingingTowardsRestState = detector.isFling(velocity) && velocity < 0 + val isLaunching = + isFlingingTowardsLaunch || (isBeyondLaunchThreshold && !isFlingingTowardsRestState) + + val progress = playbackController.progressFraction + var animationDuration = + BaseSwipeDetector.calculateDuration( + velocity, + if (isLaunching) (1 - progress) else progress, + ) + if (detector.isFling(velocity) && flingBlockCheck.isBlocked && !isLaunching) { + animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity).toLong() + } + + playbackController.setEndAction(this::clearState) + playbackController.startWithVelocity( + container, + isLaunching, + velocity, + launchEndDisplacement, + animationDuration, + ) + } + + private fun clearState() { + detector.finishedScrolling() + detector.setDetectableScrollConditions(0, false) + taskBeingDragged = null + playbackController = null + } + + companion object { + private const val LAUNCH_THRESHOLD_FRACTION: Float = 0.5f + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.kt deleted file mode 100644 index c996f34392..0000000000 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.kt +++ /dev/null @@ -1,394 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.launcher3.uioverrides.touchcontrollers - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.content.Context -import android.graphics.Rect -import android.os.VibrationEffect -import android.view.MotionEvent -import android.view.animation.Interpolator -import com.android.app.animation.Interpolators -import com.android.launcher3.AbstractFloatingView -import com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS -import com.android.launcher3.LauncherAnimUtils.blockedFlingDurationFactor -import com.android.launcher3.R -import com.android.launcher3.Utilities -import com.android.launcher3.anim.AnimatorPlaybackController -import com.android.launcher3.anim.PendingAnimation -import com.android.launcher3.touch.BaseSwipeDetector -import com.android.launcher3.touch.SingleAxisSwipeDetector -import com.android.launcher3.util.DisplayController -import com.android.launcher3.util.FlingBlockCheck -import com.android.launcher3.util.TouchController -import com.android.launcher3.util.VibratorWrapper -import com.android.quickstep.util.VibrationConstants -import com.android.quickstep.views.RecentsView -import com.android.quickstep.views.RecentsViewContainer -import com.android.quickstep.views.TaskView -import kotlin.math.abs - -/** Touch controller for handling task view card swipes */ -class TaskViewTouchController( - private val container: CONTAINER, - private val taskViewRecentsTouchContext: TaskViewRecentsTouchContext, -) : AnimatorListenerAdapter(), TouchController, SingleAxisSwipeDetector.Listener where -CONTAINER : Context, -CONTAINER : RecentsViewContainer { - private val recentsView: RecentsView<*, *> = container.getOverviewPanel() - private val detector: SingleAxisSwipeDetector = - SingleAxisSwipeDetector( - container as Context, - this, - recentsView.pagedOrientationHandler.upDownSwipeDirection, - ) - private val tempRect = Rect() - private val isRtl = Utilities.isRtl(container.resources) - private val flingBlockCheck = FlingBlockCheck() - - private var currentAnimation: AnimatorPlaybackController? = null - private var currentAnimationIsGoingUp = false - private var allowGoingUp = false - private var allowGoingDown = false - private var noIntercept = false - private var displacementShift = 0f - private var progressMultiplier = 0f - private var endDisplacement = 0f - private var draggingEnabled = true - private var overrideVelocity: Float? = null - private var taskBeingDragged: TaskView? = null - private var isDismissHapticRunning = false - - private fun canInterceptTouch(ev: MotionEvent): Boolean { - val currentAnimation = currentAnimation - return when { - (ev.edgeFlags and Utilities.EDGE_NAV_BAR) != 0 -> { - // Don't intercept swipes on the nav bar, as user might be trying to go home - // during a task dismiss animation. - currentAnimation?.animationPlayer?.end() - false - } - currentAnimation != null -> { - currentAnimation.forceFinishIfCloseToEnd() - true - } - AbstractFloatingView.getTopOpenViewWithType( - container, - AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT, - ) != null -> false - else -> taskViewRecentsTouchContext.isRecentsInteractive - } - } - - override fun onAnimationCancel(animation: Animator) { - if (animation === currentAnimation?.target) { - clearState() - } - } - - override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { - if ( - (ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_CANCEL) && - currentAnimation == null - ) { - clearState() - } - if (ev.action == MotionEvent.ACTION_DOWN) { - // Disable swiping up and down if the task overlay is modal. - if (taskViewRecentsTouchContext.isRecentsModal) { - noIntercept = true - return false - } - noIntercept = !canInterceptTouch(ev) - if (noIntercept) { - return false - } - // Now figure out which direction scroll events the controller will start - // calling the callbacks. - var directionsToDetectScroll = 0 - var ignoreSlopWhenSettling = false - if (currentAnimation != null) { - directionsToDetectScroll = SingleAxisSwipeDetector.DIRECTION_BOTH - ignoreSlopWhenSettling = true - } else { - taskBeingDragged = null - recentsView.taskViews.forEach { taskView -> - if ( - recentsView.isTaskViewVisible(taskView) && - container.dragLayer.isEventOverView(taskView, ev) - ) { - taskBeingDragged = taskView - val upDirection = recentsView.pagedOrientationHandler.getUpDirection(isRtl) - - // The task can be dragged up to dismiss it - allowGoingUp = true - - // The task can be dragged down to open it if: - // - It's the current page - // - We support gestures to enter overview - // - It's the focused task if in grid view - // - The task is snapped - allowGoingDown = - taskView === recentsView.currentPageTaskView && - DisplayController.getNavigationMode(container).hasGestures && - (!recentsView.showAsGrid() || taskView.isLargeTile) && - recentsView.isTaskInExpectedScrollPosition(taskView) - - directionsToDetectScroll = - if (allowGoingDown) SingleAxisSwipeDetector.DIRECTION_BOTH - else upDirection - return@forEach - } - } - if (taskBeingDragged == null) { - noIntercept = true - return false - } - } - detector.setDetectableScrollConditions(directionsToDetectScroll, ignoreSlopWhenSettling) - } - if (noIntercept) { - return false - } - onControllerTouchEvent(ev) - return detector.isDraggingOrSettling - } - - override fun onControllerTouchEvent(ev: MotionEvent): Boolean = detector.onTouchEvent(ev) - - private fun reInitAnimationController(goingUp: Boolean) { - if (currentAnimation != null && currentAnimationIsGoingUp == goingUp) { - // No need to init - return - } - if ((goingUp && !allowGoingUp) || (!goingUp && !allowGoingDown)) { - // Trying to re-init in an unsupported direction. - return - } - val taskBeingDragged = taskBeingDragged ?: return - currentAnimation?.setPlayFraction(0f) - currentAnimation?.target?.removeListener(this) - currentAnimation?.dispatchOnCancel() - - val orientationHandler = recentsView.pagedOrientationHandler - currentAnimationIsGoingUp = goingUp - val dl = container.dragLayer - val secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl) - val maxDuration = 2L * secondaryLayerDimension - val verticalFactor = orientationHandler.getTaskDragDisplacementFactor(isRtl) - val secondaryTaskDimension = orientationHandler.getSecondaryDimension(taskBeingDragged) - // The interpolator controlling the most prominent visual movement. We use this to determine - // whether we passed SUCCESS_TRANSITION_PROGRESS. - val currentInterpolator: Interpolator - val pa: PendingAnimation - if (goingUp) { - currentInterpolator = Interpolators.LINEAR - pa = PendingAnimation(maxDuration) - recentsView.createTaskDismissAnimation( - pa, - taskBeingDragged, - true, /* animateTaskView */ - true, /* removeTask */ - maxDuration, - false, /* dismissingForSplitSelection*/ - ) - - endDisplacement = -secondaryTaskDimension.toFloat() - } else { - currentInterpolator = Interpolators.ZOOM_IN - pa = - recentsView.createTaskLaunchAnimation( - taskBeingDragged, - maxDuration, - currentInterpolator, - ) - - // Since the thumbnail is what is filling the screen, based the end displacement on it. - taskBeingDragged.getThumbnailBounds(tempRect, /* relativeToDragLayer= */ true) - endDisplacement = (secondaryLayerDimension - tempRect.bottom).toFloat() - } - endDisplacement *= verticalFactor.toFloat() - currentAnimation = - pa.createPlaybackController().apply { - // Setting this interpolator doesn't affect the visual motion, but is used to - // determine whether we successfully reached the target state in onDragEnd(). - target.interpolator = currentInterpolator - taskViewRecentsTouchContext.onUserControlledAnimationCreated(this) - target.addListener(this@TaskViewTouchController) - dispatchOnStart() - } - progressMultiplier = 1 / endDisplacement - } - - override fun onDragStart(start: Boolean, startDisplacement: Float) { - if (!draggingEnabled) return - val currentAnimation = currentAnimation - - val orientationHandler = recentsView.pagedOrientationHandler - if (currentAnimation == null) { - reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, isRtl)) - displacementShift = 0f - } else { - displacementShift = currentAnimation.progressFraction / progressMultiplier - currentAnimation.pause() - } - flingBlockCheck.unblockFling() - overrideVelocity = null - } - - override fun onDrag(displacement: Float): Boolean { - if (!draggingEnabled) return true - val taskBeingDragged = taskBeingDragged ?: return true - val currentAnimation = currentAnimation ?: return true - - val orientationHandler = recentsView.pagedOrientationHandler - val totalDisplacement = displacement + displacementShift - val isGoingUp = - if (totalDisplacement == 0f) currentAnimationIsGoingUp - else orientationHandler.isGoingUp(totalDisplacement, isRtl) - if (isGoingUp != currentAnimationIsGoingUp) { - reInitAnimationController(isGoingUp) - flingBlockCheck.blockFling() - } else { - flingBlockCheck.onEvent() - } - - if (isGoingUp) { - if (currentAnimation.progressFraction < ANIMATION_PROGRESS_FRACTION_MIDPOINT) { - // Halve the value when dismissing, as we are animating the drag across the full - // length for only the first half of the progress - currentAnimation.setPlayFraction( - Utilities.boundToRange(totalDisplacement * progressMultiplier / 2, 0f, 1f) - ) - } else { - // Set mOverrideVelocity to control task dismiss velocity in onDragEnd - var velocityDimenId = R.dimen.default_task_dismiss_drag_velocity - if (recentsView.showAsGrid()) { - velocityDimenId = - if (taskBeingDragged.isLargeTile) { - R.dimen.default_task_dismiss_drag_velocity_grid_focus_task - } else { - R.dimen.default_task_dismiss_drag_velocity_grid - } - } - overrideVelocity = -taskBeingDragged.resources.getDimension(velocityDimenId) - - // Once halfway through task dismissal interpolation, switch from reversible - // dragging-task animation to playing the remaining task translation animations, - // while this is in progress disable dragging. - draggingEnabled = false - } - } else { - currentAnimation.setPlayFraction( - Utilities.boundToRange(totalDisplacement * progressMultiplier, 0f, 1f) - ) - } - - return true - } - - override fun onDragEnd(velocity: Float) { - val taskBeingDragged = taskBeingDragged ?: return - val currentAnimation = currentAnimation ?: return - - // Limit velocity, as very large scalar values make animations play too quickly - val maxTaskDismissDragVelocity = - taskBeingDragged.resources.getDimension(R.dimen.max_task_dismiss_drag_velocity) - val endVelocity = - Utilities.boundToRange( - overrideVelocity ?: velocity, - -maxTaskDismissDragVelocity, - maxTaskDismissDragVelocity, - ) - overrideVelocity = null - - var fling = draggingEnabled && detector.isFling(endVelocity) - val goingToEnd: Boolean - val blockedFling = fling && flingBlockCheck.isBlocked - if (blockedFling) { - fling = false - } - val orientationHandler = recentsView.pagedOrientationHandler - val goingUp = orientationHandler.isGoingUp(endVelocity, isRtl) - val progress = currentAnimation.progressFraction - val interpolatedProgress = currentAnimation.interpolatedProgress - goingToEnd = - if (fling) { - goingUp == currentAnimationIsGoingUp - } else { - interpolatedProgress > SUCCESS_TRANSITION_PROGRESS - } - var animationDuration = - BaseSwipeDetector.calculateDuration( - endVelocity, - if (goingToEnd) (1 - progress) else progress, - ) - if (blockedFling && !goingToEnd) { - animationDuration *= blockedFlingDurationFactor(endVelocity).toLong() - } - // Due to very high or low velocity dismissals, animation durations can be inconsistently - // long or short. Bound the duration for animation of task translations for a more - // standardized feel. - animationDuration = - Utilities.boundToRange( - animationDuration, - MIN_TASK_DISMISS_ANIMATION_DURATION, - MAX_TASK_DISMISS_ANIMATION_DURATION, - ) - - currentAnimation.setEndAction { this.clearState() } - currentAnimation.startWithVelocity( - container, - goingToEnd, - abs(endVelocity.toDouble()).toFloat(), - endDisplacement, - animationDuration, - ) - if (goingUp && goingToEnd && !isDismissHapticRunning) { - VibratorWrapper.INSTANCE.get(container) - .vibrate( - TASK_DISMISS_VIBRATION_PRIMITIVE, - TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE, - TASK_DISMISS_VIBRATION_FALLBACK, - ) - isDismissHapticRunning = true - } - - draggingEnabled = true - } - - private fun clearState() { - detector.finishedScrolling() - detector.setDetectableScrollConditions(0, false) - draggingEnabled = true - taskBeingDragged = null - currentAnimation = null - isDismissHapticRunning = false - } - - companion object { - private const val ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f - private const val MIN_TASK_DISMISS_ANIMATION_DURATION: Long = 300 - private const val MAX_TASK_DISMISS_ANIMATION_DURATION: Long = 600 - - private const val TASK_DISMISS_VIBRATION_PRIMITIVE: Int = - VibrationEffect.Composition.PRIMITIVE_TICK - private const val TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE: Float = 1f - private val TASK_DISMISS_VIBRATION_FALLBACK: VibrationEffect = - VibrationConstants.EFFECT_TEXTURE_TICK - } -} diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java index 7e5afc37a0..5d4f1db1a9 100644 --- a/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java +++ b/quickstep/src/com/android/quickstep/fallback/RecentsDragLayer.java @@ -21,8 +21,9 @@ import android.content.Context; import android.util.AttributeSet; import com.android.launcher3.statemanager.StatefulContainer; +import com.android.launcher3.uioverrides.touchcontrollers.TaskViewDismissTouchController; +import com.android.launcher3.uioverrides.touchcontrollers.TaskViewLaunchTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TaskViewRecentsTouchContext; -import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController; import com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchControllerDeprecated; import com.android.launcher3.util.TouchController; import com.android.launcher3.views.BaseDragLayer; @@ -54,10 +55,18 @@ public class RecentsDragLayer(mContainer, - mTaskViewRecentsTouchContext) : new TaskViewTouchControllerDeprecated<>( - mContainer, mTaskViewRecentsTouchContext), - new FallbackNavBarTouchController(mContainer)}; + mControllers = enableExpressiveDismissTaskMotion() + ? new TouchController[]{ + new TaskViewLaunchTouchController<>(mContainer, + mTaskViewRecentsTouchContext), + new TaskViewDismissTouchController<>(mContainer, + mTaskViewRecentsTouchContext), + new FallbackNavBarTouchController(mContainer) + } + : new TouchController[]{ + new TaskViewTouchControllerDeprecated<>(mContainer, + mTaskViewRecentsTouchContext), + new FallbackNavBarTouchController(mContainer) + }; } } diff --git a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt index 88ef0a8bcb..e72ccbfb7b 100644 --- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt @@ -306,6 +306,10 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler { if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE else SingleAxisSwipeDetector.DIRECTION_POSITIVE + override fun getDownDirection(isRtl: Boolean): Int = + if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE + else SingleAxisSwipeDetector.DIRECTION_NEGATIVE + override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean = if (isRtl) displacement < 0 else displacement > 0 diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java index c0b697daf8..c1e1c2b115 100644 --- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java +++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java @@ -309,6 +309,12 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements return SingleAxisSwipeDetector.DIRECTION_POSITIVE; } + @Override + public int getDownDirection(boolean isRtl) { + // Ignore rtl since it only affects X value displacement, Y displacement doesn't change + return SingleAxisSwipeDetector.DIRECTION_NEGATIVE; + } + @Override public boolean isGoingUp(float displacement, boolean isRtl) { // Ignore rtl since it only affects X value displacement, Y displacement doesn't change diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt index b8d0412eff..78f9a0a556 100644 --- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt @@ -332,6 +332,9 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler { /** @return Given [.getUpDownSwipeDirection], whether POSITIVE or NEGATIVE is up. */ fun getUpDirection(isRtl: Boolean): Int + /** @return Given [.getUpDownSwipeDirection], whether POSITIVE or NEGATIVE is down. */ + fun getDownDirection(isRtl: Boolean): Int + /** @return Whether the displacement is going towards the top of the screen. */ fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt index bc91911749..3fb4f54ae5 100644 --- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt +++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt @@ -351,6 +351,10 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() { if (isRtl) SingleAxisSwipeDetector.DIRECTION_POSITIVE else SingleAxisSwipeDetector.DIRECTION_NEGATIVE + override fun getDownDirection(isRtl: Boolean): Int = + if (isRtl) SingleAxisSwipeDetector.DIRECTION_NEGATIVE + else SingleAxisSwipeDetector.DIRECTION_POSITIVE + override fun isGoingUp(displacement: Float, isRtl: Boolean): Boolean = if (isRtl) displacement > 0 else displacement < 0 diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 0eb9dbc353..594c68e00d 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -136,6 +136,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; import androidx.core.graphics.ColorUtils; +import androidx.dynamicanimation.animation.SpringAnimation; import com.android.internal.jank.Cuj; import com.android.launcher3.AbstractFloatingView; @@ -165,6 +166,7 @@ import com.android.launcher3.statemanager.StatefulContainer; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.shared.TestProtocol; import com.android.launcher3.touch.OverScroll; +import com.android.launcher3.touch.SingleAxisSwipeDetector; import com.android.launcher3.util.CancellableTask; import com.android.launcher3.util.DynamicResource; import com.android.launcher3.util.IntArray; @@ -238,6 +240,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import kotlin.Unit; import kotlin.collections.CollectionsKt; +import kotlin.jvm.functions.Function0; import kotlinx.coroutines.CoroutineScope; import java.util.ArrayList; @@ -5694,6 +5697,13 @@ public abstract class RecentsView< mTempRect, mContainer.getDeviceProfile(), mTempPointF); } + /** + * Clears the existing PendingAnimation. + */ + public void clearPendingAnimation() { + mPendingAnimation = null; + } + public PendingAnimation createTaskLaunchAnimation( TaskView taskView, long duration, Interpolator interpolator) { if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) { @@ -5851,6 +5861,10 @@ public abstract class RecentsView< mEnableDrawingLiveTile = enableDrawingLiveTile; } + public boolean getEnableDrawingLiveTile() { + return mEnableDrawingLiveTile; + } + public void redrawLiveTile() { runActionOnRemoteHandles(remoteTargetHandle -> { TransformParams params = remoteTargetHandle.getTransformParams(); @@ -6884,6 +6898,19 @@ public abstract class RecentsView< return Typeface.Builder.NORMAL_WEIGHT; } + /** + * 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. + */ + public SpringAnimation createTaskDismissSettlingSpringAnimation(TaskView draggedTaskView, + float velocity, boolean isDismissing, SingleAxisSwipeDetector detector, + int dismissLength, Function0 onEndRunnable) { + return mUtils.createTaskDismissSettlingSpringAnimation(draggedTaskView, velocity, + isDismissing, detector, dismissLength, onEndRunnable); + } + public interface TaskLaunchListener { void onTaskLaunched(); } diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt index bce5a5e883..055e3b13ed 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt +++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt @@ -19,14 +19,21 @@ package com.android.quickstep.views import android.graphics.Rect import android.view.View import androidx.core.view.children +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce import com.android.launcher3.Flags.enableLargeDesktopWindowingTile import com.android.launcher3.Flags.enableSeparateExternalDisplayTasks +import com.android.launcher3.R +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.isExternalDisplay import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA import com.android.systemui.shared.recents.model.ThumbnailData import java.util.function.BiConsumer +import kotlin.math.abs /** * Helper class for [RecentsView]. This util class contains refactored and extracted functions from @@ -291,6 +298,58 @@ 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. + */ + fun createTaskDismissSettlingSpringAnimation( + draggedTaskView: TaskView?, + velocity: Float, + isDismissing: Boolean, + detector: SingleAxisSwipeDetector, + dismissLength: Int, + onEndRunnable: () -> Unit, + ): SpringAnimation? { + draggedTaskView ?: return null + val taskDismissFloatProperty = + 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) + } + recentsView.redrawLiveTile() + } + } + .addEndListener { _, _, _, _ -> + if (isDismissing) { + recentsView.dismissTask( + draggedTaskView, + /* animateTaskView = */ false, + /* removeTask = */ true, + ) + } + onEndRunnable() + } + } + companion object { val TEMP_RECT = Rect() } diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt index d4bae78124..fee6856718 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.kt +++ b/quickstep/src/com/android/quickstep/views/TaskView.kt @@ -214,7 +214,7 @@ constructor( get() = pagedOrientationHandler.getPrimaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y) - protected val secondaryDismissTranslationProperty: FloatProperty + val secondaryDismissTranslationProperty: FloatProperty get() = pagedOrientationHandler.getSecondaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y)