Skip to content

Commit

Permalink
Setup basic transitions
Browse files Browse the repository at this point in the history
  • Loading branch information
omkar-tenkale committed Jul 2, 2024
1 parent 57680c1 commit bfdba28
Show file tree
Hide file tree
Showing 12 changed files with 625 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,13 @@ public abstract class NodalActivity : AppCompatActivity() {
provides<UI> {
UI().also {
ui = it
container.addView(ComposeView(this@NodalActivity).also { setContent { ui.drawLayers() } })
container.addView(ComposeView(this@NodalActivity).also { setContent { ui.Content() } })
}
}
include(dependencyDeclaration)
}.also {
RootNodeUtil.dispatchAdded(it)
}
container.addView(ComposeView(this).also { setContent { ui.drawLayers() } })
}

@CallSuper
Expand Down
30 changes: 16 additions & 14 deletions nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/UI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,53 @@ package dev.omkartenkale.nodal.compose

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import dev.omkartenkale.nodal.Node
import dev.omkartenkale.nodal.Node.Companion.ui
import dev.omkartenkale.nodal.compose.transitions.Backstack
import dev.omkartenkale.nodal.compose.transitions.BackstackTransition
import dev.omkartenkale.nodal.util.doOnRemoved
import kotlinx.coroutines.flow.MutableStateFlow

public class UI {
private val layers = mutableStateListOf<Layer>()

private var layers by mutableStateOf<List<Layer>>(emptyList())
public val focusState: MutableStateFlow<Boolean> = MutableStateFlow(false)

@Composable
public fun drawLayers() {
layers.forEach {
it.draw()
}
public fun Content() {
Backstack(backstack = layers)
}

public fun draw(content: @Composable (Modifier) -> Unit): Layer {
return Layer(content) {
layers.remove(it)
return Layer(content = content) {
layers -= it
}.also {
layers.add(it)
layers += it
}
}

public suspend fun dispatchFocusChanged(isFocused: Boolean) {
focusState.emit(isFocused)
}

public class Layer(public val content: @Composable (Modifier) -> Unit, internal val onDestroy: (Layer)->Unit) {
public class Layer(public val transition: BackstackTransition = BackstackTransition.None, public val content: @Composable (Modifier) -> Unit, internal val onDestroy: (Layer)->Unit) {

@Composable
public fun draw() {
public fun Content() {
content(Modifier.fillMaxSize())
}

public fun destroy() {
onDestroy(this)
}

}
}

private fun List<UI.Layer>.secondToTop(): UI.Layer? = if(size < 2 ) null else get(lastIndex-1)

public fun Node.draw(content: @Composable (Modifier) -> Unit) {
val layer = ui.draw(content)
doOnRemoved {
Expand Down
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))
}
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)
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
)
}
Loading

0 comments on commit bfdba28

Please sign in to comment.