diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt index 77a05c1e86..98737a5568 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.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. + * + *

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 } } diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt index d37a3f9d31..cd52c952c8 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 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. + * + *

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() }