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"
         }
     }