From 267137fd5e04d8c07f962b32bc2b84c73514f46c Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Thu, 2 Feb 2023 15:03:39 -0800 Subject: [PATCH] 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 --- .../ui/binder/KeyguardBottomAreaViewBinder.kt | 183 ++++++++++++------ 1 file changed, 122 insertions(+), 61 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt index 3319f9d467ecc..ab009f4a6a660 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -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 + } + } } }