Skip to content

Commit

Permalink
Merge pull request #720 from KovalevAndrey/shared-element-prototype-copy
Browse files Browse the repository at this point in the history
Shared element prototype
  • Loading branch information
KovalevAndrey authored Jan 8, 2025
2 parents cc59b35 + 2b092b8 commit 997f4fa
Show file tree
Hide file tree
Showing 28 changed files with 1,185 additions and 45 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Pending changes

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

## 1.5.1

Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
# compose versions are resolved by BOM
compose-animation-core = { module = "androidx.compose.animation:animation-core" }
compose-animation-android = { group = "androidx.compose.animation", name = "animation-android" }
compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" }
compose-foundation = { module = "androidx.compose.foundation:foundation" }
compose-material = { module = "androidx.compose.material3:material3" }
Expand Down
1 change: 1 addition & 0 deletions libraries/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies {
api(project(":libraries:customisations"))
api(libs.androidx.lifecycle.common)
api(libs.compose.animation.core)
api(libs.compose.animation.android)
api(libs.compose.runtime)
api(libs.androidx.appcompat)
api(libs.kotlin.coroutines.android)
Expand Down
10 changes: 7 additions & 3 deletions libraries/core/detekt-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues />
<CurrentIssues />
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>CompositionLocalAllowlist:LocalNode.kt$LocalMovableContentMap</ID>
<ID>CompositionLocalAllowlist:LocalNode.kt$LocalNodeTargetVisibility</ID>
<ID>CompositionLocalAllowlist:LocalNode.kt$LocalSharedElementScope</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.bumble.appyx.core.node

import android.os.Parcelable
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.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.core.navigation.transition.localMovableContentWithTargetVisibility
import com.bumble.appyx.core.node.BackStackMovableContentTest.NavTarget.NavTarget1
import com.bumble.appyx.core.node.BackStackMovableContentTest.NavTarget.NavTarget2
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.coroutines.delay
import kotlinx.parcelize.Parcelize
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import kotlin.random.Random

class BackStackMovableContentTest {

private var currentCounter: Int = Int.MIN_VALUE

private val backStack = BackStack<NavTarget>(
savedStateMap = null,
initialElement = NavTarget1
)

var nodeFactory: (buildContext: BuildContext) -> TestParentNode = {
TestParentNode(
buildContext = it,
backStack = backStack
)
}

@get:Rule
val rule = AppyxTestScenario { buildContext ->
nodeFactory(buildContext)
}

@Test
fun `GIVEN_backStack_with_movable_content_WHEN_push_THEN_persits_movable_content_state`() {
rule.start()

val counterOne = currentCounter
backStack.push(NavTarget2)
rule.mainClock.advanceTimeBy(COUNTER_DELAY)

assertEquals(counterOne + 1, currentCounter)

val counterTwo = currentCounter
backStack.pop()
rule.mainClock.advanceTimeBy(COUNTER_DELAY)

assertEquals(counterTwo + 1, currentCounter)
}

@Parcelize
sealed class NavTarget : Parcelable {

data object NavTarget1 : NavTarget()

data object NavTarget2 : NavTarget()
}

inner class TestParentNode(
buildContext: BuildContext,
val backStack: BackStack<NavTarget>
) : ParentNode<NavTarget>(
buildContext = buildContext,
navModel = backStack
) {

override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node =
when (navTarget) {
NavTarget1 -> node(buildContext) {
MovableContent()
}

NavTarget2 -> node(buildContext) {
MovableContent()
}
}

@Composable
override fun View(modifier: Modifier) {
Children(
navModel = navModel,
withSharedElementTransition = true,
withMovableContent = true
)
}

@Composable
fun MovableContent() {
localMovableContentWithTargetVisibility(key = "key") {
var counter by remember {
mutableIntStateOf(Random.nextInt(0, 10000).apply {
currentCounter = this
})
}

LaunchedEffect(Unit) {
while (true) {
delay(COUNTER_DELAY)
counter++
currentCounter = counter
}
}
}?.invoke()
}
}
}

private val COUNTER_DELAY = 1000L
Original file line number Diff line number Diff line change
@@ -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<NavTarget>(
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<NavTarget>,
) : ParentNode<NavTarget>(
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)
}
}

}
Original file line number Diff line number Diff line change
@@ -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<NavTarget>

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<NavTarget>,
) : ParentNode<NavTarget>(
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)
}
}

}
Loading

0 comments on commit 997f4fa

Please sign in to comment.