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:
@@ -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>
|
||||
|
||||
@@ -2743,6 +2743,12 @@
|
||||
-->
|
||||
<string name="keyguard_affordance_enablement_dialog_home_instruction_2">• 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] -->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -193,6 +193,7 @@ constructor(
|
||||
isClickable = isClickable,
|
||||
isActivated = activationState is ActivationState.Active,
|
||||
isSelected = isSelected,
|
||||
useLongPress = quickAffordanceInteractor.useLongPress,
|
||||
)
|
||||
is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1327,7 +1327,9 @@ public final class NotificationPanelViewController implements Dumpable {
|
||||
mKeyguardBottomArea.init(
|
||||
mKeyguardBottomAreaViewModel,
|
||||
mFalsingManager,
|
||||
mLockIconViewController
|
||||
mLockIconViewController,
|
||||
stringResourceId ->
|
||||
mKeyguardIndicationController.showTransientIndication(stringResourceId)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user