Add Haptic feedback to task dismiss animations.
Fix: 389640582
Test: Manual.
Flag: com.android.launcher3.enable_expressive_dismiss_task_motion
Change-Id: Icfbc875b4021136f612d8227f5cbbc1491e54801
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index 77a05c1..98737a5 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -26,10 +26,12 @@
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 @@
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 @@
}
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 @@
companion object {
private const val DISMISS_THRESHOLD_FRACTION = 0.5f
+ private const val DISMISS_THRESHOLD_HAPTIC_RANGE = 10f
}
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index d37a3f9..cd52c95 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -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 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 @@
// 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 @@
lastPosition = value
if (startSettling) {
neighborsToSettle.setStartVelocity(velocity).animateToFinalPosition(0f)
+ playDismissSettlingHaptic(velocity)
}
}
@@ -500,6 +506,27 @@
)
}
+ /**
+ * 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()
}