This is the second article of a series about Jetpack Compose.
Posts in this series :
Resources:
Our Jetpack Compose Stories !
Let's warm up by using our progress bar in a typical Instagram's like screen.
@Composable
fun InstagramScreen() {
// We will hardcode those parameter for now.
val steps = 5;
val currentStep = 2;
val isPressed = remember { mutableStateOf(false) }
val goToPreviousScreen = {}
val goToNextScreen = {}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier
.background(
Brush.linearGradient( // (1)
colors = listOf(GreenLemon, GreenLeaves, BlueSea), // (2)
start = Offset.Zero, end = Offset.Infinite
)
)
) {
InstagramSlicedProgressBar(steps, currentStep, isPressed.value, goToNextScreen)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.weight(1f)
) {
Text(
text = "Hello world !",
style = Typography.h1,
color = Color.White
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Tap or wait to go to the next screen",
style = Typography.body1,
color = Color.White
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "?",
style = Typography.body1,
color = Color.White
)
}
}
}
val GreenLemon = Color(0xFFA9F24D)
val GreenLeaves = Color(0xFF00C88C)
val BlueSea = Color(0xFF4895AD)
val Purple = Color(0xFF9248AD)
val RedRaspberry = Color(0xFFE2264C)
Adding gestures to this screen is not very complicated. Jetpack Compose pointerInput
modifier is very handful in this situation :
@Composable
fun InstagramScreen() {
// We will hardcode those parameter for now.
val steps = 5;
val currentStep = 2;
val isPressed = remember { mutableStateOf(false) }
val goToPreviousScreen = {}
val goToNextScreen = {}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier
.background(
...
).pointerInput(Unit) { // (1)
val maxWidth = this.size.width // (2)
detectTapGestures(
onPress = { // (3)
val pressStartTime = System.currentTimeMillis()
isPressed.value = true
this.tryAwaitRelease() // (4)
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime // (5)
if (totalPressTime < 200) {
val isTapOnRightTwoTiers = (it.x > (maxWidth / 4)) // (6)
if (isTapOnRightTwoTiers) {
goToNextScreen()
} else {
goToPreviousScreen()
}
}
isPressed.value = false
},
)
}
) {
...
}
}
pointerInput
installs a gesture detector. It is attached to some key. If the key change on recomposition, the previous gesture detector is detached and a new one is created.Unit
because we want to install a permanent gesture detector.PointerInputScope
! Wow !detectTapGestures
's onPress
attribute is what we need to detect custom on-press behaviours. It expects a suspend function and provide in its scope a suspendable awaitRelease
and tryAwaitRelease
functions.graph LR
A[Start] -->|Down| B{Wait for release?};
B -->|Up| C[Continue coroutine execution];
B ---->|Recomposition| B;
We can easily set up a navigation with the androidx.navigation:navigation-compose
package.
implementation("androidx.navigation:navigation-compose:2.4.0-alpha03")
Here is our entry point :
@Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "instagram/{steps}/{currentStep}") { // (1)
composable( // (2)
"instagram/{steps}/{currentStep}",
arguments = listOf(
navArgument("steps") { type = NavType.IntType; defaultValue = 8 }, // (3)
navArgument("currentStep") { type = NavType.IntType; defaultValue = 1 }, // (4)
)
) { backStackEntry -> // (5)
InstagramScreen(
navController,
backStackEntry.arguments!!.getInt("steps"),
backStackEntry.arguments!!.getInt("currentStep"),
)
}
}
}
instagram/{steps}/{currentStep}
.steps
and currentStep
.steps
is an Int
; we can use a default value.currentStep
is an Int
; we can use a default value.backStackEntry
argument.Remember our hardcoded values ? Let's change those :
@Composable
fun InstagramScreen(navController: NavController, steps: Int, currentStep: Int) {
val goToNextScreen = {
if (currentStep + 1 <= steps) navController.navigate("instagram/$steps/${currentStep + 1}")
}
val goToPreviousScreen = {
if (currentStep - 1 > 0) navController.navigate("instagram/$steps/${currentStep - 1}")
}
...
}
Here you go !
The final result is here :