Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 7 additions & 24 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,16 @@
## [Unreleased]

### Added
- `NavAnimation.directionAware()` for forward/backward navigation transitions
- Sample Home → Details → Checkout flow in the demo app

## [1.0.0] - 2026-06-14

### Added
- Maven Central and JitPack publishing
- Custom animation builder via `NavAnimation.custom()`

## [0.1.0] - 2026-04-20

### Added
- Preset animations and `animatedComposable()` extension
























39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* Quick start
* Preset animations
* Custom animations
* Direction-aware navigation
* Sample app
* Project structure
* Contributing
Expand All @@ -28,6 +29,7 @@
| --- | --- |
| **Preset transitions** | `fade`, `slideLeft`, `slideRight`, `slideUp`, `scale` |
| **Custom builder** | Compose transitions with shared duration and easing |
| **Direction-aware** | `directionAware()` maps forward push and backward pop animations |
| **Pop support** | Matching back-stack enter/exit animations |
| **Simple API** | One extension: `animatedComposable()` |
| **Defaults** | 300 ms duration, `FastOutSlowInEasing` |
Expand All @@ -41,6 +43,7 @@ The sample app demo above shows:

- Home list navigation with profile entry
- Detail screen transitions using `slideLeft`
- Direction-aware Home → Details → Checkout flow (`slideLeft` forward, `slideRight` back)
- Profile screen with custom horizontal slide
- Sheet screen with mixed slide-up and fade animations

