Add Haptic feedback to task dismiss animations.

Fix: 389640582
Test: Manual.
Flag: com.android.launcher3.enable_expressive_dismiss_task_motion
Change-Id: Icfbc875b4021136f612d8227f5cbbc1491e54801
This commit is contained in:
Pat Manning
2025-02-12 12:19:23 +00:00
parent ce9ad064d6
commit fe7eb99dc4
2 changed files with 53 additions and 1 deletions
@@ -26,10 +26,12 @@ 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.MSDLPlayerWrapper
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 com.google.android.msdl.data.model.MSDLToken
import kotlin.math.abs
import kotlin.math.sign
@@ -53,6 +55,7 @@ CONTAINER : RecentsViewContainer {
private var springAnimation: SpringAnimation? = null
private var dismissLength: Int = 0
private var verticalFactor: Int = 0
private var hasDismissThresholdHapticRun = false
private var initialDisplacement: Float = 0f
private fun canInterceptTouch(ev: MotionEvent): Boolean =
@@ -159,9 +162,30 @@ CONTAINER : RecentsViewContainer {
}
recentsView.redrawLiveTile()
}
playDismissThresholdHaptic(displacement)
return true
}
/**
* Play a haptic to alert the user they have passed the dismiss threshold.
*
* <p>Check within a range of the threshold value, as the drag event does not necessarily happen
* at the exact threshold's displacement.
*/
private fun playDismissThresholdHaptic(displacement: Float) {
val dismissThreshold = (DISMISS_THRESHOLD_FRACTION * dismissLength * verticalFactor)
val inHapticRange =
displacement >= (dismissThreshold - DISMISS_THRESHOLD_HAPTIC_RANGE) &&
displacement <= (dismissThreshold + DISMISS_THRESHOLD_HAPTIC_RANGE)
if (!inHapticRange) {
hasDismissThresholdHapticRun = false
} else if (!hasDismissThresholdHapticRun) {
MSDLPlayerWrapper.INSTANCE.get(recentsView.context)
.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
hasDismissThresholdHapticRun = true
}
}
override fun onDragEnd(velocity: Float) {
val taskBeingDragged = taskBeingDragged ?: return
@@ -208,5 +232,6 @@ CONTAINER : RecentsViewContainer {
companion object {
private const val DISMISS_THRESHOLD_FRACTION = 0.5f
private const val DISMISS_THRESHOLD_HAPTIC_RANGE = 10f
}
}
@@ -17,6 +17,7 @@
package com.android.quickstep.views
import android.graphics.Rect
import android.os.VibrationAttributes
import android.view.View
import androidx.core.view.children
import androidx.dynamicanimation.animation.FloatPropertyCompat
@@ -26,14 +27,18 @@ 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.Utilities.boundToRange
import com.android.launcher3.touch.SingleAxisSwipeDetector
import com.android.launcher3.util.DynamicResource
import com.android.launcher3.util.IntArray
import com.android.launcher3.util.MSDLPlayerWrapper
import com.android.quickstep.util.GroupTask
import com.android.quickstep.util.TaskGridNavHelper
import com.android.quickstep.util.isExternalDisplay
import com.android.quickstep.views.RecentsView.RUNNING_TASK_ATTACH_ALPHA
import com.android.systemui.shared.recents.model.ThumbnailData
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.domain.InteractionProperties
import java.util.function.BiConsumer
import kotlin.math.abs
@@ -378,7 +383,7 @@ class RecentsViewUtils(private val recentsView: RecentsView<*, *>) {
// negative displacement to positive displacement). We do not check for an exact value
// to compare to, as the update listener does not necessarily hit every value (e.g. a
// value of zero). Do not check again once it has started settling, as a spring can
// bounce past the origin multiple times depending on the stifness and damping ratio.
// bounce past the origin multiple times depending on the stiffness and damping ratio.
if (startSettling) return@addUpdateListener
if (lastPosition < 0 && value >= 0) {
startSettling = true
@@ -386,6 +391,7 @@ class RecentsViewUtils(private val recentsView: RecentsView<*, *>) {
lastPosition = value
if (startSettling) {
neighborsToSettle.setStartVelocity(velocity).animateToFinalPosition(0f)
playDismissSettlingHaptic(velocity)
}
}
@@ -500,6 +506,27 @@ class RecentsViewUtils(private val recentsView: RecentsView<*, *>) {
)
}
/**
* Plays a haptic as the dragged task view settles back into its rest state.
*
* <p>Haptic intensity is proportional to velocity.
*/
private fun playDismissSettlingHaptic(velocity: Float) {
val maxDismissSettlingVelocity =
recentsView.pagedOrientationHandler.getSecondaryDimension(recentsView)
MSDLPlayerWrapper.INSTANCE.get(recentsView.context)
.playToken(
MSDLToken.CANCEL,
InteractionProperties.DynamicVibrationScale(
boundToRange(velocity / maxDismissSettlingVelocity, 0f, 1f),
VibrationAttributes.Builder()
.setUsage(VibrationAttributes.USAGE_TOUCH)
.setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
.build(),
),
)
}
companion object {
val TEMP_RECT = Rect()
}