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

Commit f2add476 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

Long-press gesture for lock screen affordances.

If the flag for customizable lock screen quick affordances is
enabled, the required geture to activate a lock screen quick affordance
is now a standard long press instead of a click.

Fix: 254857466
Test: long-pressing the button causes it to scale-animate and then
activate. A "tap" sound plays when it activates. Moving the finger too
far off the touch target while holding down cancels the gesture.
Releasing the finger too quickly causes the view to shake-animate and a
corrective message to appear in the indication area.

Change-Id: I130169d6dcdb6779bb914b0a3c0d3b0fc522567d
parent 832d6096
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -758,6 +758,8 @@
    <dimen name="keyguard_affordance_fixed_height">48dp</dimen>
    <dimen name="keyguard_affordance_fixed_width">48dp</dimen>
    <dimen name="keyguard_affordance_fixed_radius">24dp</dimen>
    <!-- Amount the button should shake when it's not long-pressed for long enough. -->
    <dimen name="keyguard_affordance_shake_amplitude">8dp</dimen>

    <dimen name="keyguard_affordance_horizontal_offset">32dp</dimen>
    <dimen name="keyguard_affordance_vertical_offset">32dp</dimen>
+6 −0
Original line number Diff line number Diff line
@@ -2743,6 +2743,12 @@
    -->
    <string name="keyguard_affordance_enablement_dialog_home_instruction_2">&#8226; At least one device is available</string>

    <!--
    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>

    <!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] -->
    <string name="rear_display_bottom_sheet_cancel">Cancel</string>
    <!-- Text for the user to confirm they flipped the device around. [CHAR_LIMIT=NONE] -->
+8 −0
Original line number Diff line number Diff line
@@ -61,6 +61,14 @@ constructor(
    private val isUsingRepository: Boolean
        get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)

    /**
     * Whether the UI should use the long press gesture to activate quick affordances.
     *
     * If `false`, the UI goes back to using single taps.
     */
    val useLongPress: Boolean
        get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)

    /** Returns an observable for the quick affordance at the given position. */
    fun quickAffordance(
        position: KeyguardQuickAffordancePosition
+113 −1
Original line number Diff line number Diff line
@@ -16,14 +16,19 @@

package com.android.systemui.keyguard.ui.binder

import android.annotation.SuppressLint
import android.graphics.drawable.Animatable2
import android.util.Size
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.widget.ImageView
import android.widget.TextView
import androidx.core.animation.CycleInterpolator
import androidx.core.animation.ObjectAnimator
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.Lifecycle
@@ -38,6 +43,9 @@ 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 kotlin.math.pow
import kotlin.math.sqrt
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
@@ -51,6 +59,7 @@ import kotlinx.coroutines.launch
 * view-binding, binding each view only once. It is okay and expected for the same instance of the
 * view-model to be reused for multiple view/view-binder bindings.
 */
@OptIn(ExperimentalCoroutinesApi::class)
object KeyguardBottomAreaViewBinder {

    private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
@@ -84,6 +93,7 @@ object KeyguardBottomAreaViewBinder {
        view: ViewGroup,
        viewModel: KeyguardBottomAreaViewModel,
        falsingManager: FalsingManager?,
        messageDisplayer: (Int) -> Unit,
    ): Binding {
        val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
        val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container)
@@ -107,6 +117,7 @@ object KeyguardBottomAreaViewBinder {
                            view = startButton,
                            viewModel = buttonModel,
                            falsingManager = falsingManager,
                            messageDisplayer = messageDisplayer,
                        )
                    }
                }
@@ -117,6 +128,7 @@ object KeyguardBottomAreaViewBinder {
                            view = endButton,
                            viewModel = buttonModel,
                            falsingManager = falsingManager,
                            messageDisplayer = messageDisplayer,
                        )
                    }
                }
@@ -221,10 +233,12 @@ object KeyguardBottomAreaViewBinder {
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun updateButton(
        view: ImageView,
        viewModel: KeyguardQuickAffordanceViewModel,
        falsingManager: FalsingManager?,
        messageDisplayer: (Int) -> Unit,
    ) {
        if (!viewModel.isVisible) {
            view.isVisible = false
@@ -297,14 +311,112 @@ object KeyguardBottomAreaViewBinder {

        view.isClickable = viewModel.isClickable
        if (viewModel.isClickable) {
            if (viewModel.useLongPress) {
                view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer))
            } else {
                view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
            }
        } else {
            view.setOnClickListener(null)
            view.setOnTouchListener(null)
        }

        view.isSelected = viewModel.isSelected
    }

    private class OnTouchListener(
        private val view: View,
        private val viewModel: KeyguardQuickAffordanceViewModel,
        private val messageDisplayer: (Int) -> Unit,
    ) : View.OnTouchListener {

        private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong()
        private var longPressAnimator: ViewPropertyAnimator? = null
        private var downTimestamp = 0L

        @SuppressLint("ClickableViewAccessibility")
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            return when (event?.actionMasked) {
                MotionEvent.ACTION_DOWN ->
                    if (viewModel.configKey != null) {
                        downTimestamp = System.currentTimeMillis()
                        longPressAnimator =
                            view
                                .animate()
                                .scaleX(PRESSED_SCALE)
                                .scaleY(PRESSED_SCALE)
                                .setDuration(longPressDurationMs)
                                .withEndAction {
                                    view.setOnClickListener {
                                        viewModel.onClicked(
                                            KeyguardQuickAffordanceViewModel.OnClickedParameters(
                                                configKey = viewModel.configKey,
                                                expandable = Expandable.fromView(view),
                                            )
                                        )
                                    }
                                    view.performClick()
                                    view.setOnClickListener(null)
                                }
                        true
                    } else {
                        false
                    }
                MotionEvent.ACTION_MOVE -> {
                    if (event.historySize > 0) {
                        val distance =
                            sqrt(
                                (event.y - event.getHistoricalY(0)).pow(2) +
                                    (event.x - event.getHistoricalX(0)).pow(2)
                            )
                        if (distance > ViewConfiguration.getTouchSlop()) {
                            cancel()
                        }
                    }
                    true
                }
                MotionEvent.ACTION_UP -> {
                    if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) {
                        messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short)
                        val shakeAnimator =
                            ObjectAnimator.ofFloat(
                                view,
                                "translationX",
                                0f,
                                view.context.resources
                                    .getDimensionPixelSize(
                                        R.dimen.keyguard_affordance_shake_amplitude
                                    )
                                    .toFloat(),
                                0f,
                            )
                        shakeAnimator.duration = 300
                        shakeAnimator.interpolator = CycleInterpolator(5f)
                        shakeAnimator.start()
                    }
                    cancel()
                    true
                }
                MotionEvent.ACTION_CANCEL -> {
                    cancel()
                    true
                }
                else -> false
            }
        }

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

        companion object {
            private const val PRESSED_SCALE = 1.5f
        }
    }

    private class OnClickListener(
        private val viewModel: KeyguardQuickAffordanceViewModel,
        private val falsingManager: FalsingManager,
+1 −0
Original line number Diff line number Diff line
@@ -193,6 +193,7 @@ constructor(
                    isClickable = isClickable,
                    isActivated = activationState is ActivationState.Active,
                    isSelected = isSelected,
                    useLongPress = quickAffordanceInteractor.useLongPress,
                )
            is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
        }
Loading