Long-press not required for mouse or stylus.

When interacting with lock screen shortcuts, a long-press is required,
mainly to help protect against false clicks (e.g. "falsing"). When using
a stylus pen or a mouse, it is no longer a requirement, as those pointer types offer a  much
greater degree of confidence that the click was not a false click.

This CL takes care of it by checking the type of pointer and, if it's a
stylus or a mouse, allowing a simple click gesture to happen, instead of the
complicated long-press.

Fix: 267534467
Test: manually tested with a stylus - simple "launch" affordances like
camera work and require a single tap. Complex "toggle" affordances like
do not disturb toggle on and off, requiring a single tap
Test: manually tested without a stylus. "Launch" and "toggle"
affordances still require a long-press, the animation still works. When
tapping them with a short click, the shake animation/vibration and
educational text still appear as expected.

Change-Id: I92270e554f41b0066f5e8c8fec52c5d7c6c93b29
This commit is contained in:
Alejandro Nijamkin
2023-02-02 15:03:39 -08:00
parent 4f197f9a85
commit 267137fd5e

View File

@@ -381,82 +381,87 @@ object KeyguardBottomAreaViewBinder {
return when (event?.actionMasked) {
MotionEvent.ACTION_DOWN ->
if (viewModel.configKey != null) {
longPressAnimator =
view
.animate()
.scaleX(PRESSED_SCALE)
.scaleY(PRESSED_SCALE)
.setDuration(longPressDurationMs)
.withEndAction {
view.setOnClickListener {
vibratorHelper?.vibrate(
if (viewModel.isActivated) {
Vibrations.Activated
} else {
Vibrations.Deactivated
}
)
viewModel.onClicked(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = viewModel.configKey,
expandable = Expandable.fromView(view),
)
)
if (isUsingAccurateTool(event)) {
// For accurate tool types (stylus, mouse, etc.), we don't require a
// long-press.
} else {
// When not using a stylus, we require a long-press to activate the
// quick affordance, mostly to do "falsing" (e.g. protect from false
// clicks in the pocket/bag).
longPressAnimator =
view
.animate()
.scaleX(PRESSED_SCALE)
.scaleY(PRESSED_SCALE)
.setDuration(longPressDurationMs)
.withEndAction {
dispatchClick(viewModel.configKey)
cancel()
}
view.performClick()
view.setOnClickListener(null)
cancel()
}
}
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()) {
if (!isUsingAccurateTool(event)) {
// Moving too far while performing a long-press gesture cancels that
// gesture.
val distanceMoved = distanceMoved(event)
if (distanceMoved > ViewConfiguration.getTouchSlop()) {
cancel()
}
}
true
}
MotionEvent.ACTION_UP -> {
cancel(
onAnimationEnd =
if (event.eventTime - event.downTime < longPressDurationMs) {
Runnable {
messageDisplayer.invoke(
R.string.keyguard_affordance_press_too_short
)
val amplitude =
view.context.resources
.getDimensionPixelSize(
R.dimen.keyguard_affordance_shake_amplitude
)
.toFloat()
val shakeAnimator =
ObjectAnimator.ofFloat(
view,
"translationX",
-amplitude / 2,
amplitude / 2,
if (isUsingAccurateTool(event)) {
// When using an accurate tool type (stylus, mouse, etc.), we don't require
// a long-press gesture to activate the quick affordance. Therefore, lifting
// the pointer performs a click.
if (
viewModel.configKey != null &&
distanceMoved(event) <= ViewConfiguration.getTouchSlop()
) {
dispatchClick(viewModel.configKey)
}
} else {
// When not using a stylus, lifting the finger/pointer will actually cancel
// the long-press gesture. Calling cancel after the quick affordance was
// already long-press activated is a no-op, so it's safe to call from here.
cancel(
onAnimationEnd =
if (event.eventTime - event.downTime < longPressDurationMs) {
Runnable {
messageDisplayer.invoke(
R.string.keyguard_affordance_press_too_short
)
shakeAnimator.duration =
ShakeAnimationDuration.inWholeMilliseconds
shakeAnimator.interpolator =
CycleInterpolator(ShakeAnimationCycles)
shakeAnimator.start()
val amplitude =
view.context.resources
.getDimensionPixelSize(
R.dimen.keyguard_affordance_shake_amplitude
)
.toFloat()
val shakeAnimator =
ObjectAnimator.ofFloat(
view,
"translationX",
-amplitude / 2,
amplitude / 2,
)
shakeAnimator.duration =
ShakeAnimationDuration.inWholeMilliseconds
shakeAnimator.interpolator =
CycleInterpolator(ShakeAnimationCycles)
shakeAnimator.start()
vibratorHelper?.vibrate(Vibrations.Shake)
vibratorHelper?.vibrate(Vibrations.Shake)
}
} else {
null
}
} else {
null
}
)
)
}
true
}
MotionEvent.ACTION_CANCEL -> {
@@ -467,6 +472,28 @@ object KeyguardBottomAreaViewBinder {
}
}
private fun dispatchClick(
configKey: String,
) {
view.setOnClickListener {
vibratorHelper?.vibrate(
if (viewModel.isActivated) {
Vibrations.Activated
} else {
Vibrations.Deactivated
}
)
viewModel.onClicked(
KeyguardQuickAffordanceViewModel.OnClickedParameters(
configKey = configKey,
expandable = Expandable.fromView(view),
)
)
}
view.performClick()
view.setOnClickListener(null)
}
private fun cancel(onAnimationEnd: Runnable? = null) {
longPressAnimator?.cancel()
longPressAnimator = null
@@ -475,6 +502,40 @@ object KeyguardBottomAreaViewBinder {
companion object {
private const val PRESSED_SCALE = 1.5f
/**
* Returns `true` if the tool type at the given pointer index is an accurate tool (like
* stylus or mouse), which means we can trust it to not be a false click; `false`
* otherwise.
*/
private fun isUsingAccurateTool(
event: MotionEvent,
pointerIndex: Int = 0,
): Boolean {
return when (event.getToolType(pointerIndex)) {
MotionEvent.TOOL_TYPE_STYLUS -> true
MotionEvent.TOOL_TYPE_MOUSE -> true
else -> false
}
}
/**
* Returns the amount of distance the pointer moved since the historical record at the
* [since] index.
*/
private fun distanceMoved(
event: MotionEvent,
since: Int = 0,
): Float {
return if (event.historySize > 0) {
sqrt(
(event.y - event.getHistoricalY(since)).pow(2) +
(event.x - event.getHistoricalX(since)).pow(2)
)
} else {
0f
}
}
}
}