Skip to content

Commit a4581ea

Browse files
feat(game): Implement predictive back gesture on GameMode screen
- Replaced `BackHandler` with `PredictiveBackHandler` on the GameMode selection screen to support Android's predictive back gesture. - Added animations to the screen content (scaling, translation, and alpha) that react to the back gesture's progress. - Enabled the `enableOnBackInvokedCallback` attribute in `AndroidManifest.xml` for the main activity. - Updated various Gradle dependencies, including Android Gradle Plugin, Kotlin, Compose BOM, and Material3.
1 parent 175ea98 commit a4581ea

3 files changed

Lines changed: 90 additions & 54 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
android:roundIcon="@mipmap/ic_launcher"
1313
android:supportsRtl="true"
1414
android:theme="@style/Theme.App.Starting"
15-
tools:targetApi="31">
15+
android:enableOnBackInvokedCallback="true"
16+
tools:targetApi="36">
1617
<activity
1718
android:name=".MainActivity"
1819
android:exported="true"
19-
android:theme="@style/Theme.App.Starting">
20+
android:theme="@style/Theme.App.Starting"
21+
android:enableOnBackInvokedCallback="true">
2022
<intent-filter>
2123
<action android:name="android.intent.action.MAIN" />
2224

app/src/main/java/com/stephenwanjala/multiply/game/GameMode.kt

Lines changed: 78 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,56 @@
11
package com.stephenwanjala.multiply.game
22

3-
import androidx.activity.compose.BackHandler
4-
import androidx.compose.animation.*
3+
import androidx.activity.BackEventCompat
4+
import androidx.activity.compose.PredictiveBackHandler
5+
import androidx.compose.animation.AnimatedVisibility
56
import androidx.compose.animation.core.Spring
67
import androidx.compose.animation.core.animateFloatAsState
78
import androidx.compose.animation.core.spring
89
import androidx.compose.animation.core.tween
10+
import androidx.compose.animation.expandVertically
11+
import androidx.compose.animation.fadeIn
12+
import androidx.compose.animation.fadeOut
13+
import androidx.compose.animation.scaleIn
14+
import androidx.compose.animation.scaleOut
15+
import androidx.compose.animation.shrinkVertically
16+
import androidx.compose.animation.slideInVertically
17+
import androidx.compose.animation.slideOutVertically
918
import androidx.compose.foundation.Image
1019
import androidx.compose.foundation.background
1120
import androidx.compose.foundation.clickable
1221
import androidx.compose.foundation.interaction.MutableInteractionSource
13-
import androidx.compose.foundation.layout.*
22+
import androidx.compose.foundation.layout.Arrangement
23+
import androidx.compose.foundation.layout.Box
24+
import androidx.compose.foundation.layout.Column
25+
import androidx.compose.foundation.layout.ExperimentalLayoutApi
26+
import androidx.compose.foundation.layout.FlowRow
27+
import androidx.compose.foundation.layout.Row
28+
import androidx.compose.foundation.layout.Spacer
29+
import androidx.compose.foundation.layout.fillMaxSize
30+
import androidx.compose.foundation.layout.fillMaxWidth
31+
import androidx.compose.foundation.layout.height
32+
import androidx.compose.foundation.layout.padding
33+
import androidx.compose.foundation.layout.size
34+
import androidx.compose.foundation.layout.statusBarsPadding
35+
import androidx.compose.foundation.layout.width
1436
import androidx.compose.foundation.shape.RoundedCornerShape
1537
import androidx.compose.material.icons.Icons
1638
import androidx.compose.material.icons.filled.CheckCircle
1739
import androidx.compose.material.icons.filled.Star
18-
import androidx.compose.material3.*
19-
import androidx.compose.runtime.*
40+
import androidx.compose.material3.Card
41+
import androidx.compose.material3.CardDefaults
42+
import androidx.compose.material3.Icon
43+
import androidx.compose.material3.MaterialTheme
44+
import androidx.compose.material3.Surface
45+
import androidx.compose.material3.Text
46+
import androidx.compose.runtime.Composable
47+
import androidx.compose.runtime.getValue
48+
import androidx.compose.runtime.mutableFloatStateOf
49+
import androidx.compose.runtime.mutableStateOf
50+
import androidx.compose.runtime.remember
51+
import androidx.compose.runtime.rememberCoroutineScope
2052
import androidx.compose.runtime.saveable.rememberSaveable
53+
import androidx.compose.runtime.setValue
2154
import androidx.compose.ui.Alignment
2255
import androidx.compose.ui.Modifier
2356
import androidx.compose.ui.draw.clip
@@ -42,8 +75,15 @@ import com.stephenwanjala.multiply.game.components.animatedBackground
4275
import com.stephenwanjala.multiply.game.components.glowingOrbs
4376
import com.stephenwanjala.multiply.game.components.neumorphicShadow
4477
import com.stephenwanjala.multiply.game.components.repeatLiquidBackground
45-
import com.stephenwanjala.multiply.game.models.*
78+
import com.stephenwanjala.multiply.game.models.BubbleMathDifficulty
79+
import com.stephenwanjala.multiply.game.models.GameMode
80+
import com.stephenwanjala.multiply.game.models.GameModeSaver
81+
import com.stephenwanjala.multiply.game.models.ModeDifficulty
82+
import com.stephenwanjala.multiply.game.models.ModeDifficultySaver
83+
import com.stephenwanjala.multiply.game.models.QuizDifficulty
4684
import com.stephenwanjala.multiply.ui.theme.MultiplyTheme
85+
import kotlinx.coroutines.CancellationException
86+
import kotlinx.coroutines.flow.Flow
4787
import kotlinx.coroutines.launch
4888

