= emptyList(),
)
diff --git a/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/transition/MovableContent.kt b/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/transition/MovableContent.kt
new file mode 100644
index 000000000..8786797f5
--- /dev/null
+++ b/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/transition/MovableContent.kt
@@ -0,0 +1,186 @@
+package com.bumble.appyx.core.navigation.transition
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.movableContentOf
+import androidx.compose.runtime.movableContentWithReceiverOf
+import com.bumble.appyx.core.node.LocalMovableContentMap
+import com.bumble.appyx.core.node.LocalNodeTargetVisibility
+
+
+/**
+ * Returns movable content for the given key. To reuse composable across Nodes movable content
+ * must to be invoked only once at any time, therefore we should return null for the case when the
+ * Node is transitioning to an invisible target and return movable content only if the targetState
+ * is visible.
+ *
+ * Example: ParentNode (P) has BackStack with one Active Node (A). We push Node (B) to the BackStack,
+ * and we want to move content from Node (A) to Node (B). Node (A) is transitioning from Active to
+ * Stashed (invisible) state and Node (B) is transitioning from Created to Active (visible) state.
+ * When this transition starts this function will return null for Node (A) and movable content for
+ * Node (B)so that this content will be moved from Node (A) to Node (B).
+ *
+ * If you have a custom NavModel keep in mind that you can only move content from a visible Node
+ * that becomes invisible to a Node that is becoming visible.
+ */
+@Composable
+fun localMovableContentWithTargetVisibility(
+ key: Any,
+ content: @Composable () -> Unit
+): (@Composable () -> Unit)? {
+ if (!LocalNodeTargetVisibility.current) return null
+ val movableContentMap = retrieveMovableContentMap()
+ return movableContentMap.getOrPut(key) {
+ movableContentOf {
+ content()
+ }
+ } as? @Composable () -> Unit ?: throw IllegalStateException(
+ "Movable content for key $key is not of the expected type." +
+ " The same $key has been used for different types of content."
+ )
+}
+
+@Composable
+fun localMovableContentWithTargetVisibility(
+ key: Any,
+ content: @Composable (P) -> Unit
+): (@Composable (P) -> Unit)? {
+ if (!LocalNodeTargetVisibility.current) return null
+ val movableContentMap = retrieveMovableContentMap()
+ return movableContentMap.getOrPut(key) {
+ movableContentOf
{
+ content(it)
+ }
+ } as? @Composable (P) -> Unit ?: throw IllegalStateException(
+ "Movable content for key $key is not of the expected type." +
+ " The same $key has been used for different types of content."
+ )
+}
+
+
+@Composable
+fun localMovableContentWithTargetVisibility(
+ key: Any,
+ content: @Composable (P1, P2) -> Unit
+): (@Composable (P1, P2) -> Unit)? {
+ if (!LocalNodeTargetVisibility.current) return null
+ val movableContentMap = retrieveMovableContentMap()
+ return movableContentMap.getOrPut(key) {
+ movableContentOf { p1, p2 ->
+ content(p1, p2)
+ }
+ } as? @Composable (P1, P2) -> Unit ?: throw IllegalStateException(
+ "Movable content for key $key is not of the expected type." +
+ " The same $key has been used for different types of content."
+ )
+}
+
+@Composable
+fun localMovableContentWithTargetVisibility(
+ key: Any,
+ content: @Composable (P1, P2, P3) -> Unit
+): (@Composable (P1, P2, P3) -> Unit)? {
+ if (!LocalNodeTargetVisibility.current) return null
+ val movableContentMap = retrieveMovableContentMap()
+ return movableContentMap.getOrPut(key) {
+ movableContentOf { p1, p2, p3 ->
+ content(p1, p2, p3)
+ }
+ } as? @Composable (P1, P2, P3) -> Unit ?: throw IllegalStateException(
+ "Movable content for key $key is not of the expected type." +
+ " The same $key has been used for different types of content."
+ )
+}
+
+@Composable
+fun localMovableContentWithTargetVisibility(
+ key: Any,
+ content: @Composable (P1, P2, P3, P4) -> Unit
+): (@Composable (P1, P2, P3, P4) -> Unit)? {
+ if (!LocalNodeTargetVisibility.current) return null
+ val movableContentMap = retrieveMovableContentMap()
+ return movableContentMap.getOrPut(key) {
+ movableContentOf { p1, p2, p3, p4 ->
+ content(p1, p2, p3, p4)
+ }
+ } as? @Composable (P1, P2, P3, P4) -> Unit ?: throw IllegalStateException(
+ "Movable content for key $key is not of the expected type." +
+ " The same $key has been used for different types of content."
+ )
+}
+
+@Composable
+fun localMovableContentWithReceiverAndTargetVisibility(
+ key: Any,
+ content: @Composable R.() -> Unit
+): (@Composable R.() -> Unit)? {
+ if (!LocalNodeTargetVisibility.current) return null
+ val movableContentMap = retrieveMovableContentMap()
+ return movableContentMap.getOrPut(key) {
+ movableContentWithReceiverOf {
+ this.content()
+ }
+ } as? @Composable R.() -> Unit ?: throw IllegalStateException(
+ "Movable content for key $key is not of the expected type." +
+ " The same $key has been used for different types of content."
+ )
+}
+
+@Composable
+fun localMovableContentWithReceiverAndTargetVisibility(
+ key: Any,
+ content: @Composable R.(P) -> Unit
+): (@Composable R.(P) -> Unit)? {
+ if (!LocalNodeTargetVisibility.current) return null
+ val movableContentMap = retrieveMovableContentMap()
+ return movableContentMap.getOrPut(key) {
+ movableContentWithReceiverOf { p ->
+ this.content(p)
+ }
+ } as? @Composable R.(P) -> Unit ?: throw IllegalStateException(
+ "Movable content for key $key is not of the expected type." +
+ " The same $key has been used for different types of content."
+ )
+}
+
+@Composable
+fun localMovableContentWithReceiverAndTargetVisibility(
+ key: Any,
+ content: @Composable R.(P1, P2) -> Unit
+): (@Composable R.(P1, P2) -> Unit)? {
+ if (!LocalNodeTargetVisibility.current) return null
+ val movableContentMap = retrieveMovableContentMap()
+ return movableContentMap.getOrPut(key) {
+ movableContentWithReceiverOf { p1, p2 ->
+ this.content(p1, p2)
+ }
+ } as? @Composable R.(P1, P2) -> Unit ?: throw IllegalStateException(
+ "Movable content for key $key is not of the expected type." +
+ " The same $key has been used for different types of content."
+ )
+}
+
+@Composable
+fun localMovableContentWithReceiverAndTargetVisibility(
+ key: Any,
+ content: @Composable R.(P1, P2, P3) -> Unit
+): (@Composable R.(P1, P2, P3) -> Unit)? {
+ if (!LocalNodeTargetVisibility.current) return null
+ val movableContentMap = retrieveMovableContentMap()
+ return movableContentMap.getOrPut(key) {
+ movableContentWithReceiverOf { p1, p2, p3 ->
+ this.content(p1, p2, p3)
+ }
+ } as? @Composable R.(P1, P2, P3) -> Unit ?: throw IllegalStateException(
+ "Movable content for key $key is not of the expected type." +
+ " The same $key has been used for different types of content."
+ )
+}
+
+@Composable
+private fun retrieveMovableContentMap(): MutableMap {
+ return requireNotNull(LocalMovableContentMap.current) {
+ "LocalMovableContentMap not found in the composition hierarchy." +
+ " Please use withMovableContent = true on Children composable."
+ }
+}
+
diff --git a/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/transition/SharedElement.kt b/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/transition/SharedElement.kt
new file mode 100644
index 000000000..f5eb21c8d
--- /dev/null
+++ b/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/transition/SharedElement.kt
@@ -0,0 +1,63 @@
+package com.bumble.appyx.core.navigation.transition
+
+import androidx.compose.animation.BoundsTransform
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VisibilityThreshold
+import androidx.compose.animation.core.spring
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import com.bumble.appyx.core.node.LocalNodeTargetVisibility
+import com.bumble.appyx.core.node.LocalSharedElementScope
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+fun Modifier.sharedElement(
+ key: Any,
+ boundsTransform: BoundsTransform = DefaultBoundsTransform,
+ placeHolderSize: PlaceHolderSize = PlaceHolderSize.contentSize,
+ renderInOverlayDuringTransition: Boolean = true,
+ zIndexInOverlay: Float = 0f,
+ clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip = ParentClip
+) = composed {
+ val scope = requireNotNull(LocalSharedElementScope.current) {
+ "LocalSharedElementScope is not provided. Please set withSharedElementTransition = true for Children composable"
+ }
+ scope.run {
+ this@composed.sharedElementWithCallerManagedVisibility(
+ boundsTransform = boundsTransform,
+ placeHolderSize = placeHolderSize,
+ renderInOverlayDuringTransition = renderInOverlayDuringTransition,
+ zIndexInOverlay = zIndexInOverlay,
+ clipInOverlayDuringTransition = clipInOverlayDuringTransition,
+ sharedContentState = rememberSharedContentState(key = key),
+ visible = LocalNodeTargetVisibility.current
+ )
+ }
+}
+
+@ExperimentalSharedTransitionApi
+private val ParentClip: SharedTransitionScope.OverlayClip =
+ object : SharedTransitionScope.OverlayClip {
+ override fun getClipPath(
+ state: SharedTransitionScope.SharedContentState,
+ bounds: Rect,
+ layoutDirection: LayoutDirection,
+ density: Density
+ ): Path? {
+ return state.parentSharedContentState?.clipPathInOverlay
+ }
+ }
+
+@ExperimentalSharedTransitionApi
+private val DefaultBoundsTransform = BoundsTransform { _, _ ->
+ spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = Rect.VisibilityThreshold
+ )
+}
diff --git a/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/LocalNode.kt b/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/LocalNode.kt
index b0a8fcd96..ec283e0c2 100644
--- a/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/LocalNode.kt
+++ b/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/LocalNode.kt
@@ -1,5 +1,23 @@
package com.bumble.appyx.core.node
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
import androidx.compose.runtime.compositionLocalOf
val LocalNode = compositionLocalOf { null }
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+val LocalSharedElementScope = compositionLocalOf { null }
+
+/**
+ * Represents the target visibility of this Node. For instance, if the underlying
+ * NavModel is a BackStack and the NavKey's target state is BackStack.ACTIVE this will return true
+ * as they ACTIVE state is visible on the screen. In the target state is STASHED or DESTROYED it will
+ * return false.
+ */
+val LocalNodeTargetVisibility = compositionLocalOf { false }
+
+/**
+ * Represents the map from which movable content can be retrieved.
+ */
+val LocalMovableContentMap = compositionLocalOf?> { null }
diff --git a/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/ParentNode.kt b/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/ParentNode.kt
index 866cc4a5a..0537e9904 100644
--- a/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/ParentNode.kt
+++ b/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/ParentNode.kt
@@ -219,5 +219,4 @@ abstract class ParentNode(
}
-
}
diff --git a/samples/app/src/main/kotlin/com/bumble/appyx/app/node/samples/SamplesContainerNode.kt b/samples/app/src/main/kotlin/com/bumble/appyx/app/node/samples/SamplesContainerNode.kt
index 7be55abe3..471256b1a 100644
--- a/samples/app/src/main/kotlin/com/bumble/appyx/app/node/samples/SamplesContainerNode.kt
+++ b/samples/app/src/main/kotlin/com/bumble/appyx/app/node/samples/SamplesContainerNode.kt
@@ -6,10 +6,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
@@ -33,7 +33,7 @@ import com.bumble.appyx.navmodel.backstack.operation.newRoot
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackSlider
-import com.bumble.appyx.sample.navigtion.compose.ComposeNavigationRoot
+import com.bumble.appyx.sample.navigation.compose.ComposeNavigationRoot
import kotlinx.parcelize.Parcelize
class SamplesContainerNode(
diff --git a/samples/app/src/main/kotlin/com/bumble/appyx/app/node/samples/SamplesSelectorNode.kt b/samples/app/src/main/kotlin/com/bumble/appyx/app/node/samples/SamplesSelectorNode.kt
index 89b4f50ad..281594bc1 100644
--- a/samples/app/src/main/kotlin/com/bumble/appyx/app/node/samples/SamplesSelectorNode.kt
+++ b/samples/app/src/main/kotlin/com/bumble/appyx/app/node/samples/SamplesSelectorNode.kt
@@ -26,7 +26,7 @@ import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.node.node
-import com.bumble.appyx.sample.navigtion.compose.ComposeNavigationRoot
+import com.bumble.appyx.sample.navigation.compose.ComposeNavigationRoot
import kotlinx.parcelize.Parcelize
class SamplesSelectorNode(
diff --git a/samples/navigation-compose/build.gradle.kts b/samples/navigation-compose/build.gradle.kts
index fc2a079f2..3858c7078 100644
--- a/samples/navigation-compose/build.gradle.kts
+++ b/samples/navigation-compose/build.gradle.kts
@@ -10,7 +10,7 @@ plugins {
}
android {
- namespace = "com.bumble.appyx.sample.navigtion.compose"
+ namespace = "com.bumble.appyx.sample.navigation.compose"
compileSdk = libs.versions.androidCompileSdk.get().toInt()
defaultConfig {
diff --git a/samples/navigation-compose/src/androidTest/kotlin/com/bumble/appyx/sample/navigtion/compose/ComposeNavigationRootTest.kt b/samples/navigation-compose/src/androidTest/kotlin/com/bumble/appyx/sample/navigation/compose/ComposeNavigationRootTest.kt
similarity index 98%
rename from samples/navigation-compose/src/androidTest/kotlin/com/bumble/appyx/sample/navigtion/compose/ComposeNavigationRootTest.kt
rename to samples/navigation-compose/src/androidTest/kotlin/com/bumble/appyx/sample/navigation/compose/ComposeNavigationRootTest.kt
index 15e2d8593..7f7b47a64 100644
--- a/samples/navigation-compose/src/androidTest/kotlin/com/bumble/appyx/sample/navigtion/compose/ComposeNavigationRootTest.kt
+++ b/samples/navigation-compose/src/androidTest/kotlin/com/bumble/appyx/sample/navigation/compose/ComposeNavigationRootTest.kt
@@ -1,4 +1,4 @@
-package com.bumble.appyx.sample.navigtion.compose
+package com.bumble.appyx.sample.navigation.compose
import androidx.annotation.CheckResult
import androidx.compose.runtime.CompositionLocalProvider
diff --git a/samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigtion/compose/ComposeNavigationContainerNode.kt b/samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigation/compose/ComposeNavigationContainerNode.kt
similarity index 97%
rename from samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigtion/compose/ComposeNavigationContainerNode.kt
rename to samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigation/compose/ComposeNavigationContainerNode.kt
index e0dfdbd64..7c580475e 100644
--- a/samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigtion/compose/ComposeNavigationContainerNode.kt
+++ b/samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigation/compose/ComposeNavigationContainerNode.kt
@@ -1,4 +1,4 @@
-package com.bumble.appyx.sample.navigtion.compose
+package com.bumble.appyx.sample.navigation.compose
import android.os.Parcelable
import androidx.compose.foundation.layout.Column
diff --git a/samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigtion/compose/ComposeNavigationRoot.kt b/samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigation/compose/ComposeNavigationRoot.kt
similarity index 98%
rename from samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigtion/compose/ComposeNavigationRoot.kt
rename to samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigation/compose/ComposeNavigationRoot.kt
index 0f4998f7a..3dcfb8f27 100644
--- a/samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigtion/compose/ComposeNavigationRoot.kt
+++ b/samples/navigation-compose/src/main/kotlin/com/bumble/appyx/sample/navigation/compose/ComposeNavigationRoot.kt
@@ -1,4 +1,4 @@
-package com.bumble.appyx.sample.navigtion.compose
+package com.bumble.appyx.sample.navigation.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
diff --git a/samples/navmodel-samples/src/main/kotlin/com/bumble/appyx/navmodel/spotlightadvanced/transitionhandler/SpotlightAdvancedSlider.kt b/samples/navmodel-samples/src/main/kotlin/com/bumble/appyx/navmodel/spotlightadvanced/transitionhandler/SpotlightAdvancedSlider.kt
index 11d6c76cc..9edb7ed28 100644
--- a/samples/navmodel-samples/src/main/kotlin/com/bumble/appyx/navmodel/spotlightadvanced/transitionhandler/SpotlightAdvancedSlider.kt
+++ b/samples/navmodel-samples/src/main/kotlin/com/bumble/appyx/navmodel/spotlightadvanced/transitionhandler/SpotlightAdvancedSlider.kt
@@ -39,7 +39,7 @@ class SpotlightAdvancedSlider(
}
) : ModifierTransitionHandler() {
- @Suppress("ModifierFactoryExtensionFunction", "MagicNumber")
+ @Suppress("ModifierFactoryExtensionFunction", "MagicNumber", "SuspiciousModifierThen")
override fun createModifier(
modifier: Modifier,
transition: Transition,
diff --git a/samples/sandbox/build.gradle.kts b/samples/sandbox/build.gradle.kts
index b83eddb24..c6be113bd 100644
--- a/samples/sandbox/build.gradle.kts
+++ b/samples/sandbox/build.gradle.kts
@@ -27,8 +27,9 @@ android {
}
buildTypes {
release {
- isMinifyEnabled = false
+ isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ signingConfig = signingConfigs.getByName("debug")
}
}
buildFeatures {
diff --git a/samples/sandbox/proguard-rules.pro b/samples/sandbox/proguard-rules.pro
index e69de29bb..efb628c5d 100644
--- a/samples/sandbox/proguard-rules.pro
+++ b/samples/sandbox/proguard-rules.pro
@@ -0,0 +1,4 @@
+-dontwarn okhttp3.internal.platform.**
+-dontwarn org.conscrypt.**
+-dontwarn org.bouncycastle.**
+-dontwarn org.openjsse.**
diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/container/ContainerNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/container/ContainerNode.kt
index 6ededd3a0..a32b701fa 100644
--- a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/container/ContainerNode.kt
+++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/container/ContainerNode.kt
@@ -39,6 +39,9 @@ import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.MviCore
import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.MviCoreLeafExample
import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.NavModelExamples
import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.Picker
+import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.SharedElementExample
+import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.SharedElementPager
+import com.bumble.appyx.sandbox.client.container.ContainerNode.NavTarget.SharedElementWithMovableContentExample
import com.bumble.appyx.sandbox.client.customisations.CustomisationsNode
import com.bumble.appyx.sandbox.client.explicitnavigation.ExplicitNavigationExampleActivity
import com.bumble.appyx.sandbox.client.integrationpoint.IntegrationPointExampleNode
@@ -47,6 +50,8 @@ import com.bumble.appyx.sandbox.client.list.LazyListContainerNode
import com.bumble.appyx.sandbox.client.mvicoreexample.MviCoreExampleBuilder
import com.bumble.appyx.sandbox.client.mvicoreexample.leaf.MviCoreLeafBuilder
import com.bumble.appyx.sandbox.client.navmodels.NavModelExamplesNode
+import com.bumble.appyx.sandbox.client.sharedelement.SharedElementExampleNode
+import com.bumble.appyx.sandbox.client.sharedelement.with_pager.SharedElementPagerParentNode
import com.bumble.appyx.utils.customisations.NodeCustomisation
import kotlinx.parcelize.Parcelize
@@ -72,6 +77,12 @@ class ContainerNode internal constructor(
@Parcelize
object LazyExamples : NavTarget()
+ @Parcelize
+ object SharedElementExample : NavTarget()
+
+ @Parcelize
+ object SharedElementWithMovableContentExample : NavTarget()
+
@Parcelize
object IntegrationPointExample : NavTarget()
@@ -89,6 +100,9 @@ class ContainerNode internal constructor(
@Parcelize
object MviCoreLeafExample : NavTarget()
+
+ @Parcelize
+ object SharedElementPager : NavTarget()
}
@Suppress("ComplexMethod")
@@ -96,15 +110,27 @@ class ContainerNode internal constructor(
when (navTarget) {
is Picker -> node(buildContext) { modifier -> ExamplesList(modifier) }
is NavModelExamples -> NavModelExamplesNode(buildContext)
+ is SharedElementExample -> SharedElementExampleNode(buildContext)
+ is SharedElementWithMovableContentExample -> SharedElementExampleNode(
+ buildContext,
+ hasMovableContent = true
+ )
+
is LazyExamples -> LazyListContainerNode(buildContext)
is IntegrationPointExample -> IntegrationPointExampleNode(buildContext)
is BlockerExample -> BlockerExampleNode(buildContext)
is Customisations -> CustomisationsNode(buildContext)
- is MviCoreExample -> MviCoreExampleBuilder().build(buildContext, "MVICore initial state")
+ is MviCoreExample -> MviCoreExampleBuilder().build(
+ buildContext,
+ "MVICore initial state"
+ )
+
is MviCoreLeafExample -> MviCoreLeafBuilder().build(
buildContext,
"MVICore leaf initial state"
)
+
+ is SharedElementPager -> SharedElementPagerParentNode(buildContext)
}
@Composable
@@ -137,6 +163,13 @@ class ContainerNode internal constructor(
label?.let {
Text(it, textAlign = TextAlign.Center)
}
+ TextButton("Shared element Pager") { backStack.push(SharedElementPager) }
+ TextButton("Shared element ") { backStack.push(SharedElementExample) }
+ TextButton("Shared element with movable content") {
+ backStack.push(
+ SharedElementWithMovableContentExample
+ )
+ }
TextButton("NavModel Examples") { backStack.push(NavModelExamples) }
TextButton("Customisations Example") { backStack.push(Customisations) }
TextButton("Explicit navigation example") {
diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/FullScreenNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/FullScreenNode.kt
new file mode 100644
index 000000000..959331ff6
--- /dev/null
+++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/FullScreenNode.kt
@@ -0,0 +1,106 @@
+package com.bumble.appyx.sandbox.client.sharedelement
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.navigation.transition.sharedElement
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.samples.common.profile.Profile
+import com.bumble.appyx.samples.common.profile.ProfileImage
+
+class FullScreenNode(
+ private val onClick: (Int) -> Unit,
+ private val hasMovableContent: Boolean,
+ private val profileId: Int,
+ buildContext: BuildContext
+) : Node(buildContext) {
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ if (hasMovableContent) {
+ SharedElementWithMovableContentContent(modifier)
+ } else {
+ SharedElementContent(modifier)
+ }
+ }
+
+ @OptIn(ExperimentalSharedTransitionApi::class)
+ @Composable
+ private fun SharedElementContent(
+ modifier: Modifier = Modifier
+ ) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .clickable {
+ onClick(profileId)
+ }
+ ) {
+ val profile = Profile.allProfiles[profileId]
+
+ ProfileImage(
+ Profile.allProfiles[profileId].drawableRes, modifier = Modifier
+ .fillMaxSize()
+ .sharedElement(key = "$profileId image")
+ )
+
+ Text(
+ text = "${profile.name}, ${profile.age}",
+ color = Color.White,
+ fontSize = 30.sp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .sharedElement(key = "$profileId text")
+ .align(Alignment.BottomStart)
+ .padding(16.dp)
+ )
+ }
+ }
+
+ @OptIn(ExperimentalSharedTransitionApi::class)
+ @Composable
+ private fun SharedElementWithMovableContentContent(
+ modifier: Modifier = Modifier
+ ) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .clickable {
+ onClick(profileId)
+ }
+ ) {
+ val profile = Profile.allProfiles[profileId]
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .sharedElement(key = "$profileId image")
+ ) {
+ ProfileImageWithCounterMovableContent(profileId)
+ }
+
+ Text(
+ text = "${profile.name}, ${profile.age}",
+ color = Color.White,
+ fontSize = 30.sp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .sharedElement(key = "$profileId text")
+ .align(Alignment.BottomStart)
+ .padding(16.dp)
+ )
+ }
+ }
+}
+
diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/ProfileHorizontalListNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/ProfileHorizontalListNode.kt
new file mode 100644
index 000000000..dd9b79741
--- /dev/null
+++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/ProfileHorizontalListNode.kt
@@ -0,0 +1,122 @@
+package com.bumble.appyx.sandbox.client.sharedelement
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.navigation.transition.sharedElement
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.samples.common.profile.Profile
+import com.bumble.appyx.samples.common.profile.ProfileImage
+
+class ProfileHorizontalListNode(
+ private val selectedId: Int,
+ private val onProfileClick: (Int) -> Unit,
+ private val hasMovableContent: Boolean,
+ buildContext: BuildContext
+) : Node(buildContext) {
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = rememberLazyListState(initialFirstVisibleItemIndex = selectedId)
+ LazyRow(
+ state = state,
+ modifier = modifier.fillMaxSize(),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(32.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ repeat(10) { profileId ->
+ item(key = profileId) {
+ if (hasMovableContent) {
+ SharedElementWithMovableContentContent(profileId)
+ } else {
+ SharedElementContent(profileId)
+ }
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalSharedTransitionApi::class)
+ @Composable
+ private fun SharedElementContent(
+ profileId: Int,
+ modifier: Modifier = Modifier
+ ) {
+ Box(
+ modifier = modifier
+ .requiredSize(200.dp)
+ .clickable {
+ onProfileClick(profileId)
+ }
+ ) {
+ val profile = Profile.allProfiles[profileId]
+
+ ProfileImage(
+ Profile.allProfiles[profileId].drawableRes, modifier = Modifier
+ .fillMaxSize()
+ .sharedElement(key = "$profileId image")
+ )
+ Text(
+ text = "${profile.name}, ${profile.age}",
+ color = Color.White,
+ fontSize = 16.sp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .sharedElement(key = "$profileId text")
+ .align(Alignment.BottomStart)
+ .padding(8.dp)
+ )
+ }
+ }
+
+ @OptIn(ExperimentalSharedTransitionApi::class)
+ @Composable
+ private fun SharedElementWithMovableContentContent(
+ profileId: Int,
+ modifier: Modifier = Modifier
+ ) {
+ Box(
+ modifier = modifier
+ .requiredSize(200.dp)
+ .clickable {
+ onProfileClick(profileId)
+ }
+ ) {
+ val profile = Profile.allProfiles[profileId]
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .sharedElement(key = "$profileId image")
+ ) {
+ ProfileImageWithCounterMovableContent(profileId)
+ }
+ Text(
+ text = "${profile.name}, ${profile.age}",
+ color = Color.White,
+ fontSize = 16.sp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .sharedElement(key = "$profileId text")
+ .align(Alignment.BottomStart)
+ .padding(8.dp)
+ )
+ }
+ }
+}
diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/ProfileVerticalListNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/ProfileVerticalListNode.kt
new file mode 100644
index 000000000..b735c215a
--- /dev/null
+++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/ProfileVerticalListNode.kt
@@ -0,0 +1,122 @@
+package com.bumble.appyx.sandbox.client.sharedelement
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.navigation.transition.sharedElement
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.samples.common.profile.Profile
+import com.bumble.appyx.samples.common.profile.ProfileImage
+
+class ProfileVerticalListNode(
+ private val profileId: Int,
+ private val onProfileClick: (Int) -> Unit,
+ private val hasMovableContent: Boolean,
+ buildContext: BuildContext
+) : Node(buildContext) {
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ val state = rememberLazyListState(initialFirstVisibleItemIndex = profileId)
+ LazyColumn(
+ state = state,
+ modifier = modifier.fillMaxSize(),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(32.dp),
+ ) {
+ repeat(10) { profileId ->
+ item(key = profileId) {
+ if (hasMovableContent) {
+ SharedElementWithMovableContentContent(profileId)
+ } else {
+ SharedElementContent(profileId)
+ }
+ }
+ }
+ }
+ }
+
+
+ @OptIn(ExperimentalSharedTransitionApi::class)
+ @Composable
+ private fun SharedElementContent(
+ profileId: Int,
+ modifier: Modifier = Modifier
+ ) {
+ Row(
+ modifier = modifier
+ .clickable {
+ onProfileClick(profileId)
+ }
+ ) {
+ val profile = Profile.allProfiles[profileId]
+
+ ProfileImage(
+ Profile.allProfiles[profileId].drawableRes, modifier = Modifier
+ .requiredSize(64.dp)
+ .sharedElement(key = "$profileId image")
+ .clip(CircleShape)
+ )
+ Text(
+ text = "${profile.name}, ${profile.age}",
+ color = LocalContentColor.current,
+ fontSize = 32.sp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .sharedElement(key = "$profileId text")
+ .padding(8.dp)
+ )
+ }
+ }
+
+ @OptIn(ExperimentalSharedTransitionApi::class)
+ @Composable
+ private fun SharedElementWithMovableContentContent(
+ profileId: Int,
+ modifier: Modifier = Modifier
+ ) {
+ Row(
+ modifier = modifier
+ .clickable {
+ onProfileClick(profileId)
+ }
+ ) {
+ val profile = Profile.allProfiles[profileId]
+ Box(
+ modifier = Modifier
+ .requiredSize(64.dp)
+ .sharedElement(key = "$profileId image")
+ .clip(CircleShape)
+ ) {
+ ProfileImageWithCounterMovableContent(profileId)
+ }
+ Text(
+ text = "${profile.name}, ${profile.age}",
+ color = LocalContentColor.current,
+ fontSize = 32.sp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .sharedElement(key = "$profileId text")
+ .padding(8.dp)
+ )
+ }
+ }
+}
diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/SharedElementExampleNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/SharedElementExampleNode.kt
new file mode 100644
index 000000000..d9e750179
--- /dev/null
+++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/SharedElementExampleNode.kt
@@ -0,0 +1,120 @@
+package com.bumble.appyx.sandbox.client.sharedelement
+
+import android.annotation.SuppressLint
+import android.os.Parcelable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.bumble.appyx.core.composable.Children
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.navigation.transition.localMovableContentWithTargetVisibility
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.node.ParentNode
+import com.bumble.appyx.navmodel.backstack.BackStack
+import com.bumble.appyx.navmodel.backstack.operation.push
+import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader
+import com.bumble.appyx.samples.common.profile.Profile.Companion.allProfiles
+import com.bumble.appyx.samples.common.profile.ProfileImage
+import kotlinx.coroutines.delay
+import kotlinx.parcelize.Parcelize
+import kotlin.random.Random
+
+class SharedElementExampleNode(
+ buildContext: BuildContext,
+ private val hasMovableContent : Boolean = false,
+ private val backStack: BackStack = BackStack(
+ savedStateMap = buildContext.savedStateMap,
+ initialElement = NavTarget.HorizontalList(0)
+ )
+) : ParentNode(
+ navModel = backStack,
+ buildContext = buildContext,
+) {
+
+ sealed class NavTarget : Parcelable {
+ @Parcelize
+ data class FullScreen(val profileId: Int) : NavTarget()
+
+ @Parcelize
+ data class HorizontalList(val profileId: Int = 0) : NavTarget()
+
+ @Parcelize
+ data class VerticalList(val profileId: Int = 0) : NavTarget()
+ }
+
+ override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
+ return when (navTarget) {
+ is NavTarget.HorizontalList -> ProfileHorizontalListNode(
+ onProfileClick = { id ->
+ backStack.push(NavTarget.FullScreen(id))
+ },
+ selectedId = navTarget.profileId,
+ buildContext = buildContext,
+ hasMovableContent = hasMovableContent
+ )
+
+ is NavTarget.VerticalList -> ProfileVerticalListNode(
+ onProfileClick = { id ->
+ backStack.push(NavTarget.HorizontalList(id))
+ },
+ profileId = navTarget.profileId,
+ buildContext = buildContext,
+ hasMovableContent = hasMovableContent
+ )
+
+ is NavTarget.FullScreen -> FullScreenNode(
+ onClick = { id ->
+ backStack.push(NavTarget.VerticalList(id))
+ },
+ profileId = navTarget.profileId,
+ buildContext = buildContext,
+ hasMovableContent = hasMovableContent
+ )
+ }
+ }
+
+ @SuppressLint("UnusedContentLambdaTargetStateParameter")
+ @Composable
+ override fun View(modifier: Modifier) {
+ Children(
+ navModel = backStack,
+ withSharedElementTransition = true,
+ withMovableContent = hasMovableContent,
+ transitionHandler = rememberBackstackFader(transitionSpec = { tween(300) })
+ )
+ }
+}
+
+@Composable
+fun ProfileImageWithCounterMovableContent(pageId: Int, modifier: Modifier = Modifier) {
+ localMovableContentWithTargetVisibility(key = pageId) {
+ var counter by remember(pageId) { mutableIntStateOf(Random.nextInt(0, 100)) }
+
+ LaunchedEffect(Unit) {
+ while (true) {
+ delay(1000)
+ counter++
+ }
+ }
+ Box(modifier = modifier) {
+ ProfileImage(
+ allProfiles[pageId].drawableRes, modifier = Modifier
+ )
+ Text(
+ text = "$counter",
+ modifier = Modifier.align(Alignment.Center),
+ color = Color.White
+ )
+
+ }
+ }?.invoke()
+}
diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/with_pager/ShareElementDetailsNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/with_pager/ShareElementDetailsNode.kt
new file mode 100644
index 000000000..99d6811b3
--- /dev/null
+++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/with_pager/ShareElementDetailsNode.kt
@@ -0,0 +1,4 @@
+package com.bumble.appyx.sandbox.client.sharedelement.with_pager
+
+class ShareElementDetailsNode {
+}
\ No newline at end of file
diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/with_pager/SharedElementPagerNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/with_pager/SharedElementPagerNode.kt
new file mode 100644
index 000000000..883125fdd
--- /dev/null
+++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/with_pager/SharedElementPagerNode.kt
@@ -0,0 +1,4 @@
+package com.bumble.appyx.sandbox.client.sharedelement.with_pager
+
+class SharedElementPagerNode {
+}
\ No newline at end of file
diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/with_pager/SharedElementPagerParentNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/with_pager/SharedElementPagerParentNode.kt
new file mode 100644
index 000000000..da4699c89
--- /dev/null
+++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/with_pager/SharedElementPagerParentNode.kt
@@ -0,0 +1,146 @@
+package com.bumble.appyx.sandbox.client.sharedelement.with_pager
+
+import android.os.Parcelable
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PageSize
+import androidx.compose.foundation.pager.PagerDefaults
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.bumble.appyx.core.composable.Children
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.navigation.transition.sharedElement
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.node.ParentNode
+import com.bumble.appyx.core.node.node
+import com.bumble.appyx.navmodel.backstack.BackStack
+import com.bumble.appyx.navmodel.backstack.operation.push
+import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader
+import com.bumble.appyx.samples.common.profile.Profile
+import com.bumble.appyx.samples.common.profile.ProfileImage
+import kotlinx.parcelize.Parcelize
+
+class SharedElementPagerParentNode(
+ buildContext: BuildContext,
+ private val backStack: BackStack = BackStack(
+ savedStateMap = buildContext.savedStateMap,
+ initialElement = NavTarget.Pager
+ )
+) : ParentNode(
+ navModel = backStack,
+ buildContext = buildContext
+) {
+
+ sealed class NavTarget : Parcelable {
+ @Parcelize
+ data object Pager : NavTarget()
+
+ @Parcelize
+ data class Detail(val index: Int) : NavTarget()
+ }
+
+ @OptIn(ExperimentalSharedTransitionApi::class)
+ override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
+ return when (navTarget) {
+ is NavTarget.Pager -> node(buildContext) {
+ Box {
+ val pagerState = rememberPagerState { Profile.allProfiles.size }
+
+ HorizontalPager(
+ state = pagerState,
+ pageSize = PageSize.Fill,
+ contentPadding = PaddingValues(horizontal = 10.dp),
+ flingBehavior = PagerDefaults.flingBehavior(
+ state = pagerState,
+ snapAnimationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy)
+ )
+ ) { pageIndex ->
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ backStack.push(NavTarget.Detail(pageIndex))
+ }
+ ) {
+ val profile = Profile.allProfiles[pageIndex]
+
+ ProfileImage(
+ profile.drawableRes, modifier = Modifier
+ .fillMaxWidth()
+ .height(200.dp)
+ .sharedElement(key = "$pageIndex image")
+ )
+
+ Text(
+ text = "${profile.name}, ${profile.age}",
+ color = Color.Black,
+ fontSize = 30.sp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .sharedElement(key = "$pageIndex text")
+ .padding(16.dp)
+ )
+ }
+
+ }
+
+ }
+ }
+
+ is NavTarget.Detail -> node(buildContext) {
+ val pageIndex = navTarget.index
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ val profile = Profile.allProfiles[pageIndex]
+
+ ProfileImage(
+ profile.drawableRes, modifier = Modifier
+ .fillMaxSize()
+ .sharedElement(key = "$pageIndex image")
+ )
+
+ Text(
+ text = "${profile.name}, ${profile.age}",
+ color = Color.White,
+ fontSize = 30.sp,
+ modifier = Modifier
+ .fillMaxWidth()
+ .sharedElement(key = "$pageIndex text")
+ .align(Alignment.BottomStart)
+ .padding(16.dp)
+ )
+ }
+
+ }
+ }
+ }
+
+ @Composable
+ override fun View(modifier: Modifier) {
+ Children(
+ navModel = backStack,
+ withSharedElementTransition = true,
+ transitionHandler = rememberBackstackFader(transitionSpec = { tween(300) })
+ )
+ }
+}
\ No newline at end of file