Expand Down Expand Up @@ -170,13 +173,47 @@ NavAnimation.custom(

---

## Direction-aware navigation

Use separate forward and backward specs. Compose Navigation applies `enter`/`exit` on push and `popEnter`/`popExit` on back — no stack-depth tracking required.

```kotlin
val navAnimation = NavAnimation.directionAware(
forward = NavAnimation.slideLeft(),
backward = NavAnimation.slideRight(),
)

NavHost(navController, startDestination = "home") {
animatedComposable(route = "home", animation = navAnimation) { HomeScreen() }
animatedComposable(route = "details", animation = navAnimation) { DetailsScreen() }
animatedComposable(route = "checkout", animation = navAnimation) { CheckoutScreen() }
}
```

Works with presets and `NavAnimation.custom()`:

```kotlin
NavAnimation.directionAware(
forward = NavAnimation.custom(
enter = { fadeIn(animationSpec = tweenSpec()) },
exit = { fadeOut(animationSpec = tweenSpec()) },
),
backward = NavAnimation.custom(
enter = { slideInHorizontally(animationSpec = tweenSpec()) },
exit = { slideOutHorizontally(animationSpec = tweenSpec()) },
),
)
```

---

## Sample app

```bash
./gradlew :app:installDebug
```

Demonstrates preset `slideLeft`, a custom horizontal profile screen, and a mixed slide-up / fade sheet screen.
Demonstrates preset `slideLeft`, direction-aware checkout flow, a custom horizontal profile screen, and a mixed slide-up / fade sheet screen.

---

Expand Down
24 changes: 23 additions & 1 deletion app/src/main/java/com/example/composenavmotion/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.composenavmotion.NavAnimation
import com.composenavmotion.animatedComposable
import com.example.composenavmotion.ui.checkout.CheckoutScreen
import com.example.composenavmotion.ui.detail.DetailScreen
import com.example.composenavmotion.ui.home.HomeScreen
import com.example.composenavmotion.ui.profile.ProfileScreen
Expand All @@ -32,21 +33,42 @@ class MainActivity : ComponentActivity() {
setContent {
ComposeNavMotionTheme {
val navController = rememberNavController()
val directionAwareAnimation = NavAnimation.directionAware(
forward = NavAnimation.slideLeft(),
backward = NavAnimation.slideRight(),
)
Scaffold { innerPadding ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(innerPadding),
) {
animatedComposable(route = "home", animation = NavAnimation.fade()) {
animatedComposable(route = "home", animation = directionAwareAnimation) {
HomeScreen(
onOpenDetail = { item ->
navController.navigate("details/${item.id}")
},
onOpenSheet = { navController.navigate("sheet") },
onOpenProfile = { navController.navigate("profile") },
onOpenCheckoutFlow = { navController.navigate("checkout-details") },
)
}
animatedComposable(
route = "checkout-details",
animation = directionAwareAnimation,
) {
DetailScreen(
itemId = "checkout-flow",
onBack = { navController.popBackStack() },
onContinueToCheckout = { navController.navigate("checkout") },
)
}
animatedComposable(
route = "checkout",
animation = directionAwareAnimation,
) {
CheckoutScreen(onBack = { navController.popBackStack() })
}
animatedComposable(
route = "details/{itemId}",
animation = NavAnimation.slideLeft(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.example.composenavmotion.ui.checkout

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.composenavmotion.ui.components.SampleTopBar

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CheckoutScreen(
onBack: () -> Unit,
) {
Scaffold(
topBar = {
SampleTopBar(title = "Checkout", onBack = onBack)
},
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "Order summary", style = MaterialTheme.typography.headlineSmall)
Text(
text = "Pop back to Details or Home to see backward slideRight.",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(top = 12.dp),
)
Text(
text = "Direction-aware: slideLeft forward, slideRight backward",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 8.dp),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
Expand All @@ -20,6 +21,7 @@ import com.example.composenavmotion.ui.home.sampleHomeItems
fun DetailScreen(
itemId: String,
onBack: () -> Unit,
onContinueToCheckout: (() -> Unit)? = null,
) {
val item = sampleHomeItems.firstOrNull { it.id == itemId }
val title = item?.title ?: "Details"
Expand All @@ -44,10 +46,22 @@ fun DetailScreen(
modifier = Modifier.padding(top = 12.dp),
)
Text(
text = "Opened with slideLeft preset",
text = if (onContinueToCheckout != null) {
"Forward navigation uses slideLeft"
} else {
"Opened with slideLeft preset"
},
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 8.dp),
)
onContinueToCheckout?.let { onContinue ->
Button(
onClick = onContinue,
modifier = Modifier.padding(top = 24.dp),
) {
Text("Continue to Checkout")
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ data class HomeItem(
val title: String,
val subtitle: String,
val opensSheet: Boolean = false,
val opensCheckoutFlow: Boolean = false,
)

val sampleHomeItems = listOf(
HomeItem("fade", "Fade transition", "NavAnimation.fade() preset"),
HomeItem("slide-left", "Slide left transition", "NavAnimation.slideLeft() preset"),
HomeItem("slide-right", "Slide right transition", "NavAnimation.slideRight() preset"),
HomeItem("scale", "Scale transition", "NavAnimation.scale() preset"),
HomeItem(
"checkout-flow",
"Direction-aware checkout",
"Home → Details → Checkout with slideLeft / slideRight",
opensCheckoutFlow = true,
),
HomeItem("sheet", "Sheet transition", "Custom slide up with fade", opensSheet = true),
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fun HomeScreen(
onOpenDetail: (HomeItem) -> Unit,
onOpenSheet: () -> Unit,
onOpenProfile: () -> Unit,
onOpenCheckoutFlow: () -> Unit,
) {
Scaffold(
topBar = {
Expand Down Expand Up @@ -53,10 +54,10 @@ fun HomeScreen(
modifier = Modifier
.fillMaxWidth()
.clickable {
if (item.opensSheet) {
onOpenSheet()
} else {
onOpenDetail(item)
when {
item.opensSheet -> onOpenSheet()
item.opensCheckoutFlow -> onOpenCheckoutFlow()
else -> onOpenDetail(item)
}
},
headlineContent = { Text(item.title) },
Expand Down
25 changes: 25 additions & 0 deletions nav-animation/src/main/java/com/composenavmotion/NavAnimation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,29 @@ object NavAnimation {
val config = AnimationConfig(duration, easing)
return NavAnimationSpec(config.enter(), config.exit(), config.popEnter(), config.popExit(), config)
}

/**
* Combines separate forward and backward [NavAnimationSpec] instances for direction-aware navigation.
*
* Forward navigation (push) uses [forward] enter and exit transitions.
* Back navigation (pop) uses [backward] enter and exit transitions as pop enter and pop exit.
*
* Example:
* ```
* val animation = NavAnimation.directionAware(
* forward = NavAnimation.slideLeft(),
* backward = NavAnimation.slideRight(),
* )
* ```
*/
fun directionAware(
forward: NavAnimationSpec,
backward: NavAnimationSpec,
): NavAnimationSpec = NavAnimationSpec(
enterTransition = forward.enterTransition,
exitTransition = forward.exitTransition,
popEnterTransition = backward.enterTransition,
popExitTransition = backward.exitTransition,
config = forward.config,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ package com.composenavmotion
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition

/**
* Resolved enter, exit, and pop transitions for a navigation destination.
*
* Use [NavAnimation.directionAware] to map separate forward and backward specs onto
* [enterTransition]/[exitTransition] and [popEnterTransition]/[popExitTransition].
*/
data class NavAnimationSpec(
val enterTransition: EnterTransition,
val exitTransition: ExitTransition,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import androidx.navigation.NavDeepLink
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable

/**
* Registers a composable destination with enter, exit, and pop transitions from [animation].
*
* Pop transitions power back navigation. Pair with [NavAnimation.directionAware] so forward
* pushes and backward pops use different visual directions.
*/
fun NavGraphBuilder.animatedComposable(
route: String,
animation: NavAnimationSpec = NavAnimation.fade(),
Expand Down
Loading
Loading