
Shared UI samples and app skeletons with modular common/platform source sets, platform-specific entrypoints, and ready-built Gradle tasks for building and running development targets.
This is a Compose Multiplatform library providing a theme transitioning animation.
In order to install the library with Gradle, be sure to use the mavenCentral() repository in your project.
Then simply use in dependencies:
implementation("io.github.gleb-skobinsky:themeanimator:0.0.18")In order to animate your screen responsible for dark/light theme switching, wrap it with the ThemeAnimationScope composable:
@Composable
fun App() {
val animationState = rememberThemeAnimationState(
format = ThemeAnimationFormat.CircularAroundPress
)
val theme = if (animationState.uiTheme.isDark()) darkColorScheme() else lightColorScheme()
ThemeAnimationScope(
state = animationState
) {
MaterialTheme(
colorScheme = theme
) {
Scaffold(
topBar = {
Column(
modifier = Modifier.statusBarsPadding().fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.End
) {
ThemeSwitchButton(
animationState = animationState
)
}
}
) { contentPadding ->
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.fillMaxSize()
.padding(contentPadding)
)
}
}
}
}The ThemeAnimationState is the primary object to manage and trigger animations.
It should be created with the rememberThemeAnimationState utility function.
Please note that your own theme object must be updated based on the ThemeAnimationState.uiTheme field. This is a known limitation, but it is essential for the animation to work.
Theme animations can be customized with an implementation of ThemeAnimationFormat. Four standard formats are provided out of the box:
| Animation name | Preview |
|---|---|
| Crossfade | |
| Sliding | |
| Circular reveal | |
| Circular reveal around press |
Please note that if your animation interacts with the position of the pressed button, the standard ThemeSwitchButton should be used. Otherwise, you want to implement your own theme switch, notify the state with the ThemeAnimationState.updateButtonPosition method.
The theme animation button is provided with the ThemeSwitchButton Composable. 5 types of button icons are provided.
For animations between the dark and light vector icons, you can utilize Lottie animations:
rememberLottieIcon(
startProgress = 1f,
endProgress = 0.5f,
animationSpec = animationSpec
) {
JsonString(Res.readBytes("files/anim.json").decodeToString())
}In most real-world production use-cases, a persistent local or remote storage is needed to manage the selected theme. Such storage can be provided by implementing and passing a custom ThemeProvider.
If you opt-in to use a ready-made solution, though, you can use the themeViewModel from an additional library:
// In your Gradle files
implementation("io.github.gleb-skobinsky:themeanimator-storage:0.0.18")
// In your Composable
val animationState = rememberThemeAnimationState(
themeProvider = themeViewModel(),
animationSpec = animationSpec,
format = ThemeAnimationFormat.CircularAroundPress
)
What's under the hood?
The theme storage library internally uses Androidx Datastore for all platforms except WASM and JS (in the browser, localStorage is used instead). Thanks to this, the theme is persisted across app relaunches.
Full docs derived from KDocs are available at Github Pages.
This is a Compose Multiplatform library providing a theme transitioning animation.
In order to install the library with Gradle, be sure to use the mavenCentral() repository in your project.
Then simply use in dependencies:
implementation("io.github.gleb-skobinsky:themeanimator:0.0.18")In order to animate your screen responsible for dark/light theme switching, wrap it with the ThemeAnimationScope composable:
@Composable
fun App() {
val animationState = rememberThemeAnimationState(
format = ThemeAnimationFormat.CircularAroundPress
)
val theme = if (animationState.uiTheme.isDark()) darkColorScheme() else lightColorScheme()
ThemeAnimationScope(
state = animationState
) {
MaterialTheme(
colorScheme = theme
) {
Scaffold(
topBar = {
Column(
modifier = Modifier.statusBarsPadding().fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.End
) {
ThemeSwitchButton(
animationState = animationState
)
}
}
) { contentPadding ->
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.primaryContainer)
.fillMaxSize()
.padding(contentPadding)
)
}
}
}
}The ThemeAnimationState is the primary object to manage and trigger animations.
It should be created with the rememberThemeAnimationState utility function.
Please note that your own theme object must be updated based on the ThemeAnimationState.uiTheme field. This is a known limitation, but it is essential for the animation to work.
Theme animations can be customized with an implementation of ThemeAnimationFormat. Four standard formats are provided out of the box:
| Animation name | Preview |
|---|---|
| Crossfade | |
| Sliding | |
| Circular reveal | |
| Circular reveal around press |
Please note that if your animation interacts with the position of the pressed button, the standard ThemeSwitchButton should be used. Otherwise, you want to implement your own theme switch, notify the state with the ThemeAnimationState.updateButtonPosition method.
The theme animation button is provided with the ThemeSwitchButton Composable. 5 types of button icons are provided.
For animations between the dark and light vector icons, you can utilize Lottie animations:
rememberLottieIcon(
startProgress = 1f,
endProgress = 0.5f,
animationSpec = animationSpec
) {
JsonString(Res.readBytes("files/anim.json").decodeToString())
}In most real-world production use-cases, a persistent local or remote storage is needed to manage the selected theme. Such storage can be provided by implementing and passing a custom ThemeProvider.
If you opt-in to use a ready-made solution, though, you can use the themeViewModel from an additional library:
// In your Gradle files
implementation("io.github.gleb-skobinsky:themeanimator-storage:0.0.18")
// In your Composable
val animationState = rememberThemeAnimationState(
themeProvider = themeViewModel(),
animationSpec = animationSpec,
format = ThemeAnimationFormat.CircularAroundPress
)
What's under the hood?
The theme storage library internally uses Androidx Datastore for all platforms except WASM and JS (in the browser, localStorage is used instead). Thanks to this, the theme is persisted across app relaunches.
Full docs derived from KDocs are available at Github Pages.