From 7d176305bd5c673f754b5e1dcce86297bee7bebd Mon Sep 17 00:00:00 2001 From: Sungyong An <soupyong@gmail.com> Date: Wed, 24 Apr 2024 02:24:18 +0900 Subject: [PATCH] Add sample for ContainerTransform --- android/build.gradle | 1 + .../motion/sample/ui/demo/AlbumContents.kt | 3 +- .../motion/sample/ui/demo/AlbumScreen.kt | 18 +++++- .../sample/ui/demo/ContainerTransform.kt | 62 ++++++++++++++++++ .../motion/sample/ui/demo/DemoScreen.kt | 63 +++++++++---------- .../motion/sample/ui/demo/LibraryContents.kt | 19 +++++- .../motion/sample/ui/demo/LibraryScreen.kt | 37 ++++++----- build.gradle | 1 + 8 files changed, 152 insertions(+), 52 deletions(-) create mode 100644 android/src/main/java/soup/compose/material/motion/sample/ui/demo/ContainerTransform.kt diff --git a/android/build.gradle b/android/build.gradle index 02803507..7a338d80 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -52,6 +52,7 @@ android { dependencies { implementation project(':core') + implementation libs.compose.animation implementation libs.compose.ui.ui implementation libs.compose.ui.tooling implementation libs.compose.material diff --git a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/AlbumContents.kt b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/AlbumContents.kt index 29d9c0a7..d54f9910 100644 --- a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/AlbumContents.kt +++ b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/AlbumContents.kt @@ -65,6 +65,7 @@ import soup.compose.material.motion.sample.ui.theme.Purple200 fun AlbumScaffold( upPress: () -> Unit, collapse: Boolean, + modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { val backgroundColor = if (collapse) { @@ -75,7 +76,7 @@ fun AlbumScaffold( } else { Color.Transparent } - Box(modifier = Modifier.fillMaxSize()) { + Box(modifier = modifier) { Surface(modifier = Modifier.fillMaxSize(), content = content) TopAppBar( modifier = Modifier diff --git a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/AlbumScreen.kt b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/AlbumScreen.kt index 103db5e0..e98f8166 100644 --- a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/AlbumScreen.kt +++ b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/AlbumScreen.kt @@ -15,7 +15,11 @@ */ package soup.compose.material.motion.sample.ui.demo +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -32,6 +36,8 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch +context(SharedTransitionScope, AnimatedVisibilityScope) +@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun AlbumScreen(album: MusicData.Album, upPress: () -> Unit) { val density = LocalDensity.current @@ -62,9 +68,17 @@ fun AlbumScreen(album: MusicData.Album, upPress: () -> Unit) { } AlbumScaffold( upPress = upPress, - collapse = collapse + collapse = collapse, + modifier = Modifier + .fillMaxSize() + .containerTransform( + rememberSharedContentState(key = "album-${album.id}"), + this@AnimatedVisibilityScope, + ), ) { - LazyColumn(state = listState) { + LazyColumn( + state = listState, + ) { item { AlbumHeader(album, showFab) } diff --git a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/ContainerTransform.kt b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/ContainerTransform.kt new file mode 100644 index 00000000..784f17de --- /dev/null +++ b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/ContainerTransform.kt @@ -0,0 +1,62 @@ +@file:OptIn(ExperimentalSharedTransitionApi::class) + +package soup.compose.material.motion.sample.ui.demo + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.SharedTransitionScope.SharedContentState +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.ui.Modifier +import soup.compose.material.motion.MotionConstants + +context(SharedTransitionScope) +fun Modifier.containerTransform( + state: SharedContentState, + animatedVisibilityScope: AnimatedVisibilityScope, + placeHolderSize: SharedTransitionScope.PlaceHolderSize = SharedTransitionScope.PlaceHolderSize.contentSize, + renderInOverlayDuringTransition: Boolean = true, + zIndexInOverlay: Float = 0f, +): Modifier = sharedBounds( + sharedContentState = state, + animatedVisibilityScope = animatedVisibilityScope, + enter = containerTransformIn(), + exit = containerTransformOut(), + placeHolderSize = placeHolderSize, + renderInOverlayDuringTransition = renderInOverlayDuringTransition, + zIndexInOverlay = zIndexInOverlay, +) + +private const val ProgressThreshold = 0.35f + +private val Int.ForOutgoing: Int + get() = (this * ProgressThreshold).toInt() + +private val Int.ForIncoming: Int + get() = this - this.ForOutgoing + +private fun containerTransformIn( + durationMillis: Int = MotionConstants.DefaultMotionDuration, +): EnterTransition = fadeIn( + animationSpec = tween( + durationMillis = durationMillis.ForIncoming, + delayMillis = durationMillis.ForOutgoing, + easing = LinearOutSlowInEasing + ) +) + +private fun containerTransformOut( + durationMillis: Int = MotionConstants.DefaultMotionDuration, +): ExitTransition = fadeOut( + animationSpec = tween( + durationMillis = durationMillis.ForOutgoing, + delayMillis = 0, + easing = FastOutLinearInEasing + ) +) diff --git a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/DemoScreen.kt b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/DemoScreen.kt index f8f7303b..78f8de28 100644 --- a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/DemoScreen.kt +++ b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/DemoScreen.kt @@ -17,7 +17,8 @@ package soup.compose.material.motion.sample.ui.demo import android.content.res.Configuration import androidx.activity.compose.BackHandler -import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavType @@ -25,45 +26,39 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import soup.compose.material.motion.animation.holdIn -import soup.compose.material.motion.animation.holdOut -import soup.compose.material.motion.animation.translateYIn -import soup.compose.material.motion.animation.translateYOut import soup.compose.material.motion.sample.ui.theme.SampleTheme -@OptIn(ExperimentalAnimationApi::class) +@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun DemoScreen(upPress: () -> Unit) { val navController = rememberNavController() - NavHost(navController, startDestination = "library") { - composable( - "library", - enterTransition = { holdIn() }, - exitTransition = { holdOut() }, - ) { - BackHandler { - upPress() - } - LibraryScreen( - onItemClick = { - navController.navigate("album/${it.id}") - } - ) - } - composable( - "album/{albumId}", - arguments = listOf(navArgument("albumId") { type = NavType.LongType }), - enterTransition = { translateYIn { it } }, - exitTransition = { translateYOut { it } }, - ) { backStackEntry -> - val currentId = backStackEntry.arguments?.getLong("albumId") - val album = MusicData.albums.first { it.id == currentId } - AlbumScreen( - album, - upPress = { - navController.popBackStack() + SharedTransitionLayout { + NavHost(navController, startDestination = "library") { + composable( + "library", + ) { + BackHandler { + upPress() } - ) + LibraryScreen( + onItemClick = { + navController.navigate("album/${it.id}") + } + ) + } + composable( + "album/{albumId}", + arguments = listOf(navArgument("albumId") { type = NavType.LongType }), + ) { backStackEntry -> + val currentId = backStackEntry.arguments?.getLong("albumId") + val album = MusicData.albums.first { it.id == currentId } + AlbumScreen( + album, + upPress = { + navController.popBackStack() + } + ) + } } } } diff --git a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/LibraryContents.kt b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/LibraryContents.kt index baafdbe0..c0753930 100644 --- a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/LibraryContents.kt +++ b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/LibraryContents.kt @@ -13,8 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalSharedTransitionApi::class) + package soup.compose.material.motion.sample.ui.demo +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -48,6 +53,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +context(SharedTransitionScope, AnimatedVisibilityScope) @Composable fun LibraryGridContents( items: List<MusicData.Album>, @@ -65,6 +71,7 @@ fun LibraryGridContents( } } +context(SharedTransitionScope, AnimatedVisibilityScope) @OptIn(ExperimentalMaterialApi::class) @Composable private fun LibraryGridItem( @@ -76,6 +83,10 @@ private fun LibraryGridItem( modifier = Modifier .fillMaxWidth() .padding(4.dp) + .containerTransform( + rememberSharedContentState(key = "album-${album.id}"), + this@AnimatedVisibilityScope, + ), ) { Column { Image( @@ -107,6 +118,7 @@ private fun LibraryGridItem( } } +context(SharedTransitionScope, AnimatedVisibilityScope) @Composable fun LibraryLinearContents( items: List<MusicData.Album>, @@ -124,6 +136,7 @@ fun LibraryLinearContents( } } +context(SharedTransitionScope, AnimatedVisibilityScope) @Composable private fun LibraryLinearItem( album: MusicData.Album, @@ -134,7 +147,11 @@ private fun LibraryLinearItem( .fillMaxWidth() .requiredHeight(88.dp) .clickable { onItemClick(album) } - .padding(horizontal = 16.dp), + .padding(horizontal = 16.dp) + .containerTransform( + rememberSharedContentState(key = "album-${album.id}"), + this@AnimatedVisibilityScope, + ), verticalAlignment = Alignment.CenterVertically ) { Image( diff --git a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/LibraryScreen.kt b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/LibraryScreen.kt index cbcef3d4..ca3a287e 100644 --- a/android/src/main/java/soup/compose/material/motion/sample/ui/demo/LibraryScreen.kt +++ b/android/src/main/java/soup/compose/material/motion/sample/ui/demo/LibraryScreen.kt @@ -13,8 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalSharedTransitionApi::class) + package soup.compose.material.motion.sample.ui.demo +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -82,6 +87,7 @@ private data class LibraryState( } } +context(SharedTransitionScope, AnimatedVisibilityScope) @Composable fun LibraryScreen(onItemClick: (MusicData.Album) -> Unit) { val (state, onStateChanged) = rememberSaveable(stateSaver = Saver) { @@ -120,20 +126,22 @@ fun LibraryScreen(onItemClick: (MusicData.Album) -> Unit) { onListTypeChange(newType) }, ) { innerPadding -> - val slideDistance = rememberSlideDistance() - MaterialMotion( - targetState = state, - transitionSpec = { - when (targetState.motionSpecType) { - MotionSpecType.SharedAxis -> materialSharedAxisY(forward = true, slideDistance) - MotionSpecType.FadeThrough -> materialFadeThrough() - } - }, - modifier = Modifier.padding(innerPadding), - pop = false - ) { currentDestination -> - LibraryContents(currentDestination, onItemClick) - } + //FIXME: not working +// val slideDistance = rememberSlideDistance() +// MaterialMotion( +// targetState = state, +// transitionSpec = { +// when (targetState.motionSpecType) { +// MotionSpecType.SharedAxis -> materialSharedAxisY(forward = true, slideDistance) +// MotionSpecType.FadeThrough -> materialFadeThrough() +// } +// }, +// modifier = Modifier.padding(innerPadding), +// pop = false +// ) { currentDestination -> +// LibraryContents(currentDestination, onItemClick) +// } + LibraryContents(state, onItemClick) } } @@ -167,6 +175,7 @@ fun LibraryScaffold( ) } +context(SharedTransitionScope, AnimatedVisibilityScope) @Composable private fun LibraryContents( state: LibraryState, diff --git a/build.gradle b/build.gradle index d2fc3f0f..84089941 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,7 @@ subprojects { jvmTarget = "1.8" // Allow use of @OptIn freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + freeCompilerArgs += "-Xcontext-receivers" } }