generated from Kotlin/multiplatform-library-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
57680c1
commit bfdba28
Showing
12 changed files
with
625 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
148
nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/Backstack.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
package dev.omkartenkale.nodal.compose.transitions | ||
|
||
|
||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.key | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.saveable.rememberSaveable | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.clip | ||
import androidx.compose.ui.graphics.RectangleShape | ||
import dev.omkartenkale.nodal.compose.UI | ||
|
||
/** | ||
* Identifies which direction a transition is being performed in. | ||
*/ | ||
internal enum class TransitionDirection { | ||
Forward, | ||
Backward | ||
} | ||
|
||
/** | ||
* Fork of https://github.com/rjrjr/compose-backstack | ||
* | ||
* Renders the top of a stack of screens (as [T]s) and animates between screens when the top | ||
* value changes. Any state used by a screen will be preserved as long as it remains in the stack | ||
* (i.e. result of [remember] calls). | ||
* | ||
* The [backstack] must follow some rules: | ||
* - Must always contain at least one item. | ||
* - Items in the stack must implement `equals` and not change over the lifetime of the screen. | ||
* If an item changes, it will be considered a new screen and any state held by the screen will | ||
* be lost. | ||
* - If items in the stack are reordered between compositions, the stack should not contain | ||
* duplicates. If it does, due to how `@Pivotal` works, the states of those screens will be | ||
* lost if they are moved around. If the list contains duplicates, an [IllegalArgumentException] | ||
* will be thrown. | ||
* | ||
* This composable does not actually provide any navigation functionality – it just manages state, | ||
* and delegates to [FrameController]s to do things like animate screen transitions. It can be | ||
* plugged into your navigation library of choice, or just used on its own with a simple list of | ||
* screens. | ||
* | ||
* ## Saveable state caching | ||
* | ||
* Screens that contain persistable state using [rememberSaveable] will automatically have that | ||
* state saved when they are hidden, and restored the next time they're shown. | ||
* | ||
* ## Example | ||
* | ||
* ``` | ||
* sealed class Screen { | ||
* object ContactList: Screen() | ||
* data class ContactDetails(val id: String): Screen() | ||
* data class EditContact(val id: String): Screen() | ||
* } | ||
* | ||
* data class Navigator( | ||
* val push: (Screen) -> Unit, | ||
* val pop: () -> Unit | ||
* ) | ||
* | ||
* @Composable fun App() { | ||
* var backstack: List<Screen> by remember { mutableStateOf(listOf(Screen.ContactList)) } | ||
* val navigator = remember { | ||
* Navigator( | ||
* push = { backstack += it }, | ||
* pop = { backstack = backstack.dropLast(1) } | ||
* ) | ||
* } | ||
* | ||
* Backstack(backstack) { screen -> | ||
* when(screen) { | ||
* Screen.ContactList -> ShowContactList(navigator) | ||
* is Screen.ContactDetails -> ShowContact(screen.id, navigator) | ||
* is Screen.EditContact -> ShowEditContact(screen.id, navigator) | ||
* } | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* @param backstack The stack of screen values. | ||
* @param modifier [Modifier] that will be applied to the container of screens. Neither affects nor | ||
* is affected by transition animations. | ||
* @param frameController The [FrameController] that manages things like transition animations. | ||
* Use [rememberTransitionController] for a reasonable default, or use the overload of this function | ||
* that takes a [BackstackTransition] instead. | ||
* @param content Called with each element of [backstack] to render it. | ||
*/ | ||
@Composable | ||
internal fun Backstack( | ||
backstack: List<UI.Layer>, | ||
modifier: Modifier = Modifier, | ||
frameController: TransitionController, | ||
) { | ||
|
||
// Notify the frame controller that the backstack has changed to allow it to do stuff like start | ||
// animating transitions. This call should eventually cause activeFrames to change, but that might | ||
// not happen immediately. | ||
// | ||
// Note: It's probably bad that this call is not done in a side effect. If the composition fails, | ||
// the controller won't know about it and will continue animating or whatever it was doing. | ||
// However, we do need to give the controller the chance to initialize itself with the initial | ||
// stack before we ask for its activeFrames, so this is a lazy way to do both that and subsequent | ||
// updates. | ||
frameController.updateBackstack(backstack) | ||
|
||
// Actually draw the screens. | ||
Box(modifier = modifier.clip(RectangleShape)) { | ||
// The frame controller is in complete control of what we actually show. The activeFrames | ||
// property should be backed by a snapshot state object, so this will recompose automatically | ||
// if the controller changes its frames. | ||
frameController.activeFrames.forEach { (item, frameControlModifier) -> | ||
// Even if screens are moved around within the list, as long as they're invoked through the | ||
// exact same sequence of source locations from within this key lambda, they will keep their | ||
// state. | ||
key(item) { | ||
// This call must be inside the key(){} wrapper. | ||
Box(frameControlModifier) { | ||
item.Content() | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Renders the top of a stack of screens (as [T]s) and animates between screens when the top | ||
* value changes. Any state used by a screen will be preserved as long as it remains in the stack | ||
* (i.e. result of [remember] calls). | ||
* | ||
* See the documentation on [Backstack] for more information. | ||
* | ||
* @param backstack The stack of screen values. | ||
* @param modifier [Modifier] that will be applied to the container of screens. Neither affects nor | ||
* is affected by transition animations. | ||
* @param transition The [BackstackTransition] to use to animate screen transitions. For more, | ||
* call [rememberTransitionController] and pass it to the overload of this function that takes a | ||
* [FrameController] directly. | ||
* @param content Called with each element of [backstack] to render it. | ||
*/ | ||
@Composable internal fun Backstack( | ||
backstack: List<UI.Layer>, | ||
modifier: Modifier = Modifier, | ||
transition: BackstackTransition = BackstackTransition.Slide | ||
) { | ||
Backstack(backstack, modifier, rememberTransitionController(transition)) | ||
} |
102 changes: 102 additions & 0 deletions
102
...l/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/BackstackTransition.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package dev.omkartenkale.nodal.compose.transitions | ||
|
||
import androidx.compose.runtime.State | ||
import androidx.compose.runtime.derivedStateOf | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.alpha | ||
import androidx.compose.ui.layout.LayoutModifier | ||
import androidx.compose.ui.layout.Measurable | ||
import androidx.compose.ui.layout.MeasureResult | ||
import androidx.compose.ui.layout.MeasureScope | ||
import androidx.compose.ui.unit.Constraints | ||
import androidx.compose.ui.unit.IntOffset | ||
import androidx.compose.ui.unit.IntSize | ||
|
||
/** | ||
* Defines transitions for a [Backstack]. Transitions control how screens are rendered by returning | ||
* [Modifier]s that will be used to wrap screen composables. | ||
* | ||
* @see Slide | ||
* @see Crossfade | ||
*/ | ||
public fun interface BackstackTransition { | ||
|
||
/** | ||
* Returns a [Modifier] to use to draw screen in a [Backstack]. | ||
* | ||
* @param visibility A float in the range `[0, 1]` that indicates at what visibility this screen | ||
* should be drawn. For example, this value will increase when [isTop] is true and the transition | ||
* is in the forward direction. | ||
* @param isTop True only when being called for the top screen. E.g. if the screen is partially | ||
* visible, then the top screen is always transitioning _out_, and non-top screens are either | ||
* transitioning out or invisible. | ||
*/ | ||
public fun Modifier.modifierForScreen( | ||
visibility: State<Float>, | ||
isTop: Boolean | ||
): Modifier | ||
|
||
/** | ||
* A simple transition that slides screens horizontally. | ||
*/ | ||
public object Slide : BackstackTransition { | ||
override fun Modifier.modifierForScreen( | ||
visibility: State<Float>, | ||
isTop: Boolean | ||
): Modifier = then(PercentageLayoutOffset( | ||
rawOffset = derivedStateOf { if (isTop) 1f - visibility.value else -1 + visibility.value } | ||
)) | ||
|
||
|
||
internal class PercentageLayoutOffset(private val rawOffset: State<Float>) : | ||
LayoutModifier { | ||
private val offset = { rawOffset.value.coerceIn(-1f..1f) } | ||
|
||
override fun MeasureScope.measure( | ||
measurable: Measurable, | ||
constraints: Constraints | ||
): MeasureResult { | ||
val placeable = measurable.measure(constraints) | ||
return layout(placeable.width, placeable.height) { | ||
placeable.place(offsetPosition(IntSize(placeable.width, placeable.height))) | ||
} | ||
} | ||
|
||
internal fun offsetPosition(containerSize: IntSize) = IntOffset( | ||
// RTL is handled automatically by place. | ||
x = (containerSize.width * offset()).toInt(), | ||
y = 0 | ||
) | ||
|
||
override fun toString(): String = "${this::class.simpleName}(offset=$offset)" | ||
} | ||
} | ||
|
||
/** | ||
* A simple transition that crossfades between screens. | ||
*/ | ||
public object Crossfade : BackstackTransition { | ||
override fun Modifier.modifierForScreen( | ||
visibility: State<Float>, | ||
isTop: Boolean | ||
): Modifier = alpha(visibility.value) | ||
} | ||
|
||
/** | ||
* A simple transition that crossfades between screens. | ||
*/ | ||
public object None : BackstackTransition { | ||
override fun Modifier.modifierForScreen( | ||
visibility: State<Float>, | ||
isTop: Boolean | ||
): Modifier = this | ||
} | ||
} | ||
|
||
/** | ||
* Convenience function to make it easier to make composition transitions. | ||
*/ | ||
public fun BackstackTransition.modifierForScreen( | ||
visibility: State<Float>, | ||
isTop: Boolean | ||
): Modifier = Modifier.modifierForScreen(visibility, isTop) |
55 changes: 55 additions & 0 deletions
55
nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/FrameController.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package dev.omkartenkale.nodal.compose.transitions | ||
|
||
import androidx.compose.runtime.Immutable | ||
import androidx.compose.runtime.MutableState | ||
import androidx.compose.runtime.Stable | ||
import androidx.compose.runtime.mutableStateListOf | ||
import androidx.compose.runtime.snapshots.SnapshotStateList | ||
import androidx.compose.ui.Modifier | ||
|
||
/** | ||
* A stable object that processes changes to a [Backstack]'s list of screen keys, determining which | ||
* screens should be actively composed at any given time, and tweaking their appearance by applying | ||
* [Modifier]s. | ||
* | ||
* The [Backstack] composable will notify its controller whenever the backstack changes by calling | ||
* [updateBackstack], but the controller is in full control of when those changes actually get | ||
* reflected in the composition. For example, a controller may choose to keep some screens around | ||
* for a while, even after they're removed from the backstack, in order to animate their removal. | ||
*/ | ||
@Stable | ||
internal interface FrameController<T : Any> { | ||
|
||
/** | ||
* The frames that are currently being active. All active frames will be composed. When a frame | ||
* that is in the backstack stops appearing in this list, its state will be saved. | ||
* | ||
* Should be backed by either a [MutableState] or a [SnapshotStateList]. This property | ||
* will not be read until after [updateBackstack] is called at least once. | ||
*/ | ||
val activeFrames: List<BackstackFrame<T>> | ||
|
||
/** | ||
* Notifies the controller that a new backstack was passed in. This method must initialize | ||
* [activeFrames] first time it's called, and subsequently should probably result in | ||
* [activeFrames] being updated to show new keys or hide old ones, although the controller may | ||
* choose to do that later (e.g. if one of the active frames is currently being animated). | ||
* | ||
* This method will be called _directly from the composition_ – it must not perform side effects | ||
* or update any state that is not backed by snapshot state objects (such as [MutableState]s, | ||
* lists created by [mutableStateListOf], etc.). | ||
* | ||
* @param keys The latest backstack passed to [Backstack]. Will always contain at least one | ||
* element. | ||
*/ | ||
fun updateBackstack(keys: List<T>) | ||
|
||
/** | ||
* A frame controlled by a [FrameController], to be shown by [Backstack]. | ||
*/ | ||
@Immutable | ||
data class BackstackFrame<out T : Any>( | ||
val key: T, | ||
val modifier: Modifier = Modifier | ||
) | ||
} |
Oops, something went wrong.