4989
@Composable
@@ -60,16 +100,29 @@ fun GameModeSelectionScreen(
60100
mutableStateOf<ModeDifficulty?>(null)
61101
}
62102

103+
var backProgress by remember { mutableFloatStateOf(0f) }
63104

64105
val modes = listOf(
65106
GameMode.BubbleMathBlitz(BubbleMathDifficulty.EASY),
66107
GameMode.QuizGenius(QuizDifficulty.BEGINNER)
67108
)
68109

110+
69111
if (selectedMode != null) {
70-
BackHandler {
71-
selectedMode = null
72-
selectedDifficulty = null
112+
PredictiveBackHandler { progress: Flow<BackEventCompat> ->
113+
try {
114+
progress.collect { backEvent ->
115+
backProgress = backEvent.progress
116+
}
117+
selectedMode = null
118+
selectedDifficulty = null
119+
backProgress = 0f
120+
} catch (e: CancellationException) {
121+
// Cancellation: user released the gesture without completing
122+
// Animate back to normal state
123+
backProgress = 0f
124+
throw e
125+
}
73126
}
74127
}
75128

@@ -93,7 +146,14 @@ fun GameModeSelectionScreen(
93146
modifier = Modifier
94147
.fillMaxSize()
95148
.animatedBackground()
96-
.padding(24.dp),
149+
.padding(24.dp)
150+
// Apply predictive back animation transforms
151+
.graphicsLayer(
152+
scaleX = 1f - (backProgress * 0.05f),
153+
scaleY = 1f - (backProgress * 0.05f),
154+
translationX = backProgress * 100f,
155+
alpha = 1f - (backProgress * 0.2f)
156+
),
97157
horizontalAlignment = Alignment.CenterHorizontally
98158
) {
99159
// Header
@@ -121,7 +181,7 @@ fun GameModeSelectionScreen(
121181

122182
Spacer(modifier = Modifier.height(32.dp))
123183

124-
// Step 1: GameMode selection - Always visible but fades/scales out when mode is selected
184+
// Step 1: GameMode selection
125185
AnimatedVisibility(
126186
visible = selectedMode == null,
127187
enter = fadeIn(animationSpec = tween(400, delayMillis = 200)) +
@@ -154,7 +214,7 @@ fun GameModeSelectionScreen(
154214
modes.forEach { modeOption ->
155215
Material3GameModeCard(
156216
mode = modeOption,
157-
isSelected = false, // Never show as selected in this view
217+
isSelected = false,
158218
onClick = {
159219
selectedMode = modeOption
160220
selectedDifficulty = null
@@ -164,12 +224,12 @@ fun GameModeSelectionScreen(
164224
}
165225
}
166226

167-
// Step 2: Difficulty selection - Slides up from bottom with smooth animation
227+
// Step 2: Difficulty selection
168228
AnimatedVisibility(
169229
visible = selectedMode != null,
170230
enter = fadeIn(animationSpec = tween(400, delayMillis = 150)) +
171231
slideInVertically(
172-
initialOffsetY = { it / 2 }, // Start from halfway down
232+
initialOffsetY = { it / 2 },
173233
animationSpec = spring(
174234
dampingRatio = Spring.DampingRatioMediumBouncy,
175235
stiffness = Spring.StiffnessLow
@@ -222,7 +282,6 @@ fun GameModeSelectionScreen(
222282
}
223283
)
224284

225-
// High Score display with smooth entrance
226285
AnimatedVisibility(
227286
visible = selectedDifficulty != null,
228287
enter = fadeIn(animationSpec = tween(300, delayMillis = 200)) +
@@ -268,7 +327,6 @@ fun GameModeSelectionScreen(
268327

269328
Spacer(modifier = Modifier.height(24.dp))
270329

271-
// Action buttons with staggered animation
272330
AnimatedVisibility(
273331
visible = true,
274332
enter = fadeIn(animationSpec = tween(300, delayMillis = 300)) +
@@ -296,28 +354,6 @@ fun GameModeSelectionScreen(
296354
}
297355
}
298356
)
299-
300-
/*
301-
OutlinedButton(
302-
onClick = {
303-
selectedMode = null
304-
selectedDifficulty = null
305-
},
306-
colors = ButtonDefaults.outlinedButtonColors(
307-
contentColor = MaterialTheme.colorScheme.onSurface
308-
),
309-
border = ButtonDefaults.outlinedButtonBorder().copy(
310-
brush = Brush.linearGradient(
311-
listOf(
312-
MaterialTheme.colorScheme.outline,
313-
MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
314-
)
315-
)
316-
)
317-
) {
318-
Text("Back to Mode Selection")
319-
}
320-
*/
321357
}
322358
}
323359
}
@@ -463,12 +499,11 @@ fun Material3LevelSelectionGrid(
463499
) {
464500
val haptics = LocalHapticFeedback.current
465501

466-
467502
val difficultyColors = listOf(
468-
MaterialTheme.colorScheme.primary, // Easy - Primary color
469-
MaterialTheme.colorScheme.secondary, // Medium - Secondary color
470-
MaterialTheme.colorScheme.tertiary, // Hard - Tertiary color
471-
MaterialTheme.colorScheme.error // Expert - Error color for danger
503+
MaterialTheme.colorScheme.primary,
504+
MaterialTheme.colorScheme.secondary,
505+
MaterialTheme.colorScheme.tertiary,
506+
MaterialTheme.colorScheme.error
472507
)
473508

474509
FlowRow(
@@ -682,7 +717,6 @@ fun MaterialSectionTitle(text: String) {
682717
}
683718
}
684719

685-
686720
private val GameMode.name: String
687721
get() = when (this) {
688722
is GameMode.BubbleMathBlitz -> "Bubble Blitz"

gradle/libs.versions.toml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
[versions]
2-
agp = "8.12.3"
3-
kotlin = "2.2.20"
4-
kotlinxSerializationVersion="3.3.1"
2+
agp = "8.13.1"
3+
kotlin = "2.2.21"
4+
kotlinxSerializationVersion="3.3.2"
55
coreKtx = "1.17.0"
66
junit = "4.13.2"
7-
coreSplashscreen = "1.0.1"
7+
coreSplashscreen = "1.2.0"
88
junitVersion = "1.3.0"
99
espressoCore = "3.7.0"
1010
lifecycleRuntimeKtx = "2.9.4"
1111
compose-material-icons-extended = "1.7.8"
12-
ksp="2.2.20-2.0.3"
12+
ksp="2.3.2"
1313
activityCompose = "1.11.0"
14-
composeBom = "2025.10.00"
14+
composeBom = "2025.11.00"
1515
hiltCompiler = "2.57.2"
1616
hiltNavigationCompose = "1.3.0"
17-
material3="1.5.0-alpha06"
17+
material3="1.5.0-alpha08"
1818
datastorePreferences = "1.1.7"
19-
navigationCompose = "2.9.5"
19+
navigationCompose = "2.9.6"
2020
testng = "7.11.0"
2121
[libraries]
2222
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }

0 commit comments

Comments
 (0)