diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 32f73b70e..76a8ebfa9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ android-activity-ktx = { group = "androidx.activity", name = "activity-ktx", ver android-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "android-activity" } android-appcompat = { group = "androidx.appcompat", name = "appcompat", version = "1.6.1" } android-browser = { group = "androidx.browser", name = "browser", version = "1.7.0" } +android-compose-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "android-compose" } android-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "android-compose" } android-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "android-compose" } android-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended", version = "1.5.4" } diff --git a/platform/animator/build.gradle.kts b/platform/animator/build.gradle.kts new file mode 100644 index 000000000..59e3106c0 --- /dev/null +++ b/platform/animator/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) +} + +android { + buildFeatures.compose = true + composeOptions.kotlinCompilerExtensionVersion = libs.versions.android.compose.compiler.get() + defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" +} + +dependencies { + androidTestImplementation(libs.android.compose.material3) + androidTestImplementation(libs.android.compose.ui.test.junit) + androidTestImplementation(libs.android.compose.ui.test.manifest) + androidTestImplementation(libs.assertk) + androidTestImplementation(libs.kotlin.test) + + implementation(libs.android.compose.animation) + implementation(project(":ext:coroutines")) + + testImplementation(libs.assertk) + testImplementation(libs.kotlin.coroutines.test) + testImplementation(libs.kotlin.test) + testImplementation(libs.turbine) +} diff --git a/platform/animator/src/androidTest/java/com/jeanbarrossilva/orca/platform/animator/AnimatorTests.kt b/platform/animator/src/androidTest/java/com/jeanbarrossilva/orca/platform/animator/AnimatorTests.kt new file mode 100644 index 000000000..3c529cdf7 --- /dev/null +++ b/platform/animator/src/androidTest/java/com/jeanbarrossilva/orca/platform/animator/AnimatorTests.kt @@ -0,0 +1,53 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.animator + +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.test.junit4.createComposeRule +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jeanbarrossilva.orca.platform.animator.animation.Motion +import com.jeanbarrossilva.orca.platform.animator.animation.animatable.Animatables +import kotlin.test.Test +import org.junit.Rule + +internal class AnimatorTests { + @get:Rule val composeRule = createComposeRule() + + @Test + fun providesAnimatablesForStillMotion() { + composeRule.setContent { + Animator(Motion.Still) { + DisposableEffect(Unit) { + assertThat(it).isEqualTo(Animatables(Motion.Still)) + onDispose {} + } + } + } + } + + @Test + fun providesAnimatablesForMovingMotion() { + composeRule.setContent { + Animator { + DisposableEffect(Unit) { + assertThat(it).isEqualTo(Animatables(Motion.Moving)) + onDispose {} + } + } + } + } +} diff --git a/platform/animator/src/androidTest/java/com/jeanbarrossilva/orca/platform/animator/animation/animatable/AnimatableTests.kt b/platform/animator/src/androidTest/java/com/jeanbarrossilva/orca/platform/animator/animation/animatable/AnimatableTests.kt new file mode 100644 index 000000000..26ab7997a --- /dev/null +++ b/platform/animator/src/androidTest/java/com/jeanbarrossilva/orca/platform/animator/animation/animatable/AnimatableTests.kt @@ -0,0 +1,62 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.animator.animation.animatable + +import androidx.compose.animation.core.AnimationConstants.DefaultDurationMillis +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.material3.Text +import androidx.compose.runtime.remember +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import assertk.assertThat +import assertk.assertions.isTrue +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +internal class AnimatableTests { + @get:Rule val composeRule = createComposeRule() + + @Test + fun showsContentImmediately() { + composeRule + .apply { setContent { remember(Animatable::Moving).Animate { Text("1️⃣0️⃣") } } } + .onNodeWithText("1️⃣0️⃣") + .assertIsDisplayed() + } + + @Test + fun waitsForAnimation() { + val animatable = Animatable.Moving() + var hasAnimationFinished = false + runTest(@OptIn(ExperimentalCoroutinesApi::class) UnconfinedTestDispatcher()) { + launch { + animatable.waitForAnimation() + hasAnimationFinished = true + } + launch { + composeRule.setContent { animatable.Animate(fadeIn(tween())) {} } + composeRule.mainClock.advanceTimeBy(milliseconds = DefaultDurationMillis.toLong()) + assertThat(hasAnimationFinished).isTrue() + } + } + } +} diff --git a/platform/animator/src/main/AndroidManifest.xml b/platform/animator/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9dd94e09d --- /dev/null +++ b/platform/animator/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/Animator.kt b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/Animator.kt new file mode 100644 index 000000000..667cab5ee --- /dev/null +++ b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/Animator.kt @@ -0,0 +1,79 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.animator + +import androidx.compose.animation.EnterTransition +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.jeanbarrossilva.orca.platform.animator.animation.Motion +import com.jeanbarrossilva.orca.platform.animator.animation.animatable.Animatable +import com.jeanbarrossilva.orca.platform.animator.animation.animatable.Animatables +import com.jeanbarrossilva.orca.platform.animator.animation.timing.after +import com.jeanbarrossilva.orca.platform.animator.animation.timing.immediately + +/** + * Animator is an Orca-specific animation API built on top of Jetpack Compose's that facilitates the + * orchestration of sequential [EnterTransition]s that depend on others for them to start running. + * + * It works by providing [Animatable]s (not to be confused with + * [androidx.compose.animation.Animatable]) to the given [content], each intended to arbitrarily + * refer to a [Composable] that will have its visibility toggled on alongside the execution of an + * [EnterTransition]. + * + * [Composable]s can have their entrance animated by being provided as the content of their assigned + * [Animatable]'s [Animatable.Animate]. For example: + * ```kotlin + * Animator { (greeting) -> + * greeting.Animate(fadeIn()) { + * Text("Hello, world!") + * } + * } + * ``` + * + * In this case, the greeting would fade in [immediately], since it doesn't depend on other + * animations and no posterior delay has been specified. Similarly, the following is how it would be + * done if the greeting was to be displayed only 2 seconds [after] another [Composable]'s animation + * has finished running: + * ```kotlin + * Animator { (emoji, greeting) -> + * emoji.Animate(fadeIn()) { + * Text("🌎") + * } + * + * greeting.Animate(fadeIn(), after(emoji) + 2.seconds) { + * Text("Hello, world!") + * } + * } + * ``` + * + * Note that this [Composable], [Animator], serves merely as an entrypoint to the overall API and + * doesn't have any intrinsic behavior other than just showing the [content] and providing a + * remembered instance of [Animatables] to it. + * + * @param motion Indicates whether the specified animations should be run or if the [Composable]s + * should be shown instantly; defaults to [Motion.Moving], which, as expected from an animation + * API, turns them on and plays the [EnterTransition]s as they were declared to be performed. + * + * Setting it to [Motion.Still] for disabling them might be useful for both previewing the final + * state of the animated [Composable]s and testing. + * + * @param content Content to be shown in which [Animatable]-based animations can be run. + */ +@Composable +fun Animator(motion: Motion = Motion.Moving, content: @Composable (Animatables) -> Unit) { + val animatables = remember(motion) { Animatables(motion) } + content(animatables) +} diff --git a/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/Animation.kt b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/Animation.kt new file mode 100644 index 000000000..85fd76fcd --- /dev/null +++ b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/Animation.kt @@ -0,0 +1,34 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.animator.animation + +import androidx.compose.runtime.Immutable + +/** Stage in which an animation can be. */ +@Immutable +internal enum class Animation { + /** States that an animation hasn't started running. */ + Idle, + + /** Denotes that an animation has been requested to be run but hasn't started yet. */ + Prepared, + + /** Indicates that an animation is in progress. */ + Ongoing, + + /** Represents that an animation has finished running. */ + Ended +} diff --git a/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/Motion.kt b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/Motion.kt new file mode 100644 index 000000000..ac958c967 --- /dev/null +++ b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/Motion.kt @@ -0,0 +1,41 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.animator.animation + +import com.jeanbarrossilva.orca.platform.animator.animation.animatable.Animatable + +/** Indicates whether animations should be run or not. */ +enum class Motion { + /** Denotes that animations should be enabled and run as expected. */ + Moving { + override fun createAnimatable(): Animatable { + return Animatable.Moving() + } + }, + + /** + * Denotes that all animations should be disabled and that the content should be displayed + * instantly. + */ + Still { + override fun createAnimatable(): Animatable { + return Animatable.Still() + } + }; + + /** Creates an [Animatable]. */ + internal abstract fun createAnimatable(): Animatable +} diff --git a/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/animatable/Animatable.kt b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/animatable/Animatable.kt new file mode 100644 index 000000000..7706cb949 --- /dev/null +++ b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/animatable/Animatable.kt @@ -0,0 +1,122 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.animator.animation.animatable + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import com.jeanbarrossilva.orca.ext.coroutines.await +import com.jeanbarrossilva.orca.ext.coroutines.getValue +import com.jeanbarrossilva.orca.ext.coroutines.setValue +import com.jeanbarrossilva.orca.platform.animator.animation.Animation +import com.jeanbarrossilva.orca.platform.animator.animation.timing.Timing +import com.jeanbarrossilva.orca.platform.animator.animation.timing.immediately +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter + +/** + * Schedules the execution of the animation of a [Composable]. + * + * @param initialAnimation Stage that is considered to be the starting one of an animation. + */ +sealed class Animatable(initialAnimation: Animation) { + /** [MutableStateFlow] to which the current stage of the animation is emitted. */ + internal val animationFlow = MutableStateFlow(initialAnimation) + + /** [Animatable] that animates the entrance of its content. */ + internal class Moving : Animatable(initialAnimation = Animation.Idle) { + /** Stage in which the animation currently is. */ + private var animation by animationFlow + + @Composable + override fun Animate( + transition: EnterTransition, + timing: Timing, + content: @Composable () -> Unit + ) { + LaunchedEffect(transition, timing, content) { + animation = Animation.Idle + timing.time() + animation = Animation.Ended + } + + AnimatedVisibility( + visible = animationFlow.collectAsState().value >= Animation.Prepared, + enter = transition + ) { + @OptIn(ExperimentalAnimationApi::class) + LaunchedEffect(this.transition.isRunning) { + if (this@AnimatedVisibility.transition.isRunning) { + animation = Animation.Ongoing + } + } + + content() + } + } + } + + /** [Animatable] that displays its content instantly, ignoring any specified animation. */ + internal class Still : Animatable(initialAnimation = Animation.Ended) { + @Composable + override fun Animate( + transition: EnterTransition, + timing: Timing, + content: @Composable () -> Unit + ) { + content() + } + } + + /** + * Shows the [content] while animating it with the given [transition]. + * + * @param transition [EnterTransition] to animate the [content]'s visibility change. + * @param timing [Timing] that dictates when the animation will start running. + * @param content [Composable] to be displayed. + */ + @Composable + abstract fun Animate(transition: EnterTransition, timing: Timing, content: @Composable () -> Unit) + + /** + * Shows the [content] immediately without an [EnterTransition]. + * + * @param content [Composable] to be displayed. + */ + @Composable + fun Animate(content: @Composable () -> Unit) { + Animate(EnterTransition.None, content) + } + + /** + * Shows the [content] immediately while animating it with the given [transition]. + * + * @param transition [EnterTransition] to animate the [content]'s visibility change. + * @param content [Composable] to be displayed. + */ + @Composable + fun Animate(transition: EnterTransition, content: @Composable () -> Unit) { + Animate(transition, immediately(), content) + } + + /** Suspends until the animation finishes running. */ + internal suspend fun waitForAnimation() { + animationFlow.filter { it == Animation.Ended }.await() + } +} diff --git a/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/animatable/Animatables.kt b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/animatable/Animatables.kt new file mode 100644 index 000000000..ca808d05f --- /dev/null +++ b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/animatable/Animatables.kt @@ -0,0 +1,106 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.animator.animation.animatable + +import com.jeanbarrossilva.orca.platform.animator.animation.Motion + +/** + * Allows for the provision of [Animatable]s via destructuring. + * + * @param motion [Motion] by which all [Animatable]s will be created. + */ +@JvmInline +value class Animatables internal constructor(private val motion: Motion) { + /** Creates a first [Animatable]. */ + operator fun component1(): Animatable { + return motion.createAnimatable() + } + + /** Creates a second [Animatable]. */ + operator fun component2(): Animatable { + return motion.createAnimatable() + } + + /** Creates a third [Animatable]. */ + operator fun component3(): Animatable { + return motion.createAnimatable() + } + + /** Creates a fourth [Animatable]. */ + operator fun component4(): Animatable { + return motion.createAnimatable() + } + + /** Creates a fifth [Animatable]. */ + operator fun component5(): Animatable { + return motion.createAnimatable() + } + + /** Creates a sixth [Animatable]. */ + operator fun component6(): Animatable { + return motion.createAnimatable() + } + + /** Creates a seventh [Animatable]. */ + operator fun component7(): Animatable { + return motion.createAnimatable() + } + + /** Creates an eighth [Animatable]. */ + operator fun component8(): Animatable { + return motion.createAnimatable() + } + + /** Creates a ninth [Animatable]. */ + operator fun component9(): Animatable { + return motion.createAnimatable() + } + + /** Creates a tenth [Animatable]. */ + operator fun component10(): Animatable { + return motion.createAnimatable() + } + + /** Creates an eleventh [Animatable]. */ + operator fun component11(): Animatable { + return motion.createAnimatable() + } + + /** Creates a twelfth [Animatable]. */ + operator fun component12(): Animatable { + return motion.createAnimatable() + } + + /** Creates a thirteenth [Animatable]. */ + operator fun component13(): Animatable { + return motion.createAnimatable() + } + + /** Creates a fourteenth [Animatable]. */ + operator fun component14(): Animatable { + return motion.createAnimatable() + } + + /** Creates a fifteenth [Animatable]. */ + operator fun component15(): Animatable { + return motion.createAnimatable() + } + + /** Creates a sixteenth [Animatable]. */ + operator fun component16(): Animatable { + return motion.createAnimatable() + } +} diff --git a/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/timing/Timing.extensions.kt b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/timing/Timing.extensions.kt new file mode 100644 index 000000000..d680e57cc --- /dev/null +++ b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/timing/Timing.extensions.kt @@ -0,0 +1,34 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.animator.animation.timing + +import com.jeanbarrossilva.orca.platform.animator.animation.animatable.Animatable + +/** [Timing] that indicates that an animation should be run immediately. */ +fun immediately(): Timing { + return Timing.Immediate() +} + +/** + * [Timing] that indicates that an animation should be run after the given [animatable] has finished + * animating. + * + * @param animatable [Animatable] whose animation has to finish for the one to which this [Timing] + * refers to to start. + */ +fun after(animatable: Animatable): Timing { + return Timing.Sequential(animatable) +} diff --git a/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/timing/Timing.kt b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/timing/Timing.kt new file mode 100644 index 000000000..b0a8fe268 --- /dev/null +++ b/platform/animator/src/main/java/com/jeanbarrossilva/orca/platform/animator/animation/timing/Timing.kt @@ -0,0 +1,66 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.animator.animation.timing + +import androidx.annotation.CallSuper +import com.jeanbarrossilva.orca.platform.animator.animation.animatable.Animatable +import kotlin.time.Duration +import kotlinx.coroutines.delay + +/** Indicates when an animation should be run. */ +sealed class Timing { + /** [Duration] to be waited for before the animation runs. */ + internal abstract val delay: Duration + + /** Indicates that an animation should be run immediately. */ + internal data class Immediate(override val delay: Duration = Duration.ZERO) : Timing() { + override fun plus(delay: Duration): Timing { + return Immediate(delay) + } + } + + /** + * Indicates that an animation should be run after the given [animatable] has finished animating. + * + * @param animatable [Animatable] whose animation has to finish for the one's to which this + * [Timing] refers to to start. + */ + internal data class Sequential( + private val animatable: Animatable, + override val delay: Duration = Duration.ZERO + ) : Timing() { + override fun plus(delay: Duration): Timing { + return Sequential(animatable, delay) + } + + override suspend fun time() { + animatable.waitForAnimation() + super.time() + } + } + + /** Indicates that the animation should only be run after the given amount of time. */ + abstract operator fun plus(delay: Duration): Timing + + /** + * Suspends until the condition specified by this [Timing] for the animation to be run is + * satisfied. + */ + @CallSuper + internal open suspend fun time() { + delay(delay) + } +} diff --git a/platform/animator/src/test/java/com/jeanbarrossilva/orca/platform/animator/animation/timing/AnimatableScopeExtensionsTests.kt b/platform/animator/src/test/java/com/jeanbarrossilva/orca/platform/animator/animation/timing/AnimatableScopeExtensionsTests.kt new file mode 100644 index 000000000..5a5abe42a --- /dev/null +++ b/platform/animator/src/test/java/com/jeanbarrossilva/orca/platform/animator/animation/timing/AnimatableScopeExtensionsTests.kt @@ -0,0 +1,34 @@ +/* + * Copyright © 2024 Orca + * + * This program is free software: you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If + * not, see https://www.gnu.org/licenses. + */ + +package com.jeanbarrossilva.orca.platform.animator.animation.timing + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.jeanbarrossilva.orca.platform.animator.animation.animatable.Animatable +import kotlin.test.Test + +internal class AnimatableScopeExtensionsTests { + @Test + fun createsImmediateTiming() { + assertThat(immediately()).isEqualTo(Timing.Immediate()) + } + + @Test + fun createsSequentialTiming() { + val animatable = Animatable.Still() + assertThat(after(animatable)).isEqualTo(Timing.Sequential(animatable)) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7851d2317..edcda27ab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,6 +49,7 @@ include( ":feature:search", ":feature:settings", ":feature:settings:term-muting", + ":platform:animator", ":platform:autos", ":platform:cache", ":platform:autos-test",