1
1
package dev.omkartenkale.nodal.compose.transitions
2
2
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
3
7
import androidx.compose.runtime.State
4
8
import androidx.compose.runtime.derivedStateOf
5
9
import androidx.compose.ui.Modifier
6
10
import androidx.compose.ui.draw.alpha
11
+ import androidx.compose.ui.graphics.Color
7
12
import androidx.compose.ui.layout.LayoutModifier
8
13
import androidx.compose.ui.layout.Measurable
9
14
import androidx.compose.ui.layout.MeasureResult
10
15
import androidx.compose.ui.layout.MeasureScope
11
16
import androidx.compose.ui.unit.Constraints
12
17
import androidx.compose.ui.unit.IntOffset
13
18
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
+ }
14
44
15
45
/* *
16
46
* Defines transitions for a [Backstack]. Transitions control how screens are rendered by returning
@@ -36,39 +66,116 @@ public fun interface BackstackTransition {
36
66
isTop : Boolean
37
67
): Modifier
38
68
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
+ }
62
134
}
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 )"
63
142
}
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
+ }
64
170
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
+ )
70
176
71
- override fun toString (): String = " ${this ::class .simpleName} (offset=$offset )"
177
+ override fun toString (): String = " ${this ::class .simpleName} (offset=$offset )"
178
+ }
72
179
}
73
180
}
74
181
@@ -83,7 +190,7 @@ public fun interface BackstackTransition {
83
190
}
84
191
85
192
/* *
86
- * A simple transition that crossfades between screens.
193
+ * No transition
87
194
*/
88
195
public object None : BackstackTransition {
89
196
override fun Modifier.modifierForScreen (
@@ -92,7 +199,6 @@ public fun interface BackstackTransition {
92
199
): Modifier = this
93
200
}
94
201
}
95
-
96
202
/* *
97
203
* Convenience function to make it easier to make composition transitions.
98
204
*/
0 commit comments