Skip to content

Commit 997f4fa

Browse files
Merge pull request #720 from KovalevAndrey/shared-element-prototype-copy
Shared element prototype
2 parents cc59b35 + 2b092b8 commit 997f4fa

File tree

28 files changed

+1185
-45
lines changed

28 files changed

+1185
-45
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Pending changes
44

55
- [#719](https://github.com/bumble-tech/appyx/pull/719)**Updated**: Jetpack Compose to 1.7.6
6+
- [#720](https://github.com/bumble-tech/appyx/pull/720)**Added**: Shared element transition and movable content support
67

78
## 1.5.1
89

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
3535
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
3636
# compose versions are resolved by BOM
3737
compose-animation-core = { module = "androidx.compose.animation:animation-core" }
38+
compose-animation-android = { group = "androidx.compose.animation", name = "animation-android" }
3839
compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
3940
compose-foundation = { module = "androidx.compose.foundation:foundation" }
4041
compose-material = { module = "androidx.compose.material3:material3" }

libraries/core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ dependencies {
4646
api(project(":libraries:customisations"))
4747
api(libs.androidx.lifecycle.common)
4848
api(libs.compose.animation.core)
49+
api(libs.compose.animation.android)
4950
api(libs.compose.runtime)
5051
api(libs.androidx.appcompat)
5152
api(libs.kotlin.coroutines.android)

libraries/core/detekt-baseline.xml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
<?xml version='1.0' encoding='UTF-8'?>
1+
<?xml version="1.0" ?>
22
<SmellBaseline>
3-
<ManuallySuppressedIssues />
4-
<CurrentIssues />
3+
<ManuallySuppressedIssues></ManuallySuppressedIssues>
4+
<CurrentIssues>
5+
<ID>CompositionLocalAllowlist:LocalNode.kt$LocalMovableContentMap</ID>
6+
<ID>CompositionLocalAllowlist:LocalNode.kt$LocalNodeTargetVisibility</ID>
7+
<ID>CompositionLocalAllowlist:LocalNode.kt$LocalSharedElementScope</ID>
8+
</CurrentIssues>
59
</SmellBaseline>
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package com.bumble.appyx.core.node
2+
3+
import android.os.Parcelable
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.LaunchedEffect
6+
import androidx.compose.runtime.getValue
7+
import androidx.compose.runtime.mutableIntStateOf
8+
import androidx.compose.runtime.remember
9+
import androidx.compose.runtime.setValue
10+
import androidx.compose.ui.Modifier
11+
import com.bumble.appyx.core.AppyxTestScenario
12+
import com.bumble.appyx.core.composable.Children
13+
import com.bumble.appyx.core.modality.BuildContext
14+
import com.bumble.appyx.core.navigation.transition.localMovableContentWithTargetVisibility
15+
import com.bumble.appyx.core.node.BackStackMovableContentTest.NavTarget.NavTarget1
16+
import com.bumble.appyx.core.node.BackStackMovableContentTest.NavTarget.NavTarget2
17+
import com.bumble.appyx.navmodel.backstack.BackStack
18+
import com.bumble.appyx.navmodel.backstack.operation.pop
19+
import com.bumble.appyx.navmodel.backstack.operation.push
20+
import kotlinx.coroutines.delay
21+
import kotlinx.parcelize.Parcelize
22+
import org.junit.Assert.assertEquals
23+
import org.junit.Rule
24+
import org.junit.Test
25+
import kotlin.random.Random
26+
27+
class BackStackMovableContentTest {
28+
29+
private var currentCounter: Int = Int.MIN_VALUE
30+
31+
private val backStack = BackStack<NavTarget>(
32+
savedStateMap = null,
33+
initialElement = NavTarget1
34+
)
35+
36+
var nodeFactory: (buildContext: BuildContext) -> TestParentNode = {
37+
TestParentNode(
38+
buildContext = it,
39+
backStack = backStack
40+
)
41+
}
42+
43+
@get:Rule
44+
val rule = AppyxTestScenario { buildContext ->
45+
nodeFactory(buildContext)
46+
}
47+
48+
@Test
49+
fun `GIVEN_backStack_with_movable_content_WHEN_push_THEN_persits_movable_content_state`() {
50+
rule.start()
51+
52+
val counterOne = currentCounter
53+
backStack.push(NavTarget2)
54+
rule.mainClock.advanceTimeBy(COUNTER_DELAY)
55+
56+
assertEquals(counterOne + 1, currentCounter)
57+
58+
val counterTwo = currentCounter
59+
backStack.pop()
60+
rule.mainClock.advanceTimeBy(COUNTER_DELAY)
61+
62+
assertEquals(counterTwo + 1, currentCounter)
63+
}
64+
65+
@Parcelize
66+
sealed class NavTarget : Parcelable {
67+
68+
data object NavTarget1 : NavTarget()
69+
70+
data object NavTarget2 : NavTarget()
71+
}
72+
73+
inner class TestParentNode(
74+
buildContext: BuildContext,
75+
val backStack: BackStack<NavTarget>
76+
) : ParentNode<NavTarget>(
77+
buildContext = buildContext,
78+
navModel = backStack
79+
) {
80+
81+
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node =
82+
when (navTarget) {
83+
NavTarget1 -> node(buildContext) {
84+
MovableContent()
85+
}
86+
87+
NavTarget2 -> node(buildContext) {
88+
MovableContent()
89+
}
90+
}
91+
92+
@Composable
93+
override fun View(modifier: Modifier) {
94+
Children(
95+
navModel = navModel,
96+
withSharedElementTransition = true,
97+
withMovableContent = true
98+
)
99+
}
100+
101+
@Composable
102+
fun MovableContent() {
103+
localMovableContentWithTargetVisibility(key = "key") {
104+
var counter by remember {
105+
mutableIntStateOf(Random.nextInt(0, 10000).apply {
106+
currentCounter = this
107+
})
108+
}
109+
110+
LaunchedEffect(Unit) {
111+
while (true) {
112+
delay(COUNTER_DELAY)
113+
counter++
114+
currentCounter = counter
115+
}
116+
}
117+
}?.invoke()
118+
}
119+
}
120+
}
121+
122+
private val COUNTER_DELAY = 1000L
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.bumble.appyx.core.node
2+
3+
import android.os.Parcelable
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.ui.Modifier
6+
import com.bumble.appyx.core.AppyxTestScenario
7+
import com.bumble.appyx.core.composable.Children
8+
import com.bumble.appyx.core.modality.BuildContext
9+
import com.bumble.appyx.navmodel.backstack.BackStack
10+
import com.bumble.appyx.navmodel.backstack.operation.pop
11+
import com.bumble.appyx.navmodel.backstack.operation.push
12+
import kotlinx.parcelize.Parcelize
13+
import org.junit.Assert.assertFalse
14+
import org.junit.Assert.assertTrue
15+
import org.junit.Rule
16+
import org.junit.Test
17+
18+
class BackStackTargetVisibilityTest {
19+
20+
private val backStack = BackStack<NavTarget>(
21+
savedStateMap = null,
22+
initialElement = NavTarget.NavTarget1
23+
)
24+
25+
var nodeOneTargetVisibilityState: Boolean = false
26+
var nodeTwoTargetVisibilityState: Boolean = false
27+
28+
var nodeFactory: (buildContext: BuildContext) -> TestParentNode = {
29+
TestParentNode(buildContext = it, backStack = backStack)
30+
}
31+
32+
@get:Rule
33+
val rule = AppyxTestScenario { buildContext ->
34+
nodeFactory(buildContext)
35+
}
36+
37+
@Test
38+
fun `GIVEN_backStack_WHEN_operations_called_THEN_child_nodes_have_correct_targetVisibility_state`() {
39+
rule.start()
40+
assertTrue(nodeOneTargetVisibilityState)
41+
42+
backStack.push(NavTarget.NavTarget2)
43+
rule.waitForIdle()
44+
45+
assertFalse(nodeOneTargetVisibilityState)
46+
assertTrue(nodeTwoTargetVisibilityState)
47+
48+
backStack.pop()
49+
rule.waitForIdle()
50+
51+
assertFalse(nodeTwoTargetVisibilityState)
52+
assertTrue(nodeOneTargetVisibilityState)
53+
}
54+
55+
56+
@Parcelize
57+
sealed class NavTarget : Parcelable {
58+
59+
data object NavTarget1 : NavTarget()
60+
61+
data object NavTarget2 : NavTarget()
62+
}
63+
64+
inner class TestParentNode(
65+
buildContext: BuildContext,
66+
val backStack: BackStack<NavTarget>,
67+
) : ParentNode<NavTarget>(
68+
buildContext = buildContext,
69+
navModel = backStack
70+
) {
71+
72+
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node =
73+
when (navTarget) {
74+
NavTarget.NavTarget1 -> node(buildContext) {
75+
nodeOneTargetVisibilityState = LocalNodeTargetVisibility.current
76+
}
77+
78+
NavTarget.NavTarget2 -> node(buildContext) {
79+
nodeTwoTargetVisibilityState = LocalNodeTargetVisibility.current
80+
}
81+
}
82+
83+
@Composable
84+
override fun View(modifier: Modifier) {
85+
Children(navModel)
86+
}
87+
}
88+
89+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.bumble.appyx.core.node
2+
3+
import android.os.Parcelable
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.ui.Modifier
6+
import com.bumble.appyx.core.AppyxTestScenario
7+
import com.bumble.appyx.core.composable.Children
8+
import com.bumble.appyx.core.modality.BuildContext
9+
import com.bumble.appyx.navmodel.spotlight.Spotlight
10+
import com.bumble.appyx.navmodel.spotlight.operation.activate
11+
import kotlinx.parcelize.Parcelize
12+
import org.junit.Assert.assertFalse
13+
import org.junit.Assert.assertTrue
14+
import org.junit.Rule
15+
import org.junit.Test
16+
17+
class SpotlightTargetVisibilityTest {
18+
19+
private lateinit var spotlight: Spotlight<NavTarget>
20+
21+
var nodeOneTargetVisibilityState: Boolean = false
22+
var nodeTwoTargetVisibilityState: Boolean = false
23+
var nodeThreeTargetVisibilityState: Boolean = false
24+
25+
var nodeFactory: (buildContext: BuildContext) -> TestParentNode = {
26+
TestParentNode(buildContext = it, spotlight = spotlight)
27+
}
28+
29+
@get:Rule
30+
val rule = AppyxTestScenario { buildContext ->
31+
nodeFactory(buildContext)
32+
}
33+
34+
@Test
35+
fun `GIVEN_spotlight_WHEN_operations_called_THEN_child_nodes_have_correct_targetVisibility_state`() {
36+
val initialActiveIndex = 2
37+
createSpotlight(initialActiveIndex)
38+
rule.start()
39+
40+
assertTrue(nodeThreeTargetVisibilityState)
41+
42+
spotlight.activate(1)
43+
rule.waitForIdle()
44+
45+
assertFalse(nodeOneTargetVisibilityState)
46+
assertTrue(nodeTwoTargetVisibilityState)
47+
assertFalse(nodeThreeTargetVisibilityState)
48+
49+
spotlight.activate(0)
50+
rule.waitForIdle()
51+
52+
assertTrue(nodeOneTargetVisibilityState)
53+
assertFalse(nodeTwoTargetVisibilityState)
54+
assertFalse(nodeThreeTargetVisibilityState)
55+
}
56+
57+
58+
private fun createSpotlight(initialActiveIndex: Int) {
59+
spotlight = Spotlight(
60+
savedStateMap = null,
61+
items = listOf(NavTarget.NavTarget1, NavTarget.NavTarget2, NavTarget.NavTarget3),
62+
initialActiveIndex = initialActiveIndex
63+
)
64+
}
65+
66+
@Parcelize
67+
sealed class NavTarget : Parcelable {
68+
69+
data object NavTarget1 : NavTarget()
70+
71+
data object NavTarget2 : NavTarget()
72+
73+
data object NavTarget3 : NavTarget()
74+
}
75+
76+
inner class TestParentNode(
77+
buildContext: BuildContext,
78+
val spotlight: Spotlight<NavTarget>,
79+
) : ParentNode<NavTarget>(
80+
buildContext = buildContext,
81+
navModel = spotlight
82+
) {
83+
84+
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node =
85+
when (navTarget) {
86+
NavTarget.NavTarget1 -> node(buildContext) {
87+
nodeOneTargetVisibilityState = LocalNodeTargetVisibility.current
88+
}
89+
90+
NavTarget.NavTarget2 -> node(buildContext) {
91+
nodeTwoTargetVisibilityState = LocalNodeTargetVisibility.current
92+
}
93+
NavTarget.NavTarget3 -> node(buildContext) {
94+
nodeThreeTargetVisibilityState = LocalNodeTargetVisibility.current
95+
}
96+
}
97+
98+
@Composable
99+
override fun View(modifier: Modifier) {
100+
Children(navModel)
101+
}
102+
}
103+
104+
}

0 commit comments

Comments
 (0)