Handling case when touchpad gesture is in progress
Introducing new GestureState that adds IN_PROGRESS.
While gesture is in progress, original education animation should jumpcut to 1st frame and stay there (freeze).
Bug: 346579074
Test: Open back gesture screen, start doing gesture and see animation freezing
Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial
Change-Id: I5bc62e7050bd53524f289e5c114590d7bbbc97e9
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index 94ff65e..4e1829a 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -26,6 +26,7 @@
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -49,6 +50,7 @@
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
@@ -61,6 +63,9 @@
import com.airbnb.lottie.compose.rememberLottieDynamicProperty
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
@@ -78,23 +83,49 @@
) {
val screenColors = rememberScreenColors()
BackHandler(onBack = onBack)
- var gestureDone by remember { mutableStateOf(false) }
+ var gestureState by remember { mutableStateOf(GestureState.NOT_STARTED) }
val swipeDistanceThresholdPx =
LocalContext.current.resources.getDimensionPixelSize(
com.android.internal.R.dimen.system_gestures_distance_threshold
)
val gestureHandler =
remember(swipeDistanceThresholdPx) {
- TouchpadGestureHandler(BACK, swipeDistanceThresholdPx, onDone = { gestureDone = true })
+ TouchpadGestureHandler(
+ BACK,
+ swipeDistanceThresholdPx,
+ onGestureStateChanged = { gestureState = it }
+ )
}
+ TouchpadGesturesHandlingBox(gestureHandler, gestureState) {
+ GestureTutorialContent(gestureState, onDoneButtonClicked, screenColors)
+ }
+}
+
+@Composable
+private fun TouchpadGesturesHandlingBox(
+ gestureHandler: TouchpadGestureHandler,
+ gestureState: GestureState,
+ modifier: Modifier = Modifier,
+ content: @Composable BoxScope.() -> Unit
+) {
Box(
modifier =
- Modifier.fillMaxSize()
+ modifier
+ .fillMaxSize()
// we need to use pointerInteropFilter because some info about touchpad gestures is
// only available in MotionEvent
- .pointerInteropFilter(onTouchEvent = gestureHandler::onMotionEvent)
+ .pointerInteropFilter(
+ onTouchEvent = { event ->
+ // FINISHED is the final state so we don't need to process touches anymore
+ if (gestureState != FINISHED) {
+ gestureHandler.onMotionEvent(event)
+ } else {
+ false
+ }
+ }
+ )
) {
- GestureTutorialContent(gestureDone, onDoneButtonClicked, screenColors)
+ content()
}
}
@@ -126,14 +157,14 @@
@Composable
private fun GestureTutorialContent(
- gestureDone: Boolean,
+ gestureState: GestureState,
onDoneButtonClicked: () -> Unit,
screenColors: TutorialScreenColors
) {
val animatedColor by
animateColorAsState(
targetValue =
- if (gestureDone) screenColors.successBackgroundColor
+ if (gestureState == FINISHED) screenColors.successBackgroundColor
else screenColors.backgroundColor,
animationSpec = tween(durationMillis = 150, easing = LinearEasing),
label = "backgroundColor"
@@ -148,7 +179,7 @@
Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
TutorialDescription(
titleTextId =
- if (gestureDone) R.string.touchpad_tutorial_gesture_done
+ if (gestureState == FINISHED) R.string.touchpad_tutorial_gesture_done
else R.string.touchpad_back_gesture_action_title,
titleColor = screenColors.titleColor,
bodyTextId = R.string.touchpad_back_gesture_guidance,
@@ -156,7 +187,7 @@
)
Spacer(modifier = Modifier.width(76.dp))
TutorialAnimation(
- gestureDone,
+ gestureState,
screenColors.animationProperties,
modifier = Modifier.weight(1f).padding(top = 8.dp)
)
@@ -189,27 +220,38 @@
@Composable
fun TutorialAnimation(
- gestureDone: Boolean,
+ gestureState: GestureState,
animationProperties: LottieDynamicProperties,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
- val resId = if (gestureDone) R.raw.trackpad_back_success else R.raw.trackpad_back_edu
+ val resId =
+ if (gestureState == FINISHED) R.raw.trackpad_back_success else R.raw.trackpad_back_edu
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(resId))
- val progress by
- animateLottieCompositionAsState(
- composition,
- iterations = if (gestureDone) 1 else LottieConstants.IterateForever
- )
+ val progress = progressForGestureState(composition, gestureState)
LottieAnimation(
composition = composition,
- progress = { progress },
+ progress = progress,
dynamicProperties = animationProperties
)
}
}
@Composable
+private fun progressForGestureState(
+ composition: LottieComposition?,
+ gestureState: GestureState
+): () -> Float {
+ if (gestureState == IN_PROGRESS) {
+ return { 0f } // when gesture is in progress, animation should freeze on 1st frame
+ } else {
+ val iterations = if (gestureState == FINISHED) 1 else LottieConstants.IterateForever
+ val animationState by animateLottieCompositionAsState(composition, iterations = iterations)
+ return { animationState }
+ }
+}
+
+@Composable
fun rememberColorFilterProperty(
layerName: String,
color: Color
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
index 1fa7a0c..6fa9bcd 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
@@ -17,23 +17,26 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
import android.view.MotionEvent
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
import kotlin.math.abs
/**
- * Monitor for touchpad gestures that calls [gestureDoneCallback] when gesture was successfully
- * done. All tracked motion events should be passed to [processTouchpadEvent]
+ * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState]
+ * changes. All tracked motion events should be passed to [processTouchpadEvent]
*/
interface TouchpadGestureMonitor {
val gestureDistanceThresholdPx: Int
- val gestureDoneCallback: () -> Unit
+ val gestureStateChangedCallback: (GestureState) -> Unit
fun processTouchpadEvent(event: MotionEvent)
}
class BackGestureMonitor(
override val gestureDistanceThresholdPx: Int,
- override val gestureDoneCallback: () -> Unit
+ override val gestureStateChangedCallback: (GestureState) -> Unit
) : TouchpadGestureMonitor {
private var xStart = 0f
@@ -44,13 +47,16 @@
MotionEvent.ACTION_DOWN -> {
if (isThreeFingerTouchpadSwipe(event)) {
xStart = event.x
+ gestureStateChangedCallback(IN_PROGRESS)
}
}
MotionEvent.ACTION_UP -> {
if (isThreeFingerTouchpadSwipe(event)) {
val distance = abs(event.x - xStart)
if (distance >= gestureDistanceThresholdPx) {
- gestureDoneCallback()
+ gestureStateChangedCallback(FINISHED)
+ } else {
+ gestureStateChangedCallback(NOT_STARTED)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
new file mode 100644
index 0000000..446875a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui.gesture
+
+enum class GestureState {
+ NOT_STARTED,
+ IN_PROGRESS,
+ FINISHED
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
index 4ae9c7b..190da62 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt
@@ -22,10 +22,10 @@
fun toMonitor(
swipeDistanceThresholdPx: Int,
- gestureDoneCallback: () -> Unit
+ onStateChanged: (GestureState) -> Unit
): TouchpadGestureMonitor {
return when (this) {
- BACK -> BackGestureMonitor(swipeDistanceThresholdPx, gestureDoneCallback)
+ BACK -> BackGestureMonitor(swipeDistanceThresholdPx, onStateChanged)
else -> throw IllegalArgumentException("Not implemented yet")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
index dc8471c..cac2a99 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
@@ -26,11 +26,11 @@
class TouchpadGestureHandler(
touchpadGesture: TouchpadGesture,
swipeDistanceThresholdPx: Int,
- onDone: () -> Unit
+ onGestureStateChanged: (GestureState) -> Unit
) {
private val gestureRecognition =
- touchpadGesture.toMonitor(swipeDistanceThresholdPx, gestureDoneCallback = onDone)
+ touchpadGesture.toMonitor(swipeDistanceThresholdPx, onStateChanged = onGestureStateChanged)
fun onMotionEvent(event: MotionEvent): Boolean {
// events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
index cf0db7b..8875b84 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -28,6 +28,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@@ -36,16 +39,19 @@
@RunWith(AndroidJUnit4::class)
class BackGestureMonitorTest : SysuiTestCase() {
- private var gestureDoneWasCalled = false
- private val gestureDoneCallback = { gestureDoneWasCalled = true }
- private val gestureMonitor = BackGestureMonitor(SWIPE_DISTANCE.toInt(), gestureDoneCallback)
+ private var gestureState = NOT_STARTED
+ private val gestureMonitor =
+ BackGestureMonitor(
+ gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
+ gestureStateChangedCallback = { gestureState = it }
+ )
companion object {
const val SWIPE_DISTANCE = 100f
}
@Test
- fun triggersGestureDoneForThreeFingerGestureRight() {
+ fun triggersGestureFinishedForThreeFingerGestureRight() {
val events =
listOf(
threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
@@ -59,11 +65,11 @@
events.forEach { gestureMonitor.processTouchpadEvent(it) }
- assertThat(gestureDoneWasCalled).isTrue()
+ assertThat(gestureState).isEqualTo(FINISHED)
}
@Test
- fun triggersGestureDoneForThreeFingerGestureLeft() {
+ fun triggersGestureFinishedForThreeFingerGestureLeft() {
val events =
listOf(
threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f),
@@ -77,7 +83,21 @@
events.forEach { gestureMonitor.processTouchpadEvent(it) }
- assertThat(gestureDoneWasCalled).isTrue()
+ assertThat(gestureState).isEqualTo(FINISHED)
+ }
+
+ @Test
+ fun triggersGestureProgressForThreeFingerGestureStarted() {
+ val events =
+ listOf(
+ threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f),
+ threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f),
+ threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f),
+ )
+
+ events.forEach { gestureMonitor.processTouchpadEvent(it) }
+
+ assertThat(gestureState).isEqualTo(IN_PROGRESS)
}
private fun threeFingerEvent(action: Int, x: Float, y: Float): MotionEvent {
@@ -91,7 +111,7 @@
}
@Test
- fun doesntTriggerGestureDone_onThreeFingersSwipeUp() {
+ fun doesntTriggerGestureFinished_onThreeFingersSwipeUp() {
val events =
listOf(
threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f),
@@ -105,11 +125,11 @@
events.forEach { gestureMonitor.processTouchpadEvent(it) }
- assertThat(gestureDoneWasCalled).isFalse()
+ assertThat(gestureState).isEqualTo(NOT_STARTED)
}
@Test
- fun doesntTriggerGestureDone_onTwoFingersSwipe() {
+ fun doesntTriggerGestureFinished_onTwoFingersSwipe() {
fun twoFingerEvent(action: Int, x: Float, y: Float) =
motionEvent(
action = action,
@@ -127,11 +147,11 @@
events.forEach { gestureMonitor.processTouchpadEvent(it) }
- assertThat(gestureDoneWasCalled).isFalse()
+ assertThat(gestureState).isEqualTo(NOT_STARTED)
}
@Test
- fun doesntTriggerGestureDone_onFourFingersSwipe() {
+ fun doesntTriggerGestureFinished_onFourFingersSwipe() {
fun fourFingerEvent(action: Int, x: Float, y: Float) =
motionEvent(
action = action,
@@ -155,6 +175,6 @@
events.forEach { gestureMonitor.processTouchpadEvent(it) }
- assertThat(gestureDoneWasCalled).isFalse()
+ assertThat(gestureState).isEqualTo(NOT_STARTED)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index 769f264..dc4d5f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -32,6 +32,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NOT_STARTED
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -41,8 +43,8 @@
@RunWith(AndroidJUnit4::class)
class TouchpadGestureHandlerTest : SysuiTestCase() {
- private var gestureDone = false
- private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureDone = true }
+ private var gestureState = NOT_STARTED
+ private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureState = it }
companion object {
const val SWIPE_DISTANCE = 100
@@ -84,7 +86,7 @@
fun triggersGestureDoneForThreeFingerGesture() {
backGestureEvents().forEach { handler.onMotionEvent(it) }
- assertThat(gestureDone).isTrue()
+ assertThat(gestureState).isEqualTo(FINISHED)
}
private fun backGestureEvents(): List<MotionEvent> {