Skip to content

Commit

Permalink
Merge pull request #701 from KovalevAndrey/1.x-shared-element-prototype
Browse files Browse the repository at this point in the history
1.x shared element transition and movable content integration
  • Loading branch information
KovalevAndrey authored May 8, 2024
2 parents 4f37347 + 9909b89 commit 98fc34b
Show file tree
Hide file tree
Showing 30 changed files with 1,205 additions and 50 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
instrumentation-tests:
name: Instrumentation tests
runs-on: macOS-latest
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -119,6 +119,8 @@ jobs:
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: |
adb uninstall "com.bumble.appyx.core.test"
adb uninstall "com.bumble.appyx.interop.ribs.test"
adb logcat > logcat.out &
./gradlew connectedCheck
- name: Upload failed instrumentation artifacts
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Pending changes

- [#701](https://github.com/bumble-tech/appyx/pull/701)**Added**: Shared element transition and movable content support

---

## 1.5.0
Expand Down
138 changes: 137 additions & 1 deletion documentation/ui/transitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,145 @@ Below you can find the different options how to visualise `NavModel` state chang

## No transitions

Using the provided [Child-related composables](children-view.md) you'll see no transitions as a default – UI changes resulting from the NavModel's state update will be rendered instantly.
Using the provided [Child-related composables](children-view.md) you'll see no transitions as a default – UI changes resulting from the NavModel's state update will be rendered instantly.


## Shared element transitions

To support shared element transition between two Child Nodes you need:

1. Use the `sharedElement` Modifier with the same key on the composable you want to connect.
2. On the `Children` composable, set `withSharedElementTransition` to true and use either fader or
no transition handler at all. Using a slider will make the shared element slide away with the
rest of of the content.
3. When operation is performed on the NavModel, the shared element will be animated between the two
Child Nodes. For instance, in the example below backStack currently has NavTarget.Child1 as the
active element. Performing a push operation with NavTarget.Child2 will animate the shared element
between NodeOne and NodeTwo. Popping back to NavTarget.Child1 will animate the shared element back.

```kotlin
class NodeOne(
buildContext: BuildContext
) : Node(
buildContext = buildContext
) {

@Composable
override fun View(modifier: Modifier) {
Box(
modifier = Modifier
// make sure you specify the size before using sharedElement modifier
.fillMaxSize()
.sharedElement(key = "sharedContainer")
) { /** ... */ }
}
}

class NodeTwo(
buildContext: BuildContext
) : Node(
buildContext = buildContext
) {

@Composable
override fun View(modifier: Modifier) {
Box(
modifier = Modifier
// make sure you specify the size before using sharedElement modifier
.requiredSize(64.dp)
.sharedElement(key = "sharedContainer")
) { /** ... */ }
}
}

class ParentNode(
buildContext: BuildContext,
backStack: BackStack<NavTarget> = BackStack(
initialElement = NavTarget.Child1,
savedStateMap = buildContext.savedStateMap
)
) : ParentNode<NavTarget>(
buildContext = buildContext,
navModel = backStack,
) {

override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node =
when (navTarget) {
NavTarget.Child1 -> NodeOne(buildContext)
NavTarget.Child2 -> NodeTwo(buildContext)
}

@Composable
override fun View(modifier: Modifier) {
Children(
// or any other NavModel
navModel = backStack,
// or no transitionHandler at all. Using a slider will make the shared element slide away
// with the rest of of the content.
transitionHandler = rememberBackStackFader(),
withSharedElementTransition = true
)
}
}

```

## Transitions with movable content

You can move composable content between two Child Nodes without losing its state. You can only move
content from a Node that is currently visible and transitioning to invisible state to a Node that
is currently invisible and transitioning to visible state as movable content is intended to be
composed once design and is moved from one part of the composition to another.

To move content between two Child Nodes you need to use `localMovableContentWithTargetVisibility`
composable function with the correct key to retrieve existing content if it exists or put content
for this key if it doesn't exist. In addition to that, on Parent's `Children` composable you need to
set `withMovableContent` to true.

In the example below when a NodeOne is being replaced with NodeTwo in a BackStack or Spotlight NavModel
`CustomMovableContent("movableContentKey")` will be moved from NodeOne to NodeTwo without losing its
state.


```kotlin
@Composable
fun CustomMovableContent(key: Any, modifier: Modifier = Modifier) {
localMovableContentWithTargetVisibility(key = key) {
// implement movable content here
var counter by remember(pageId) { mutableIntStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
counter++
}
}
Text(text = "$counter")
}?.invoke()
}

// NodeOne
@Composable
override fun View(modifier: Modifier) {
CustomMovableContent("movableContentKey")
}

// NodeTwo
@Composable
override fun View(modifier: Modifier) {
CustomMovableContent("movableContentKey")
}

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

```

## Jetpack Compose default animations

You can use [standard Compose animations](https://developer.android.com/jetpack/compose/animation) for embedded child `Nodes` in the view, e.g. `AnimatedVisibility`:
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ org.gradle.parallel=true
android.useAndroidX=true
kotlin.code.style=official
library.version=1.5.0
android.experimental.lint.version=8.3.0
android.experimental.lint.version=8.4.0-rc02
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ accompanist = "0.28.0"
androidx-lifecycle = "2.6.1"
androidx-navigation-compose = "2.5.1"
coil = "2.2.1"
composeBom = "2024.04.00"
composeBom = "2024.05.00"
composeCompiler = "1.5.11"
ribs = "0.39.0"
mvicore = "1.2.6"
Expand Down
2 changes: 1 addition & 1 deletion libraries/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ dependencies {
val composeBom = platform(libs.compose.bom)

api(composeBom)
api("androidx.compose.animation:animation:1.7.0-alpha08")
api(project(":libraries:customisations"))
api(libs.androidx.lifecycle.common)
api(libs.compose.animation.core)
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,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)
}
}

}
Loading

0 comments on commit 98fc34b

Please sign in to comment.