Loading packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +59 −17 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import androidx.compose.animation.core.LinearEasing 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 Loading @@ -49,6 +50,7 @@ import androidx.compose.ui.input.pointer.pointerInteropFilter 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 Loading @@ -61,6 +63,9 @@ import com.airbnb.lottie.compose.rememberLottieDynamicProperties 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 Loading @@ -78,23 +83,49 @@ fun BackGestureTutorialScreen( ) { 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() } } Loading Loading @@ -126,14 +157,14 @@ private fun rememberScreenColors(): TutorialScreenColors { @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" Loading @@ -148,7 +179,7 @@ private fun GestureTutorialContent( 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, Loading @@ -156,7 +187,7 @@ private fun GestureTutorialContent( ) Spacer(modifier = Modifier.width(76.dp)) TutorialAnimation( gestureDone, gestureState, screenColors.animationProperties, modifier = Modifier.weight(1f).padding(top = 8.dp) ) Loading Loading @@ -189,26 +220,37 @@ fun TutorialDescription( @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, Loading packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt +11 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -44,13 +47,16 @@ class BackGestureMonitor( 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) } } } Loading packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt 0 → 100644 +23 −0 Original line number Diff line number Diff line /* * 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 } packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt +2 −2 Original line number Diff line number Diff line Loading @@ -22,10 +22,10 @@ enum class TouchpadGesture { 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") } } Loading packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt +2 −2 Original line number Diff line number Diff line Loading @@ -26,11 +26,11 @@ import android.view.MotionEvent 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 Loading Loading
packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +59 −17 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import androidx.compose.animation.core.LinearEasing 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 Loading @@ -49,6 +50,7 @@ import androidx.compose.ui.input.pointer.pointerInteropFilter 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 Loading @@ -61,6 +63,9 @@ import com.airbnb.lottie.compose.rememberLottieDynamicProperties 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 Loading @@ -78,23 +83,49 @@ fun BackGestureTutorialScreen( ) { 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() } } Loading Loading @@ -126,14 +157,14 @@ private fun rememberScreenColors(): TutorialScreenColors { @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" Loading @@ -148,7 +179,7 @@ private fun GestureTutorialContent( 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, Loading @@ -156,7 +187,7 @@ private fun GestureTutorialContent( ) Spacer(modifier = Modifier.width(76.dp)) TutorialAnimation( gestureDone, gestureState, screenColors.animationProperties, modifier = Modifier.weight(1f).padding(top = 8.dp) ) Loading Loading @@ -189,26 +220,37 @@ fun TutorialDescription( @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, Loading
packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt +11 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -44,13 +47,16 @@ class BackGestureMonitor( 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) } } } Loading
packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt 0 → 100644 +23 −0 Original line number Diff line number Diff line /* * 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 }
packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt +2 −2 Original line number Diff line number Diff line Loading @@ -22,10 +22,10 @@ enum class TouchpadGesture { 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") } } Loading
packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt +2 −2 Original line number Diff line number Diff line Loading @@ -26,11 +26,11 @@ import android.view.MotionEvent 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 Loading