diff --git a/libraries/core/src/androidTest/kotlin/com/bumble/appyx/core/node/BackStackTargetVisibilityTest.kt b/libraries/core/src/androidTest/kotlin/com/bumble/appyx/core/node/BackStackTargetVisibilityTest.kt new file mode 100644 index 000000000..626175e00 --- /dev/null +++ b/libraries/core/src/androidTest/kotlin/com/bumble/appyx/core/node/BackStackTargetVisibilityTest.kt @@ -0,0 +1,89 @@ +package com.bumble.appyx.core.node + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.AppyxTestScenario +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.pop +import com.bumble.appyx.navmodel.backstack.operation.push +import kotlinx.parcelize.Parcelize +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +class BackStackTargetVisibilityTest { + + private val backStack = BackStack( + savedStateMap = null, + initialElement = NavTarget.NavTarget1 + ) + + var nodeOneTargetVisibilityState: Boolean = false + var nodeTwoTargetVisibilityState: Boolean = false + + var nodeFactory: (buildContext: BuildContext) -> TestParentNode = { + TestParentNode(buildContext = it, backStack = backStack) + } + + @get:Rule + val rule = AppyxTestScenario { buildContext -> + nodeFactory(buildContext) + } + + @Test + fun `GIVEN_backStack_WHEN_operations_called_THEN_child_nodes_have_correct_targetVisibility_state`() { + rule.start() + assertTrue(nodeOneTargetVisibilityState) + + backStack.push(NavTarget.NavTarget2) + rule.waitForIdle() + + assertFalse(nodeOneTargetVisibilityState) + assertTrue(nodeTwoTargetVisibilityState) + + backStack.pop() + rule.waitForIdle() + + assertFalse(nodeTwoTargetVisibilityState) + assertTrue(nodeOneTargetVisibilityState) + } + + + @Parcelize + sealed class NavTarget : Parcelable { + + data object NavTarget1 : NavTarget() + + data object NavTarget2 : NavTarget() + } + + inner class TestParentNode( + buildContext: BuildContext, + val backStack: BackStack, + ) : ParentNode( + buildContext = buildContext, + navModel = backStack + ) { + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node = + when (navTarget) { + NavTarget.NavTarget1 -> node(buildContext) { + nodeOneTargetVisibilityState = LocalNodeTargetVisibility.current + } + + NavTarget.NavTarget2 -> node(buildContext) { + nodeTwoTargetVisibilityState = LocalNodeTargetVisibility.current + } + } + + @Composable + override fun View(modifier: Modifier) { + Children(navModel) + } + } + +} diff --git a/libraries/core/src/androidTest/kotlin/com/bumble/appyx/core/node/SpotlightTargetVisibilityTest.kt b/libraries/core/src/androidTest/kotlin/com/bumble/appyx/core/node/SpotlightTargetVisibilityTest.kt new file mode 100644 index 000000000..ac71699ed --- /dev/null +++ b/libraries/core/src/androidTest/kotlin/com/bumble/appyx/core/node/SpotlightTargetVisibilityTest.kt @@ -0,0 +1,104 @@ +package com.bumble.appyx.core.node + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.AppyxTestScenario +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.navmodel.spotlight.Spotlight +import com.bumble.appyx.navmodel.spotlight.operation.activate +import kotlinx.parcelize.Parcelize +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test + +class SpotlightTargetVisibilityTest { + + private lateinit var spotlight: Spotlight + + var nodeOneTargetVisibilityState: Boolean = false + var nodeTwoTargetVisibilityState: Boolean = false + var nodeThreeTargetVisibilityState: Boolean = false + + var nodeFactory: (buildContext: BuildContext) -> TestParentNode = { + TestParentNode(buildContext = it, spotlight = spotlight) + } + + @get:Rule + val rule = AppyxTestScenario { buildContext -> + nodeFactory(buildContext) + } + + @Test + fun `GIVEN_spotlight_WHEN_operations_called_THEN_child_nodes_have_correct_targetVisibility_state`() { + val initialActiveIndex = 2 + createSpotlight(initialActiveIndex) + rule.start() + + assertTrue(nodeThreeTargetVisibilityState) + + spotlight.activate(1) + rule.waitForIdle() + + assertFalse(nodeOneTargetVisibilityState) + assertTrue(nodeTwoTargetVisibilityState) + assertFalse(nodeThreeTargetVisibilityState) + + spotlight.activate(0) + rule.waitForIdle() + + assertTrue(nodeOneTargetVisibilityState) + assertFalse(nodeTwoTargetVisibilityState) + assertFalse(nodeThreeTargetVisibilityState) + } + + + private fun createSpotlight(initialActiveIndex: Int) { + spotlight = Spotlight( + savedStateMap = null, + items = listOf(NavTarget.NavTarget1, NavTarget.NavTarget2, NavTarget.NavTarget3), + initialActiveIndex = initialActiveIndex + ) + } + + @Parcelize + sealed class NavTarget : Parcelable { + + data object NavTarget1 : NavTarget() + + data object NavTarget2 : NavTarget() + + data object NavTarget3 : NavTarget() + } + + inner class TestParentNode( + buildContext: BuildContext, + val spotlight: Spotlight, + ) : ParentNode( + buildContext = buildContext, + navModel = spotlight + ) { + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node = + when (navTarget) { + NavTarget.NavTarget1 -> node(buildContext) { + nodeOneTargetVisibilityState = LocalNodeTargetVisibility.current + } + + NavTarget.NavTarget2 -> node(buildContext) { + nodeTwoTargetVisibilityState = LocalNodeTargetVisibility.current + } + NavTarget.NavTarget3 -> node(buildContext) { + nodeThreeTargetVisibilityState = LocalNodeTargetVisibility.current + } + } + + @Composable + override fun View(modifier: Modifier) { + Children(navModel) + } + } + +} diff --git a/libraries/core/src/main/kotlin/com/bumble/appyx/core/composable/Children.kt b/libraries/core/src/main/kotlin/com/bumble/appyx/core/composable/Children.kt index 4f920b87f..f772f8aa3 100644 --- a/libraries/core/src/main/kotlin/com/bumble/appyx/core/composable/Children.kt +++ b/libraries/core/src/main/kotlin/com/bumble/appyx/core/composable/Children.kt @@ -65,6 +65,7 @@ inline fun ParentNode.Children( } ) { CompositionLocalProvider( + /** LocalSharedElementScope will be consumed by children UI to apply shareElement modifier */ LocalSharedElementScope provides this ) { block( @@ -83,6 +84,8 @@ inline fun ParentNode.Children( } ) { CompositionLocalProvider( + /** If sharedElement is not supported for this Node - provide null otherwise children + * can consume ascendant's LocalSharedElementScope */ LocalSharedElementScope provides null ) { block( @@ -190,7 +193,7 @@ class ChildrenTransitionScope( key(navElement.key.id) { CompositionLocalProvider( LocalNodeTargetVisibility provides - children.targetStateVisible.contains(navElement) + children.onScreenWithVisibleTargetState.contains(navElement) ) { Child( navElement, diff --git a/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/BaseNavModel.kt b/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/BaseNavModel.kt index 90d503757..e14d3b460 100644 --- a/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/BaseNavModel.kt +++ b/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/BaseNavModel.kt @@ -69,7 +69,7 @@ abstract class BaseNavModel( state .mapState(scope) { elements -> NavModelAdapter.ScreenState( - targetStateVisible = elements.filter { screenResolver.isOnScreen(it.targetState) }, + onScreenWithVisibleTargetState = elements.filter { screenResolver.isOnScreen(it.targetState) }, onScreen = elements.filter { screenResolver.isOnScreen(it) }, offScreen = elements.filterNot { screenResolver.isOnScreen(it) }, ) diff --git a/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/NavModelAdapter.kt b/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/NavModelAdapter.kt index b95ebeb9d..49f0d3498 100644 --- a/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/NavModelAdapter.kt +++ b/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/NavModelAdapter.kt @@ -9,7 +9,11 @@ interface NavModelAdapter { val screenState: StateFlow> data class ScreenState( - val targetStateVisible: NavElements = emptyList(), + /** onScreenWithVisibleTargetState represents the list of NavElements that have a target state + * as visible. For instance if the NavModel is a BackStack it will represent the element that + * is transitioning to ACTIVE state. + */ + val onScreenWithVisibleTargetState: NavElements = emptyList(), val onScreen: NavElements = emptyList(), val offScreen: NavElements = 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..fa7ee35aa --- /dev/null +++ b/libraries/core/src/main/kotlin/com/bumble/appyx/core/navigation/transition/MovableContent.kt @@ -0,0 +1,36 @@ +package com.bumble.appyx.core.navigation.transition + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.movableContentOf +import com.bumble.appyx.core.node.LocalNodeTargetVisibility +import com.bumble.appyx.core.node.LocalParentNodeMovableContent + + +/** + * 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 if 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, + defaultValue: @Composable () -> Unit +): (@Composable () -> Unit)? { + if (!LocalNodeTargetVisibility.current) return null + val movableContentMap = LocalParentNodeMovableContent.current + return movableContentMap.getOrPut(key) { + movableContentOf { + defaultValue() + } + } +} + 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 index e5a460ed5..f5eb21c8d 100644 --- 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 @@ -3,6 +3,7 @@ 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 @@ -19,7 +20,7 @@ import com.bumble.appyx.core.node.LocalSharedElementScope fun Modifier.sharedElement( key: Any, boundsTransform: BoundsTransform = DefaultBoundsTransform, - placeHolderSize: SharedTransitionScope.PlaceHolderSize = SharedTransitionScope.PlaceHolderSize.contentSize, + placeHolderSize: PlaceHolderSize = PlaceHolderSize.contentSize, renderInOverlayDuringTransition: Boolean = true, zIndexInOverlay: Float = 0f, clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip = ParentClip 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 caf890fd4..2d9c5cf8e 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 @@ -2,6 +2,7 @@ package com.bumble.appyx.core.node import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope +import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf val LocalNode = compositionLocalOf { null } @@ -16,3 +17,6 @@ val LocalSharedElementScope = compositionLocalOf { null * return false. */ val LocalNodeTargetVisibility = compositionLocalOf { false } + +val LocalParentNodeMovableContent = + compositionLocalOf Unit>> { mutableMapOf() } diff --git a/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/Node.kt b/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/Node.kt index 2ffcceef7..d9974b293 100644 --- a/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/Node.kt +++ b/libraries/core/src/main/kotlin/com/bumble/appyx/core/node/Node.kt @@ -123,11 +123,18 @@ open class Node @VisibleForTesting internal constructor( LocalNode provides this, LocalLifecycleOwner provides this, ) { - HandleBackPress() - View(modifier) + DerivedSetup { + HandleBackPress() + View(modifier) + } } } + @Composable + protected open fun DerivedSetup(innerContent: @Composable () -> Unit) { + innerContent() + } + @Composable private fun HandleBackPress() { // can't use BackHandler Composable because plugins provide OnBackPressedCallback which is not observable 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..61f33f571 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 @@ -2,6 +2,8 @@ package com.bumble.appyx.core.node import androidx.annotation.CallSuper import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle @@ -74,6 +76,15 @@ abstract class ParentNode( manageTransitions() } + @Composable + final override fun DerivedSetup(innerContent: @Composable () -> Unit) { + CompositionLocalProvider( + LocalParentNodeMovableContent provides mutableMapOf() + ) { + innerContent() + } + } + fun childOrCreate(navKey: NavKey): ChildEntry.Initialized = childNodeCreationManager.childOrCreate(navKey) @@ -219,5 +230,4 @@ abstract class ParentNode( } - } 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 84d858f17..2f825caa1 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 @@ -48,7 +48,7 @@ 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.SharedElementFaderNode +import com.bumble.appyx.sandbox.client.sharedelement.SharedElementWithMovableContentExampleNode import com.bumble.appyx.utils.customisations.NodeCustomisation import kotlinx.parcelize.Parcelize @@ -101,7 +101,7 @@ class ContainerNode internal constructor( when (navTarget) { is Picker -> node(buildContext) { modifier -> ExamplesList(modifier) } is NavModelExamples -> NavModelExamplesNode(buildContext) - is SharedElementFaderExample -> SharedElementFaderNode(buildContext) + is SharedElementFaderExample -> SharedElementWithMovableContentExampleNode(buildContext) is LazyExamples -> LazyListContainerNode(buildContext) is IntegrationPointExample -> IntegrationPointExampleNode(buildContext) is BlockerExample -> BlockerExampleNode(buildContext) 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 index 6890b1135..5506d4e37 100644 --- 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 @@ -17,7 +17,6 @@ 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, @@ -36,11 +35,15 @@ class FullScreenNode( } ) { val profile = Profile.allProfiles[profileId] - ProfileImage( - profile.drawableRes, modifier = Modifier + + + Box( + modifier = Modifier .fillMaxSize() .sharedElement(key = "$profileId image") - ) + ) { + ProfileImageWithCounterMovableContent(profileId) + } Text( text = "${profile.name}, ${profile.age}", 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 index 443e0f10a..ddca47c88 100644 --- 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 @@ -22,7 +22,6 @@ 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, @@ -41,28 +40,30 @@ class ProfileHorizontalListNode( horizontalArrangement = Arrangement.spacedBy(32.dp), verticalAlignment = Alignment.CenterVertically ) { - repeat(10) { pageId -> - item(key = pageId) { + repeat(10) { profileId -> + item(key = profileId) { Box( modifier = Modifier .requiredSize(200.dp) .clickable { - onProfileClick(pageId) + onProfileClick(profileId) } ) { - val profile = Profile.allProfiles[pageId] - ProfileImage( - profile.drawableRes, modifier = Modifier + val profile = Profile.allProfiles[profileId] + Box( + modifier = Modifier .fillMaxSize() - .sharedElement(key = "$pageId image") - ) + .sharedElement(key = "$profileId image") + ) { + ProfileImageWithCounterMovableContent(profileId) + } Text( text = "${profile.name}, ${profile.age}", color = Color.White, fontSize = 16.sp, modifier = Modifier .fillMaxWidth() - .sharedElement(key = "$pageId text") + .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 index 8b3d466a2..4e7b29d39 100644 --- 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 @@ -3,6 +3,7 @@ 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 @@ -23,7 +24,6 @@ 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, @@ -50,12 +50,14 @@ class ProfileVerticalListNode( } ) { val profile = Profile.allProfiles[pageId] - ProfileImage( - profile.drawableRes, modifier = Modifier + Box( + modifier = Modifier .requiredSize(64.dp) .sharedElement(key = "$pageId image") .clip(CircleShape) - ) + ) { + ProfileImageWithCounterMovableContent(pageId) + } Text( text = "${profile.name}, ${profile.age}", color = Color.Black, diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/SharedElementFaderNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/SharedElementWithMovableContentExampleNode.kt similarity index 61% rename from samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/SharedElementFaderNode.kt rename to samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/SharedElementWithMovableContentExampleNode.kt index d9e4c4bd5..3a6c19cee 100644 --- a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/SharedElementFaderNode.kt +++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/sharedelement/SharedElementWithMovableContentExampleNode.kt @@ -3,30 +3,42 @@ 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.operation.replace 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 SharedElementFaderNode( +class SharedElementWithMovableContentExampleNode( buildContext: BuildContext, private val backStack: BackStack = BackStack( savedStateMap = buildContext.savedStateMap, initialElement = NavTarget.HorizontalList(0) ) -) : ParentNode( +) : ParentNode( navModel = backStack, buildContext = buildContext, ) { - sealed class NavTarget : Parcelable { @Parcelize data class FullScreen(val profileId: Int) : NavTarget() @@ -50,7 +62,7 @@ class SharedElementFaderNode( is NavTarget.VerticalList -> ProfileVerticalListNode( onProfileClick = { id -> - backStack.replace(NavTarget.HorizontalList(id)) + backStack.push(NavTarget.HorizontalList(id)) }, profileId = navTarget.profileId, buildContext = buildContext @@ -76,3 +88,28 @@ class SharedElementFaderNode( ) } } + +@Composable +fun ProfileImageWithCounterMovableContent(pageId: Int) { + 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() +}