-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: compose animation routing support
- Loading branch information
1 parent
d2521f9
commit c8aab69
Showing
16 changed files
with
973 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
plugins { | ||
kotlin("multiplatform") | ||
kotlin("plugin.serialization") | ||
alias(libs.plugins.jetbrains.compose) | ||
id("org.jlleitschuh.gradle.ktlint") | ||
id("org.jetbrains.kotlinx.kover") | ||
alias(libs.plugins.maven.publish) | ||
} | ||
|
||
configureCommon() | ||
configureJvm() | ||
setupJvmToolchain() | ||
|
||
// TODO: org.jetbrains.compose.animation has targets limitation. That is the reason to duplicate configs below | ||
kotlin { | ||
explicitApi() | ||
|
||
setCompilationOptions() | ||
configureSourceSets() | ||
|
||
js(IR) { | ||
nodejs() | ||
browser() | ||
} | ||
|
||
configureJs() | ||
|
||
macosX64() | ||
macosArm64() | ||
iosX64() | ||
iosArm64() | ||
iosSimulatorArm64() | ||
|
||
sourceSets { | ||
commonMain { | ||
dependencies { | ||
api(projects.resources) | ||
implementation(compose.runtime) | ||
implementation(compose.animation) | ||
} | ||
} | ||
|
||
val jvmMain by getting { | ||
dependsOn(commonMain.get()) | ||
dependencies { | ||
api(libs.slf4j.api) | ||
} | ||
} | ||
val jvmTest by getting { | ||
dependsOn(commonTest.get()) | ||
dependencies { | ||
implementation(libs.test.junit) | ||
implementation(libs.test.coroutines.debug) | ||
implementation(libs.test.kotlin.test.junit) | ||
implementation(compose.desktop.uiTestJUnit4) | ||
implementation(compose.desktop.currentOs) | ||
} | ||
} | ||
|
||
val nativeMain by creating { | ||
dependsOn(commonMain.get()) | ||
} | ||
|
||
val macosMain by creating { | ||
dependsOn(nativeMain) | ||
} | ||
val macosX64Main by getting { | ||
dependsOn(macosMain) | ||
} | ||
val macosArm64Main by getting { | ||
dependsOn(macosMain) | ||
} | ||
val iosX64Main by getting { | ||
dependsOn(nativeMain) | ||
} | ||
val iosArm64Main by getting { | ||
dependsOn(nativeMain) | ||
} | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
compose-animation/common/src/dev/programadorthi/routing/compose/ComposeEntry.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package dev.programadorthi.routing.compose | ||
|
||
import androidx.compose.animation.AnimatedContentScope | ||
import androidx.compose.animation.AnimatedContentTransitionScope | ||
import androidx.compose.animation.EnterTransition | ||
import androidx.compose.animation.ExitTransition | ||
import androidx.compose.runtime.Composable | ||
import dev.programadorthi.routing.core.application.ApplicationCall | ||
import kotlin.jvm.JvmSuppressWildcards | ||
|
||
internal typealias Animation<T> = @JvmSuppressWildcards | ||
AnimatedContentTransitionScope<ComposeEntry>.() -> T | ||
|
||
public data class ComposeEntry( | ||
val call: ApplicationCall, | ||
internal val content: @Composable AnimatedContentScope.() -> Unit, | ||
) { | ||
@PublishedApi | ||
internal var popResult: Any? = null | ||
|
||
@PublishedApi | ||
internal var resource: Any? = null | ||
|
||
internal var enterTransition: Animation<EnterTransition>? = null | ||
internal var exitTransition: Animation<ExitTransition>? = null | ||
internal var popEnterTransition: Animation<EnterTransition>? = enterTransition | ||
internal var popExitTransition: Animation<ExitTransition>? = exitTransition | ||
|
||
public var popped: Boolean = false | ||
internal set | ||
|
||
public inline fun <reified T> popResult(): T? = | ||
when (popResult) { | ||
is T -> popResult as T | ||
else -> null | ||
} | ||
|
||
public inline fun <reified T> popResult(default: T): T = popResult() ?: default | ||
|
||
public inline fun <reified T> popResult(default: () -> T): T = popResult() ?: default() | ||
|
||
public inline fun <reified T> resource(): T? = | ||
when (resource) { | ||
is T -> resource as T | ||
else -> null | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
compose-animation/common/src/dev/programadorthi/routing/compose/ComposeManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package dev.programadorthi.routing.compose | ||
|
||
import androidx.compose.runtime.snapshots.SnapshotStateList | ||
import dev.programadorthi.routing.core.Routing | ||
import io.ktor.util.AttributeKey | ||
|
||
private val ComposeRoutingAttributeKey: AttributeKey<SnapshotStateList<ComposeEntry>> = | ||
AttributeKey("ComposeRoutingAttributeKey") | ||
|
||
private val ComposeRoutingPoppedEntryAttributeKey: AttributeKey<ComposeEntry> = | ||
AttributeKey("ComposeRoutingPoppedEntryAttributeKey") | ||
|
||
internal var Routing.contentList: SnapshotStateList<ComposeEntry> | ||
get() = attributes[ComposeRoutingAttributeKey] | ||
set(value) { | ||
attributes.put(ComposeRoutingAttributeKey, value) | ||
} | ||
|
||
internal var Routing.poppedEntry: ComposeEntry? | ||
get() = attributes.getOrNull(ComposeRoutingPoppedEntryAttributeKey) | ||
set(value) { | ||
if (value != null) { | ||
attributes.put(ComposeRoutingPoppedEntryAttributeKey, value) | ||
} else { | ||
attributes.remove(ComposeRoutingPoppedEntryAttributeKey) | ||
} | ||
} |
157 changes: 157 additions & 0 deletions
157
compose-animation/common/src/dev/programadorthi/routing/compose/ComposeRouting.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
package dev.programadorthi.routing.compose | ||
|
||
import androidx.compose.animation.AnimatedContent | ||
import androidx.compose.animation.AnimatedContentScope | ||
import androidx.compose.animation.AnimatedContentTransitionScope | ||
import androidx.compose.animation.ContentTransform | ||
import androidx.compose.animation.EnterTransition | ||
import androidx.compose.animation.ExitTransition | ||
import androidx.compose.animation.core.tween | ||
import androidx.compose.animation.fadeIn | ||
import androidx.compose.animation.fadeOut | ||
import androidx.compose.animation.scaleIn | ||
import androidx.compose.animation.togetherWith | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.CompositionLocalProvider | ||
import androidx.compose.runtime.DisposableEffect | ||
import androidx.compose.runtime.ProvidableCompositionLocal | ||
import androidx.compose.runtime.mutableStateListOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.staticCompositionLocalOf | ||
import dev.programadorthi.routing.core.Route | ||
import dev.programadorthi.routing.core.Routing | ||
import dev.programadorthi.routing.core.application | ||
import dev.programadorthi.routing.core.application.ApplicationCall | ||
import dev.programadorthi.routing.core.routing | ||
import io.ktor.util.logging.KtorSimpleLogger | ||
import io.ktor.util.logging.Logger | ||
import kotlin.coroutines.CoroutineContext | ||
import kotlin.coroutines.EmptyCoroutineContext | ||
|
||
public val LocalRouting: ProvidableCompositionLocal<Routing> = | ||
staticCompositionLocalOf { | ||
error("Composition local LocalRouting not found") | ||
} | ||
|
||
@Composable | ||
public fun Routing( | ||
routing: Routing, | ||
enterTransition: Animation<EnterTransition> = { | ||
fadeIn(animationSpec = tween(220, delayMillis = 90)) + | ||
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) | ||
}, | ||
exitTransition: Animation<ExitTransition> = { | ||
fadeOut(animationSpec = tween(90)) | ||
}, | ||
popEnterTransition: Animation<EnterTransition> = enterTransition, | ||
popExitTransition: Animation<ExitTransition> = exitTransition, | ||
initial: @Composable AnimatedContentScope.() -> Unit, | ||
) { | ||
CompositionLocalProvider(LocalRouting provides routing) { | ||
val stateList = | ||
remember(routing) { | ||
mutableStateListOf<ComposeEntry>().apply { | ||
routing.contentList = this | ||
add( | ||
ComposeEntry( | ||
content = initial, | ||
call = | ||
ApplicationCall( | ||
application = routing.application, | ||
uri = routing.toString(), | ||
), | ||
).apply { | ||
this.enterTransition = enterTransition | ||
this.exitTransition = exitTransition | ||
this.popEnterTransition = popEnterTransition | ||
this.popExitTransition = popExitTransition | ||
}, | ||
) | ||
} | ||
} | ||
AnimatedContent( | ||
targetState = stateList.last(), | ||
transitionSpec = { | ||
transitionSpec( | ||
scope = this, | ||
enterTransition = enterTransition, | ||
exitTransition = exitTransition, | ||
popEnterTransition = popEnterTransition, | ||
popExitTransition = popExitTransition, | ||
) | ||
}, | ||
content = { entry -> | ||
entry.content(this) | ||
}, | ||
) | ||
} | ||
} | ||
|
||
@Composable | ||
public fun Routing( | ||
rootPath: String = "/", | ||
parent: Routing? = null, | ||
coroutineContext: CoroutineContext = EmptyCoroutineContext, | ||
log: Logger = KtorSimpleLogger("kotlin-routing"), | ||
developmentMode: Boolean = false, | ||
enterTransition: Animation<EnterTransition> = { | ||
fadeIn(animationSpec = tween(220, delayMillis = 90)) + | ||
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) | ||
}, | ||
exitTransition: Animation<ExitTransition> = { | ||
fadeOut(animationSpec = tween(90)) | ||
}, | ||
popEnterTransition: Animation<EnterTransition> = enterTransition, | ||
popExitTransition: Animation<ExitTransition> = exitTransition, | ||
configuration: Route.() -> Unit, | ||
initial: @Composable AnimatedContentScope.() -> Unit, | ||
) { | ||
val routing = | ||
remember { | ||
routing( | ||
rootPath = rootPath, | ||
parent = parent, | ||
parentCoroutineContext = coroutineContext, | ||
log = log, | ||
developmentMode = developmentMode, | ||
configuration = configuration, | ||
) | ||
} | ||
|
||
DisposableEffect(routing) { | ||
onDispose { | ||
routing.dispose() | ||
} | ||
} | ||
|
||
Routing( | ||
routing = routing, | ||
enterTransition = enterTransition, | ||
exitTransition = exitTransition, | ||
popEnterTransition = popEnterTransition, | ||
popExitTransition = popExitTransition, | ||
initial = initial, | ||
) | ||
} | ||
|
||
private fun transitionSpec( | ||
scope: AnimatedContentTransitionScope<ComposeEntry>, | ||
enterTransition: Animation<EnterTransition>, | ||
exitTransition: Animation<ExitTransition>, | ||
popEnterTransition: Animation<EnterTransition> = enterTransition, | ||
popExitTransition: Animation<ExitTransition> = exitTransition, | ||
): ContentTransform = | ||
with(scope) { | ||
val previousEntry = initialState | ||
val nextEntry = targetState | ||
|
||
if (previousEntry.popped) { | ||
val enter = nextEntry.popEnterTransition ?: popEnterTransition | ||
val exit = previousEntry.popExitTransition ?: popExitTransition | ||
return@with enter(this).togetherWith(exit(this)) | ||
} | ||
|
||
val enter = nextEntry.enterTransition ?: enterTransition | ||
val exit = previousEntry.exitTransition ?: exitTransition | ||
return@with enter(this).togetherWith(exit(this)) | ||
} |
Oops, something went wrong.