Merge "Refactor TaskViewTouchController to separately handle dismiss and launch." into main
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
+212
@@ -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
|
||||
}
|
||||
}
|
||||
+201
@@ -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
|
||||
}
|
||||
}
|
||||
-394
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user