From eea40cd589665462cf58a71d1e671ebf31b6f0c8 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 20 Dec 2022 18:43:13 +0400 Subject: [PATCH 1/5] Add Spotlight.current extension to ebserve current nav target --- .../kotlin/com/bumble/appyx/navmodel/spotlight/SpotlightExt.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/core/src/main/kotlin/com/bumble/appyx/navmodel/spotlight/SpotlightExt.kt b/libraries/core/src/main/kotlin/com/bumble/appyx/navmodel/spotlight/SpotlightExt.kt index 0c0fe3a86..ffaa66bc1 100644 --- a/libraries/core/src/main/kotlin/com/bumble/appyx/navmodel/spotlight/SpotlightExt.kt +++ b/libraries/core/src/main/kotlin/com/bumble/appyx/navmodel/spotlight/SpotlightExt.kt @@ -11,5 +11,8 @@ fun Spotlight.hasPrevious() = fun Spotlight.activeIndex() = elements.map { value -> value.currentIndex } +fun Spotlight.current() = + elements.map { value -> value.current?.key?.navTarget } + fun Spotlight.elementsCount() = elements.value.size From 05328ec7ba89328c5adfb51ceb3c226ab3bd2757 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 20 Dec 2022 18:44:03 +0400 Subject: [PATCH 2/5] Update SpotlightExampleNode to use material navigation bar for tabs and have FadeThrough transition --- .../client/spotlight/SpotlightExampleNode.kt | 168 ++++++++++-------- .../client/spotlight/SpotlightFaderThrough.kt | 89 ++++++++++ 2 files changed, 186 insertions(+), 71 deletions(-) create mode 100644 samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightExampleNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightExampleNode.kt index f19f248be..b9bdb4301 100644 --- a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightExampleNode.kt +++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightExampleNode.kt @@ -3,20 +3,33 @@ package com.bumble.appyx.sandbox.client.spotlight import android.os.Parcelable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column 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.material3.Button +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow import androidx.compose.ui.unit.dp import androidx.lifecycle.coroutineScope import com.bumble.appyx.core.composable.Children @@ -25,27 +38,27 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.navmodel.spotlight.Spotlight import com.bumble.appyx.navmodel.spotlight.backpresshandler.GoToPrevious +import com.bumble.appyx.navmodel.spotlight.current import com.bumble.appyx.navmodel.spotlight.elementsCount import com.bumble.appyx.navmodel.spotlight.hasNext import com.bumble.appyx.navmodel.spotlight.hasPrevious -import com.bumble.appyx.navmodel.spotlight.operation.next import com.bumble.appyx.navmodel.spotlight.operation.activate +import com.bumble.appyx.navmodel.spotlight.operation.next import com.bumble.appyx.navmodel.spotlight.operation.previous import com.bumble.appyx.navmodel.spotlight.operation.updateElements -import com.bumble.appyx.navmodel.spotlight.transitionhandler.rememberSpotlightSlider import com.bumble.appyx.sandbox.client.child.ChildNode -import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.Item.C1 -import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.Item.C2 -import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.Item.C3 import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.NavTarget.Child1 import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.NavTarget.Child2 import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.NavTarget.Child3 -import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.State.Loaded -import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.State.Loading +import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.ScreenState.Loaded +import com.bumble.appyx.sandbox.client.spotlight.SpotlightExampleNode.ScreenState.Loading import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize +/** + * Shows how to use spotlight to create a UI with BottomTabBar + */ class SpotlightExampleNode( buildContext: BuildContext, private val spotlight: Spotlight = Spotlight( @@ -58,11 +71,11 @@ class SpotlightExampleNode( navModel = spotlight ) { - private val screenState = mutableStateOf(null) + private val screenState = mutableStateOf(null) - sealed class State { - object Loading : State() - object Loaded : State() + sealed class ScreenState { + object Loading : ScreenState() + object Loaded : ScreenState() } init { @@ -70,7 +83,7 @@ class SpotlightExampleNode( if (spotlight.elementsCount() == 0) { screenState.value = Loading lifecycle.coroutineScope.launch { - delay(2000) + delay(1000) spotlight.updateElements(items = Item.getItemList()) screenState.value = Loaded } @@ -126,78 +139,91 @@ class SpotlightExampleNode( } } + @OptIn(ExperimentalMaterial3Api::class) @Suppress("LongMethod") @Composable private fun LoadedState(modifier: Modifier = Modifier) { val hasPrevious = spotlight.hasPrevious().collectAsState(initial = false) val hasNext = spotlight.hasNext().collectAsState(initial = false) - Column( - verticalArrangement = Arrangement.spacedBy(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - TextButton( - text = "Previous", - enabled = hasPrevious.value - ) { - spotlight.previous() - } - TextButton( - text = "Next", - enabled = hasNext.value - ) { - spotlight.next() - } + val currentTab = spotlight.current().collectAsState(initial = null) + Scaffold( + modifier = modifier.fillMaxSize(), + floatingActionButtonPosition = FabPosition.Center, + floatingActionButton = { PageButtons(hasPrevious, hasNext) }, + bottomBar = { + BottomTabs(currentTab) } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - TextButton( - text = "C1", - enabled = true - ) { - spotlight.activate(C1) - } - TextButton( - text = "C2", - enabled = true - ) { - spotlight.activate(C2) - } - TextButton( - text = "C3", - enabled = true - ) { - spotlight.activate(C3) - } - } - + ) { Children( modifier = Modifier - .padding(top = 12.dp, bottom = 12.dp) - .fillMaxWidth(), - transitionHandler = rememberSpotlightSlider(clipToBounds = true), + .padding(it), + transitionHandler = rememberSpotlightFaderThrough(), navModel = spotlight ) + } + } + @Composable + private fun BottomTabs(currentTab: State) { + NavigationBar { + Item.values().forEach { + val selected = currentTab.value == it.navTarget + NavigationBarItem( + icon = { + Icon( + if (selected) + Icons.Filled.Favorite + else + Icons.Outlined.FavoriteBorder, + contentDescription = it.toString() + ) + }, + label = { Text(it.toString()) }, + selected = selected, + onClick = { spotlight.activate(it) } + ) + } } } @Composable - private fun TextButton(text: String, enabled: Boolean = true, onClick: () -> Unit) { - Button(onClick = onClick, enabled = enabled, modifier = Modifier.padding(4.dp)) { - Text(text = text) + private fun PageButtons( + hasPrevious: State, + hasNext: State + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + FilledIconButton( + onClick = { spotlight.previous() }, + modifier = if (hasPrevious.value) Modifier.shadow( + 4.dp, + IconButtonDefaults.filledShape + ) else Modifier, + enabled = hasPrevious.value, + ) { + Icon( + Icons.Filled.ArrowBack, + contentDescription = "Previous" + ) + } + FilledIconButton( + onClick = { spotlight.next() }, + modifier = if (hasNext.value) Modifier.shadow( + 4.dp, + IconButtonDefaults.filledShape + ) else Modifier, + enabled = hasNext.value, + ) { + Icon( + Icons.Filled.ArrowForward, + contentDescription = "Next" + ) + } } } diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt new file mode 100644 index 000000000..a048c4c25 --- /dev/null +++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt @@ -0,0 +1,89 @@ +package com.bumble.appyx.sandbox.client.spotlight + +import android.annotation.SuppressLint +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler +import com.bumble.appyx.core.navigation.transition.TransitionDescriptor +import com.bumble.appyx.navmodel.spotlight.Spotlight + +class SpotlightFaderThrough() : ModifierTransitionHandler() { + + @SuppressLint("ModifierFactoryExtensionFunction") + override fun createModifier( + modifier: Modifier, + transition: Transition, + descriptor: TransitionDescriptor + ): Modifier = modifier.composed { + val alpha = transition.animateFloat( + transitionSpec = { + when (targetState) { + Spotlight.State.ACTIVE -> tween( + durationMillis = 210, + delayMillis = 10, + easing = FastOutSlowInEasing + ) + + Spotlight.State.INACTIVE_BEFORE, + Spotlight.State.INACTIVE_AFTER -> tween( + durationMillis = 90, + delayMillis = 0, + easing = FastOutSlowInEasing + ) + } + + }, + targetValueByState = { + when (it) { + Spotlight.State.ACTIVE -> 1f + else -> 0f + } + }, label = "" + ) + val scale = transition.animateFloat( + transitionSpec = { + when (targetState) { + Spotlight.State.ACTIVE -> tween( + durationMillis = 210, + delayMillis = 10, + easing = FastOutSlowInEasing + ) + + Spotlight.State.INACTIVE_BEFORE, + Spotlight.State.INACTIVE_AFTER -> tween( + durationMillis = 90, + delayMillis = 0, + easing = FastOutSlowInEasing + ) + } + + }, + targetValueByState = { + when (it) { + Spotlight.State.ACTIVE -> 1f + else -> 0.92f + } + }, label = "" + ) + + if (transition.targetState == Spotlight.State.ACTIVE) { + scale(scale.value).alpha(alpha.value) + } else { + alpha(alpha.value) + } + + } +} + +@Composable +fun rememberSpotlightFaderThrough(): ModifierTransitionHandler = remember { + SpotlightFaderThrough() +} \ No newline at end of file From 612138ba743640f651c2fc96e4206a73df3e985e Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 20 Dec 2022 19:03:42 +0400 Subject: [PATCH 3/5] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b3b6e96..70aa8a383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - [#287](https://github.com/bumble-tech/appyx/pull/287) – **Added**: `ImmutableList` has been added to avoid non-skippable compositions. - [#289](https://github.com/bumble-tech/appyx/issues/289) – **Added**: Introduced `interop-rx3` for RxJava 3 support. This has identical functionality to `interop-rx2`. - [#298](https://github.com/bumble-tech/appyx/pulls/298) – **Updated**: ChildView documentation. `TransitionDescriptor` generics has been renamed to `NavTarget` and `State` +- [#307](https://github.com/bumble-tech/appyx/pull/307) - **Added**: `Spotlight,current()` method to observe currently active `NavTarget. --- From 41811ef1b959a47cea825009c23c82b0f301ee0f Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 21 Dec 2022 12:45:25 +0400 Subject: [PATCH 4/5] Fix CR comments --- CHANGELOG.md | 2 +- .../client/spotlight/SpotlightExampleNode.kt | 32 +++++++++---------- .../client/spotlight/SpotlightFaderThrough.kt | 28 +++++++++------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70aa8a383..9d0bd33a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - [#287](https://github.com/bumble-tech/appyx/pull/287) – **Added**: `ImmutableList` has been added to avoid non-skippable compositions. - [#289](https://github.com/bumble-tech/appyx/issues/289) – **Added**: Introduced `interop-rx3` for RxJava 3 support. This has identical functionality to `interop-rx2`. - [#298](https://github.com/bumble-tech/appyx/pulls/298) – **Updated**: ChildView documentation. `TransitionDescriptor` generics has been renamed to `NavTarget` and `State` -- [#307](https://github.com/bumble-tech/appyx/pull/307) - **Added**: `Spotlight,current()` method to observe currently active `NavTarget. +- [#307](https://github.com/bumble-tech/appyx/pull/307) - **Added**: `Spotlight.current()` method to observe currently active `NavTarget. --- diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightExampleNode.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightExampleNode.kt index b9bdb4301..328a5948f 100644 --- a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightExampleNode.kt +++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightExampleNode.kt @@ -84,7 +84,7 @@ class SpotlightExampleNode( screenState.value = Loading lifecycle.coroutineScope.launch { delay(1000) - spotlight.updateElements(items = Item.getItemList()) + spotlight.updateElements(items = Tab.getTabList()) screenState.value = Loaded } } else { @@ -105,13 +105,13 @@ class SpotlightExampleNode( } @Parcelize - private enum class Item(val navTarget: NavTarget) : Parcelable { + private enum class Tab(val navTarget: NavTarget) : Parcelable { C1(Child1), C2(Child2), C3(Child3); companion object { - fun getItemList() = values().map { it.navTarget } + fun getTabList() = values().map { it.navTarget } } } @@ -149,7 +149,7 @@ class SpotlightExampleNode( Scaffold( modifier = modifier.fillMaxSize(), floatingActionButtonPosition = FabPosition.Center, - floatingActionButton = { PageButtons(hasPrevious, hasNext) }, + floatingActionButton = { PageButtons(hasPrevious.value, hasNext.value) }, bottomBar = { BottomTabs(currentTab) } @@ -166,8 +166,8 @@ class SpotlightExampleNode( @Composable private fun BottomTabs(currentTab: State) { NavigationBar { - Item.values().forEach { - val selected = currentTab.value == it.navTarget + Tab.values().forEach { tab -> + val selected = currentTab.value == tab.navTarget NavigationBarItem( icon = { Icon( @@ -175,12 +175,12 @@ class SpotlightExampleNode( Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, - contentDescription = it.toString() + contentDescription = tab.toString() ) }, - label = { Text(it.toString()) }, + label = { Text(tab.toString()) }, selected = selected, - onClick = { spotlight.activate(it) } + onClick = { spotlight.activate(tab) } ) } } @@ -188,8 +188,8 @@ class SpotlightExampleNode( @Composable private fun PageButtons( - hasPrevious: State, - hasNext: State + hasPrevious: Boolean, + hasNext: Boolean ) { Row( modifier = Modifier @@ -200,11 +200,11 @@ class SpotlightExampleNode( ) { FilledIconButton( onClick = { spotlight.previous() }, - modifier = if (hasPrevious.value) Modifier.shadow( + modifier = if (hasPrevious) Modifier.shadow( 4.dp, IconButtonDefaults.filledShape ) else Modifier, - enabled = hasPrevious.value, + enabled = hasPrevious, ) { Icon( Icons.Filled.ArrowBack, @@ -213,11 +213,11 @@ class SpotlightExampleNode( } FilledIconButton( onClick = { spotlight.next() }, - modifier = if (hasNext.value) Modifier.shadow( + modifier = if (hasNext) Modifier.shadow( 4.dp, IconButtonDefaults.filledShape ) else Modifier, - enabled = hasNext.value, + enabled = hasNext, ) { Icon( Icons.Filled.ArrowForward, @@ -227,7 +227,7 @@ class SpotlightExampleNode( } } - private fun Spotlight<*>.activate(item: Item) { + private fun Spotlight<*>.activate(item: Tab) { activate(item.ordinal) } } diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt index a048c4c25..421bfae5d 100644 --- a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt +++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt @@ -15,6 +15,10 @@ import com.bumble.appyx.core.navigation.transition.ModifierTransitionHandler import com.bumble.appyx.core.navigation.transition.TransitionDescriptor import com.bumble.appyx.navmodel.spotlight.Spotlight +/** + * Fade through transition from material design + * [Specification](https://m2.material.io/design/motion/the-motion-system.html#fade-through) + */ class SpotlightFaderThrough() : ModifierTransitionHandler() { @SuppressLint("ModifierFactoryExtensionFunction") @@ -27,15 +31,14 @@ class SpotlightFaderThrough() : ModifierTransitionHandler transitionSpec = { when (targetState) { Spotlight.State.ACTIVE -> tween( - durationMillis = 210, - delayMillis = 10, + durationMillis = enterDuration, + delayMillis = exitDuration, easing = FastOutSlowInEasing ) Spotlight.State.INACTIVE_BEFORE, Spotlight.State.INACTIVE_AFTER -> tween( - durationMillis = 90, - delayMillis = 0, + durationMillis = exitDuration, easing = FastOutSlowInEasing ) } @@ -52,15 +55,14 @@ class SpotlightFaderThrough() : ModifierTransitionHandler transitionSpec = { when (targetState) { Spotlight.State.ACTIVE -> tween( - durationMillis = 210, - delayMillis = 10, + durationMillis = enterDuration, + delayMillis = exitDuration, easing = FastOutSlowInEasing ) Spotlight.State.INACTIVE_BEFORE, Spotlight.State.INACTIVE_AFTER -> tween( - durationMillis = 90, - delayMillis = 0, + durationMillis = exitDuration, easing = FastOutSlowInEasing ) } @@ -81,9 +83,13 @@ class SpotlightFaderThrough() : ModifierTransitionHandler } } + + companion object { + private const val enterDuration = 210 + private const val exitDuration = 90 + } } @Composable -fun rememberSpotlightFaderThrough(): ModifierTransitionHandler = remember { - SpotlightFaderThrough() -} \ No newline at end of file +fun rememberSpotlightFaderThrough(): ModifierTransitionHandler = + remember { SpotlightFaderThrough() } From d3d42fd7929f0c59fd307ca771cdc956a0fb7e14 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 21 Dec 2022 13:03:28 +0400 Subject: [PATCH 5/5] Fix detekt --- .../appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt index 421bfae5d..9a9168ea2 100644 --- a/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt +++ b/samples/sandbox/src/main/kotlin/com/bumble/appyx/sandbox/client/spotlight/SpotlightFaderThrough.kt @@ -19,7 +19,7 @@ import com.bumble.appyx.navmodel.spotlight.Spotlight * Fade through transition from material design * [Specification](https://m2.material.io/design/motion/the-motion-system.html#fade-through) */ -class SpotlightFaderThrough() : ModifierTransitionHandler() { +class SpotlightFaderThrough : ModifierTransitionHandler() { @SuppressLint("ModifierFactoryExtensionFunction") override fun createModifier(