Merge "Refactor TaskViewTouchController to separately handle dismiss and launch." into main

This commit is contained in:
Pat Manning
2025-02-04 03:47:03 -08:00
committed by Android (Google) Code Review
12 changed files with 536 additions and 403 deletions
@@ -149,8 +149,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;
@@ -687,7 +688,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));
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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<T extends Context & RecentsViewContainer
@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)
};
}
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
@@ -240,6 +242,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;
@@ -5703,6 +5706,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) {
@@ -5864,6 +5874,10 @@ public abstract class RecentsView<
mEnableDrawingLiveTile = enableDrawingLiveTile;
}
public boolean getEnableDrawingLiveTile() {
return mEnableDrawingLiveTile;
}
public void redrawLiveTile() {
runActionOnRemoteHandles(remoteTargetHandle -> {
TransformParams params = remoteTargetHandle.getTransformParams();
@@ -6897,6 +6911,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.
*
* <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();
}
@@ -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
@@ -294,6 +301,58 @@ 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.
*/
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()
}
@@ -213,7 +213,7 @@ constructor(
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)