Merge "Refactor TaskViewTouchController to separately handle dismiss and launch." into main
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 690dec4..907e45c 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -149,8 +149,9 @@
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;
@@ -687,7 +688,8 @@
}
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 0000000..99b962b
--- /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<CONTAINER>(
+ 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 0000000..c740dad
--- /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<CONTAINER>(
+ 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 c996f34..0000000
--- 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<CONTAINER>(
- 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 7e5afc3..5d4f1db 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.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 @@
@Override
public void recreateControllers() {
- mControllers = new TouchController[]{
- enableExpressiveDismissTaskMotion() ? new TaskViewTouchController<>(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 88ef0a8..e72ccbf 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -306,6 +306,10 @@
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 c0b697d..c1e1c2b 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -310,6 +310,12 @@
}
@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
return displacement < 0;
diff --git a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
index b8d0412..78f9a0a 100644
--- a/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/RecentsPagedOrientationHandler.kt
@@ -332,6 +332,9 @@
/** @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 bc91911..3fb4f54 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -351,6 +351,10 @@
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 9c8b249..a8f9dd4 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.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.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;
@@ -240,6 +242,7 @@
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
+import kotlin.jvm.functions.Function0;
import kotlinx.coroutines.CoroutineScope;
import java.util.ArrayList;
@@ -5703,6 +5706,13 @@
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) {
@@ -5864,6 +5874,10 @@
mEnableDrawingLiveTile = enableDrawingLiveTile;
}
+ public boolean getEnableDrawingLiveTile() {
+ return mEnableDrawingLiveTile;
+ }
+
public void redrawLiveTile() {
runActionOnRemoteHandles(remoteTargetHandle -> {
TransformParams params = remoteTargetHandle.getTransformParams();
@@ -6897,6 +6911,19 @@
return Typeface.Builder.NORMAL_WEIGHT;
}
+ /**
+ * 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.
+ */
+ public SpringAnimation createTaskDismissSettlingSpringAnimation(TaskView draggedTaskView,
+ float velocity, boolean isDismissing, SingleAxisSwipeDetector detector,
+ int dismissLength, Function0<Unit> 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 94e8c03..f610335 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -19,14 +19,21 @@
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
@@ -294,6 +301,58 @@
}
}
+ /**
+ * 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.
+ */
+ 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 ce0efb6..062955b 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -213,7 +213,7 @@
get() =
pagedOrientationHandler.getPrimaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y)
- protected val secondaryDismissTranslationProperty: FloatProperty<TaskView>
+ val secondaryDismissTranslationProperty: FloatProperty<TaskView>
get() =
pagedOrientationHandler.getSecondaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y)