Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 667c4952 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

Quick affordance gesture polish and haptics.

- UX polish for quick affordance long press failure: updated string to
  match specified copy from writer and delay the shake animation until
  after the cancel animation is complete
- Adds haptic feedback to activating on, deactivating, and to long press
  failure based on spec sent by scottj@

Fix: 263293154, 262776614
Test: manually verified all four scenarios: no haptic feedback for quick
affordances that don't support activation mode, activation,
deactivation, and shake animation when clicking or holding for too short
a time.

Change-Id: Iada8441892c3db328489f798f59128ee6531560b
parent 6680d5cd
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -2751,7 +2751,7 @@
    Error message shown when a button should be pressed and held to activate it, usually shown when
    the user attempted to tap the button or held it for too short a time. [CHAR LIMIT=32].
    -->
    <string name="keyguard_affordance_press_too_short">Press and hold to activate</string>
    <string name="keyguard_affordance_press_too_short">Touch &amp; hold to open</string>

    <!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] -->
    <string name="rear_display_bottom_sheet_cancel">Cancel</string>
+123 −21
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.binder

import android.annotation.SuppressLint
import android.graphics.drawable.Animatable2
import android.os.VibrationEffect
import android.util.Size
import android.util.TypedValue
import android.view.MotionEvent
@@ -43,8 +44,11 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.kotlin.pairwise
import kotlin.math.pow
import kotlin.math.sqrt
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@@ -93,6 +97,7 @@ object KeyguardBottomAreaViewBinder {
        view: ViewGroup,
        viewModel: KeyguardBottomAreaViewModel,
        falsingManager: FalsingManager?,
        vibratorHelper: VibratorHelper?,
        messageDisplayer: (Int) -> Unit,
    ): Binding {
        val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
@@ -118,10 +123,23 @@ object KeyguardBottomAreaViewBinder {
                            viewModel = buttonModel,
                            falsingManager = falsingManager,
                            messageDisplayer = messageDisplayer,
                            vibratorHelper = vibratorHelper,
                        )
                    }
                }

                launch {
                    viewModel.startButton
                        .map { it.isActivated }
                        .pairwise()
                        .collect { (prev, next) ->
                            when {
                                !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated)
                                prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated)
                            }
                        }
                }

                launch {
                    viewModel.endButton.collect { buttonModel ->
                        updateButton(
@@ -129,10 +147,23 @@ object KeyguardBottomAreaViewBinder {
                            viewModel = buttonModel,
                            falsingManager = falsingManager,
                            messageDisplayer = messageDisplayer,
                            vibratorHelper = vibratorHelper,
                        )
                    }
                }

                launch {
                    viewModel.endButton
                        .map { it.isActivated }
                        .pairwise()
                        .collect { (prev, next) ->
                            when {
                                !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated)
                                prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated)
                            }
                        }
                }

                launch {
                    viewModel.isOverlayContainerVisible.collect { isVisible ->
                        overlayContainer.visibility =
@@ -239,6 +270,7 @@ object KeyguardBottomAreaViewBinder {
        viewModel: KeyguardQuickAffordanceViewModel,
        falsingManager: FalsingManager?,
        messageDisplayer: (Int) -> Unit,
        vibratorHelper: VibratorHelper?,
    ) {
        if (!viewModel.isVisible) {
            view.isVisible = false
@@ -312,7 +344,9 @@ object KeyguardBottomAreaViewBinder {
        view.isClickable = viewModel.isClickable
        if (viewModel.isClickable) {
            if (viewModel.useLongPress) {
                view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer))
                view.setOnTouchListener(
                    OnTouchListener(view, viewModel, messageDisplayer, vibratorHelper)
                )
            } else {
                view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
            }
@@ -328,6 +362,7 @@ object KeyguardBottomAreaViewBinder {
        private val view: View,
        private val viewModel: KeyguardQuickAffordanceViewModel,
        private val messageDisplayer: (Int) -> Unit,
        private val vibratorHelper: VibratorHelper?,
    ) : View.OnTouchListener {

        private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
@@ -376,25 +411,38 @@ object KeyguardBottomAreaViewBinder {
                    true
                }
                MotionEvent.ACTION_UP -> {
                    cancel(
                        onAnimationEnd =
                            if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) {
                        messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short)
                        val shakeAnimator =
                            ObjectAnimator.ofFloat(
                                view,
                                "translationX",
                                0f,
                                Runnable {
                                    messageDisplayer.invoke(
                                        R.string.keyguard_affordance_press_too_short
                                    )
                                    val amplitude =
                                        view.context.resources
                                            .getDimensionPixelSize(
                                                R.dimen.keyguard_affordance_shake_amplitude
                                            )
                                    .toFloat(),
                                0f,
                                            .toFloat()
                                    val shakeAnimator =
                                        ObjectAnimator.ofFloat(
                                            view,
                                            "translationX",
                                            -amplitude / 2,
                                            amplitude / 2,
                                        )
                        shakeAnimator.duration = 300
                        shakeAnimator.interpolator = CycleInterpolator(5f)
                                    shakeAnimator.duration =
                                        ShakeAnimationDuration.inWholeMilliseconds
                                    shakeAnimator.interpolator =
                                        CycleInterpolator(ShakeAnimationCycles)
                                    shakeAnimator.start()

                                    vibratorHelper?.vibrate(Vibrations.Shake)
                                }
                    cancel()
                            } else {
                                null
                            }
                    )
                    true
                }
                MotionEvent.ACTION_CANCEL -> {
@@ -405,11 +453,11 @@ object KeyguardBottomAreaViewBinder {
            }
        }

        private fun cancel() {
        private fun cancel(onAnimationEnd: Runnable? = null) {
            downTimestamp = 0L
            longPressAnimator?.cancel()
            longPressAnimator = null
            view.animate().scaleX(1f).scaleY(1f)
            view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd)
        }

        companion object {
@@ -461,4 +509,58 @@ object KeyguardBottomAreaViewBinder {
        val indicationTextSizePx: Int,
        val buttonSizePx: Size,
    )

    private val ShakeAnimationDuration = 300.milliseconds
    private val ShakeAnimationCycles = 5f

    object Vibrations {

        private const val SmallVibrationScale = 0.3f
        private const val BigVibrationScale = 0.6f

        val Shake =
            VibrationEffect.startComposition()
                .apply {
                    val vibrationDelayMs =
                        (ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2))
                            .toInt()
                    val vibrationCount = ShakeAnimationCycles.toInt() * 2
                    repeat(vibrationCount) {
                        addPrimitive(
                            VibrationEffect.Composition.PRIMITIVE_TICK,
                            SmallVibrationScale,
                            vibrationDelayMs,
                        )
                    }
                }
                .compose()

        val Activated =
            VibrationEffect.startComposition()
                .addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_TICK,
                    BigVibrationScale,
                    0,
                )
                .addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
                    0.1f,
                    0,
                )
                .compose()

        val Deactivated =
            VibrationEffect.startComposition()
                .addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_TICK,
                    BigVibrationScale,
                    0,
                )
                .addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
                    0.1f,
                    0,
                )
                .compose()
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -1370,8 +1370,8 @@ public final class NotificationPanelViewController implements Dumpable {
                mFalsingManager,
                mLockIconViewController,
                stringResourceId ->
                        mKeyguardIndicationController.showTransientIndication(stringResourceId)
        );
                        mKeyguardIndicationController.showTransientIndication(stringResourceId),
                mVibratorHelper);
    }

    @VisibleForTesting
+3 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder.bind
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.VibratorHelper

/**
 * Renders the bottom area of the lock-screen. Concerned primarily with the quick affordance UI
@@ -65,12 +66,14 @@ constructor(
        falsingManager: FalsingManager? = null,
        lockIconViewController: LockIconViewController? = null,
        messageDisplayer: MessageDisplayer? = null,
        vibratorHelper: VibratorHelper? = null,
    ) {
        binding =
            bind(
                this,
                viewModel,
                falsingManager,
                vibratorHelper,
            ) {
                messageDisplayer?.display(it)
            }