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
This commit is contained in:
Alejandro Nijamkin
2022-12-11 14:48:19 -08:00
parent 832d609613
commit f2add476cc
8 changed files with 143 additions and 3 deletions

View File

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

View File

@@ -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] -->

View File

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

View File

@@ -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) {
view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
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,

View File

@@ -193,6 +193,7 @@ constructor(
isClickable = isClickable,
isActivated = activationState is ActivationState.Active,
isSelected = isSelected,
useLongPress = quickAffordanceInteractor.useLongPress,
)
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
}

View File

@@ -30,6 +30,7 @@ data class KeyguardQuickAffordanceViewModel(
val isClickable: Boolean = false,
val isActivated: Boolean = false,
val isSelected: Boolean = false,
val useLongPress: Boolean = false,
) {
data class OnClickedParameters(
val configKey: String,

View File

@@ -1327,7 +1327,9 @@ public final class NotificationPanelViewController implements Dumpable {
mKeyguardBottomArea.init(
mKeyguardBottomAreaViewModel,
mFalsingManager,
mLockIconViewController
mLockIconViewController,
stringResourceId ->
mKeyguardIndicationController.showTransientIndication(stringResourceId)
);
}

View File

@@ -23,6 +23,7 @@ import android.view.ViewGroup
import android.view.ViewPropertyAnimator
import android.view.WindowInsets
import android.widget.FrameLayout
import androidx.annotation.StringRes
import com.android.keyguard.LockIconViewController
import com.android.systemui.R
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder
@@ -50,6 +51,10 @@ constructor(
defStyleRes,
) {
interface MessageDisplayer {
fun display(@StringRes stringResourceId: Int)
}
private var ambientIndicationArea: View? = null
private lateinit var binding: KeyguardBottomAreaViewBinder.Binding
private var lockIconViewController: LockIconViewController? = null
@@ -59,13 +64,16 @@ constructor(
viewModel: KeyguardBottomAreaViewModel,
falsingManager: FalsingManager? = null,
lockIconViewController: LockIconViewController? = null,
messageDisplayer: MessageDisplayer? = null,
) {
binding =
bind(
this,
viewModel,
falsingManager,
)
) {
messageDisplayer?.display(it)
}
this.lockIconViewController = lockIconViewController
}