Skip to content

Commit b535810

Browse files
committed
back press and improved slide transition
1 parent bfdba28 commit b535810

File tree

13 files changed

+249
-129
lines changed

13 files changed

+249
-129
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package dev.omkartenkale.nodal
2+
3+
import androidx.activity.OnBackPressedCallback
4+
import androidx.activity.OnBackPressedDispatcher
5+
import dev.omkartenkale.nodal.misc.BackPressCallback
6+
import dev.omkartenkale.nodal.misc.BackPressHandler
7+
8+
public class DefaultBackPressHandler(private val onBackPressedDispatcher: OnBackPressedDispatcher) : BackPressHandler {
9+
override fun dispatchBackPress() {
10+
onBackPressedDispatcher.onBackPressed()
11+
}
12+
13+
override fun addBackPressCallback(backPressCallback: BackPressCallback) {
14+
onBackPressedDispatcher.addCallback(object :
15+
OnBackPressedCallback(backPressCallback.isEnabled) {
16+
override fun handleOnBackPressed() {
17+
backPressCallback()
18+
}
19+
})
20+
}
21+
}

nodal/src/androidMain/kotlin/dev.omkartenkale.nodal/NodalActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package dev.omkartenkale.nodal
22

33
import android.widget.FrameLayout
4-
import androidx.activity.OnBackPressedDispatcher
54
import androidx.activity.compose.setContent
65
import androidx.annotation.CallSuper
76
import androidx.appcompat.app.AppCompatActivity
87
import androidx.compose.ui.platform.ComposeView
98
import androidx.lifecycle.lifecycleScope
109
import dev.omkartenkale.nodal.Node.Companion.createRootNode
1110
import dev.omkartenkale.nodal.compose.UI
11+
import dev.omkartenkale.nodal.misc.BackPressHandler
1212
import dev.omkartenkale.nodal.util.RootNodeUtil
1313
import kotlinx.coroutines.launch
1414
import kotlin.reflect.KClass
@@ -61,7 +61,7 @@ public abstract class NodalActivity : AppCompatActivity() {
6161
//
6262
// providesSelf<RemovalRequest>(RemovalRequest{ finish() })
6363

64-
provides<OnBackPressedDispatcher> { onBackPressedDispatcher }
64+
provides<BackPressHandler> { DefaultBackPressHandler(onBackPressedDispatcher) }
6565
provides<UI> {
6666
UI().also {
6767
ui = it

nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/Node.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import dev.omkartenkale.nodal.compose.UI
44
import dev.omkartenkale.nodal.exceptions.DisallowedNodeAdditionException
55
import dev.omkartenkale.nodal.exceptions.NodeCreationException
66
import dev.omkartenkale.nodal.lifecycle.ChildrenUpdatedEvent
7+
import dev.omkartenkale.nodal.misc.BackPressHandler
78
import dev.omkartenkale.nodal.misc.RemovalRequest
89
import dev.omkartenkale.nodal.misc.instantiate
910
import dev.omkartenkale.nodal.plugin.NodalPlugin
@@ -144,6 +145,7 @@ public open class Node {
144145
}
145146

146147
public val Node.ui: UI get() = dependencies.get<UI>()
148+
public val Node.backPressHandler: BackPressHandler get() = dependencies.get<BackPressHandler>()
147149
private fun instantiateNode(
148150
klass: KClass<out Node>,
149151
parentScope: Scope,

nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/UI.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import dev.omkartenkale.nodal.Node
1010
import dev.omkartenkale.nodal.Node.Companion.ui
1111
import dev.omkartenkale.nodal.compose.transitions.Backstack
1212
import dev.omkartenkale.nodal.compose.transitions.BackstackTransition
13+
import dev.omkartenkale.nodal.compose.transitions.TransitionSpec
1314
import dev.omkartenkale.nodal.util.doOnRemoved
1415
import kotlinx.coroutines.flow.MutableStateFlow
1516

@@ -21,8 +22,8 @@ public class UI {
2122
Backstack(backstack = layers)
2223
}
2324

24-
public fun draw(content: @Composable (Modifier) -> Unit): Layer {
25-
return Layer(content = content) {
25+
public fun draw(transitionSpec: TransitionSpec = TransitionSpec.Slide, content: @Composable (Modifier) -> Unit): Layer {
26+
return Layer(transitionSpec = transitionSpec, content = content) {
2627
layers -= it
2728
}.also {
2829
layers += it
@@ -33,7 +34,7 @@ public class UI {
3334
focusState.emit(isFocused)
3435
}
3536

36-
public class Layer(public val transition: BackstackTransition = BackstackTransition.None, public val content: @Composable (Modifier) -> Unit, internal val onDestroy: (Layer)->Unit) {
37+
public class Layer(public val transitionSpec: TransitionSpec, public val content: @Composable (Modifier) -> Unit, internal val onDestroy: (Layer)->Unit) {
3738

3839
@Composable
3940
public fun Content() {
@@ -50,7 +51,7 @@ public class UI {
5051
private fun List<UI.Layer>.secondToTop(): UI.Layer? = if(size < 2 ) null else get(lastIndex-1)
5152

5253
public fun Node.draw(content: @Composable (Modifier) -> Unit) {
53-
val layer = ui.draw(content)
54+
val layer = ui.draw(content = content)
5455
doOnRemoved {
5556
layer.destroy()
5657
}

nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/Backstack.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ internal fun Backstack(
142142
@Composable internal fun Backstack(
143143
backstack: List<UI.Layer>,
144144
modifier: Modifier = Modifier,
145-
transition: BackstackTransition = BackstackTransition.Slide
146145
) {
147-
Backstack(backstack, modifier, rememberTransitionController(transition))
146+
Backstack(backstack, modifier, rememberTransitionController())
148147
}

nodal/src/commonMain/kotlin/dev.omkartenkale.nodal/compose/transitions/BackstackTransition.kt

Lines changed: 137 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,46 @@
11
package dev.omkartenkale.nodal.compose.transitions
22

3+
import androidx.compose.animation.core.AnimationSpec
4+
import androidx.compose.animation.core.CubicBezierEasing
5+
import androidx.compose.animation.core.TweenSpec
6+
import androidx.compose.foundation.background
37
import androidx.compose.runtime.State
48
import androidx.compose.runtime.derivedStateOf
59
import androidx.compose.ui.Modifier
610
import androidx.compose.ui.draw.alpha
11+
import androidx.compose.ui.graphics.Color
712
import androidx.compose.ui.layout.LayoutModifier
813
import androidx.compose.ui.layout.Measurable
914
import androidx.compose.ui.layout.MeasureResult
1015
import androidx.compose.ui.layout.MeasureScope
1116
import androidx.compose.ui.unit.Constraints
1217
import androidx.compose.ui.unit.IntOffset
1318
import androidx.compose.ui.unit.IntSize
19+
import androidx.compose.ui.util.lerp
20+
21+
// https://easings.net/#easeOutExpo
22+
internal val EaseOutExpoEasing = CubicBezierEasing(0.16f, 1f, 0.3f, 1f)
23+
public class TransitionSpec(
24+
public val topTransition: BackstackTransition,
25+
public val bottomTransition: BackstackTransition,
26+
public val bottomOnTop: Boolean,
27+
public val animationSpec: AnimationSpec<Float>,
28+
){
29+
public companion object{
30+
public val Slide: TransitionSpec = TransitionSpec(
31+
topTransition = BackstackTransition.Top.HorizontalSlide,
32+
bottomTransition = BackstackTransition.Bottom.HorizontalSlide,
33+
bottomOnTop = false,
34+
animationSpec = TweenSpec(durationMillis = 750, easing = EaseOutExpoEasing),
35+
)
36+
public val BottomSheet: TransitionSpec = TransitionSpec(
37+
topTransition = BackstackTransition.Top.VerticalSlide,
38+
bottomTransition = BackstackTransition.None,
39+
bottomOnTop = false,
40+
animationSpec = TweenSpec(durationMillis = 500, easing = EaseOutExpoEasing),
41+
)
42+
}
43+
}
1444

1545
/**
1646
* Defines transitions for a [Backstack]. Transitions control how screens are rendered by returning
@@ -36,39 +66,116 @@ public fun interface BackstackTransition {
3666
isTop: Boolean
3767
): Modifier
3868

39-
/**
40-
* A simple transition that slides screens horizontally.
41-
*/
42-
public object Slide : BackstackTransition {
43-
override fun Modifier.modifierForScreen(
44-
visibility: State<Float>,
45-
isTop: Boolean
46-
): Modifier = then(PercentageLayoutOffset(
47-
rawOffset = derivedStateOf { if (isTop) 1f - visibility.value else -1 + visibility.value }
48-
))
49-
50-
51-
internal class PercentageLayoutOffset(private val rawOffset: State<Float>) :
52-
LayoutModifier {
53-
private val offset = { rawOffset.value.coerceIn(-1f..1f) }
54-
55-
override fun MeasureScope.measure(
56-
measurable: Measurable,
57-
constraints: Constraints
58-
): MeasureResult {
59-
val placeable = measurable.measure(constraints)
60-
return layout(placeable.width, placeable.height) {
61-
placeable.place(offsetPosition(IntSize(placeable.width, placeable.height)))
69+
public object Top {
70+
/**
71+
* A simple transition that slides screens horizontally.
72+
*/
73+
public object HorizontalSlide : BackstackTransition {
74+
override fun Modifier.modifierForScreen(
75+
visibility: State<Float>,
76+
isTop: Boolean
77+
): Modifier = background(
78+
Color.Black.copy(
79+
alpha = lerp(
80+
0f,
81+
0.7f,
82+
visibility.value
83+
)
84+
)
85+
) then (PercentageLayoutOffset(
86+
rawOffset = derivedStateOf { if (isTop) 1f - visibility.value else -1 + visibility.value }
87+
))
88+
89+
90+
internal class PercentageLayoutOffset(private val rawOffset: State<Float>) :
91+
LayoutModifier {
92+
private val offset = { rawOffset.value.coerceIn(-1f..1f) }
93+
94+
override fun MeasureScope.measure(
95+
measurable: Measurable,
96+
constraints: Constraints
97+
): MeasureResult {
98+
val placeable = measurable.measure(constraints)
99+
return layout(placeable.width, placeable.height) {
100+
placeable.place(offsetPosition(IntSize(placeable.width, placeable.height)))
101+
}
102+
}
103+
104+
internal fun offsetPosition(containerSize: IntSize) = IntOffset(
105+
// RTL is handled automatically by place.
106+
x = (containerSize.width * offset()).toInt(),
107+
y = 0
108+
)
109+
110+
override fun toString(): String = "${this::class.simpleName}(offset=$offset)"
111+
}
112+
}
113+
114+
public object VerticalSlide : BackstackTransition {
115+
override fun Modifier.modifierForScreen(
116+
visibility: State<Float>,
117+
isTop: Boolean
118+
): Modifier = then(PercentageLayoutOffset(
119+
rawOffset = derivedStateOf { if (isTop) 1f - visibility.value else -1 + visibility.value }
120+
))
121+
122+
internal class PercentageLayoutOffset(private val rawOffset: State<Float>) :
123+
LayoutModifier {
124+
private val offset = { rawOffset.value.coerceIn(-1f..1f) }
125+
126+
override fun MeasureScope.measure(
127+
measurable: Measurable,
128+
constraints: Constraints
129+
): MeasureResult {
130+
val placeable = measurable.measure(constraints)
131+
return layout(placeable.width, placeable.height) {
132+
placeable.place(offsetPosition(IntSize(placeable.width, placeable.height)))
133+
}
62134
}
135+
136+
internal fun offsetPosition(containerSize: IntSize) = IntOffset(
137+
x = 0,
138+
y = (containerSize.height * offset()).toInt()
139+
)
140+
141+
override fun toString(): String = "${this::class.simpleName}(offset=$offset)"
63142
}
143+
}
144+
145+
146+
}
147+
148+
public object Bottom {
149+
public object HorizontalSlide : BackstackTransition {
150+
override fun Modifier.modifierForScreen(
151+
visibility: State<Float>,
152+
isTop: Boolean //isTop is always false
153+
): Modifier = then (PercentageLayoutOffset(
154+
rawOffset = derivedStateOf { if (isTop) 1f - visibility.value else -1 * lerp(0.1f, 0f, visibility.value)}
155+
))
156+
157+
internal class PercentageLayoutOffset(private val rawOffset: State<Float>) :
158+
LayoutModifier {
159+
private val offset = { rawOffset.value.coerceIn(-1f..1f) }
160+
161+
override fun MeasureScope.measure(
162+
measurable: Measurable,
163+
constraints: Constraints
164+
): MeasureResult {
165+
val placeable = measurable.measure(constraints)
166+
return layout(placeable.width, placeable.height) {
167+
placeable.place(offsetPosition(IntSize(placeable.width, placeable.height)))
168+
}
169+
}
64170

65-
internal fun offsetPosition(containerSize: IntSize) = IntOffset(
66-
// RTL is handled automatically by place.
67-
x = (containerSize.width * offset()).toInt(),
68-
y = 0
69-
)
171+
internal fun offsetPosition(containerSize: IntSize) = IntOffset(
172+
// RTL is handled automatically by place.
173+
x = (containerSize.width * offset()).toInt(),
174+
y = 0
175+
)
70176

71-
override fun toString(): String = "${this::class.simpleName}(offset=$offset)"
177+
override fun toString(): String = "${this::class.simpleName}(offset=$offset)"
178+
}
72179
}
73180
}
74181

@@ -83,7 +190,7 @@ public fun interface BackstackTransition {
83190
}
84191

85192
/**
86-
* A simple transition that crossfades between screens.
193+
* No transition
87194
*/
88195
public object None : BackstackTransition {
89196
override fun Modifier.modifierForScreen(
@@ -92,7 +199,6 @@ public fun interface BackstackTransition {
92199
): Modifier = this
93200
}
94201
}
95-
96202
/**
97203
* Convenience function to make it easier to make composition transitions.
98204
*/

0 commit comments

Comments
 (0)