@@ -2,38 +2,43 @@ package kiwi.orbit.compose.ui.controls
2
2
3
3
import androidx.compose.foundation.background
4
4
import androidx.compose.foundation.layout.Box
5
- import androidx.compose.foundation.layout.ExperimentalLayoutApi
6
5
import androidx.compose.foundation.layout.PaddingValues
6
+ import androidx.compose.foundation.layout.Spacer
7
7
import androidx.compose.foundation.layout.WindowInsets
8
- import androidx.compose.foundation.layout.asPaddingValues
9
- import androidx.compose.foundation.layout.calculateEndPadding
10
- import androidx.compose.foundation.layout.calculateStartPadding
11
8
import androidx.compose.foundation.layout.fillMaxSize
12
9
import androidx.compose.foundation.layout.fillMaxWidth
13
10
import androidx.compose.foundation.layout.ime
14
- import androidx.compose.foundation.layout.isImeVisible
15
11
import androidx.compose.foundation.layout.systemBars
16
- import androidx.compose.foundation.layout.union
12
+ import androidx.compose.foundation.layout.windowInsetsBottomHeight
13
+ import androidx.compose.foundation.layout.windowInsetsEndWidth
14
+ import androidx.compose.foundation.layout.windowInsetsPadding
15
+ import androidx.compose.foundation.layout.windowInsetsStartWidth
16
+ import androidx.compose.foundation.layout.windowInsetsTopHeight
17
17
import androidx.compose.foundation.layout.wrapContentSize
18
18
import androidx.compose.runtime.Composable
19
+ import androidx.compose.runtime.getValue
20
+ import androidx.compose.runtime.mutableStateOf
19
21
import androidx.compose.runtime.remember
22
+ import androidx.compose.runtime.setValue
20
23
import androidx.compose.ui.Alignment
21
24
import androidx.compose.ui.Modifier
22
25
import androidx.compose.ui.graphics.Brush
23
26
import androidx.compose.ui.graphics.Color
24
- import androidx.compose.ui.layout.AlignmentLine
25
27
import androidx.compose.ui.layout.HorizontalAlignmentLine
26
28
import androidx.compose.ui.layout.Layout
27
- import androidx.compose.ui.layout.SubcomposeLayout
28
29
import androidx.compose.ui.platform.LocalDensity
30
+ import androidx.compose.ui.unit.Constraints
29
31
import androidx.compose.ui.unit.Dp
32
+ import androidx.compose.ui.unit.LayoutDirection
30
33
import androidx.compose.ui.unit.dp
31
34
import androidx.compose.ui.unit.offset
32
35
import kiwi.orbit.compose.ui.OrbitTheme
33
36
import kiwi.orbit.compose.ui.controls.internal.CustomPlaceholder
34
37
import kiwi.orbit.compose.ui.controls.internal.OrbitPreviews
35
38
import kiwi.orbit.compose.ui.controls.internal.Preview
36
39
import kiwi.orbit.compose.ui.foundation.contentColorFor
40
+ import kotlin.math.min
41
+ import kotlin.math.roundToInt
37
42
38
43
/* *
39
44
* Scaffold helps layouting basic screen widgets, such as [TopAppBar], [action] and the [content].
@@ -43,6 +48,8 @@ import kiwi.orbit.compose.ui.foundation.contentColorFor
43
48
*
44
49
* Utilize contentPadding passed to [content] lambda to handle IME, navigation or status bar paddings.
45
50
* If you define your own [actionLayout], do not forget to handle IME and navigation bar insets in it.
51
+ *
52
+ * The [contentWindowInsets] allows disabling handling IME insets and keep the Scaffold unaffected.
46
53
*/
47
54
@Composable
48
55
public fun Scaffold (
@@ -59,7 +66,7 @@ public fun Scaffold(
59
66
},
60
67
toastHostState : ToastHostState = remember { ToastHostState () },
61
68
toastHost : @Composable (ToastHostState ) -> Unit = { ToastHost (it) },
62
- contentWindowInsets : WindowInsets = WindowInsets .systemBars.union( WindowInsets . ime) ,
69
+ contentWindowInsets : WindowInsets = WindowInsets .ime,
63
70
content : @Composable (contentPadding: PaddingValues ) -> Unit ,
64
71
) {
65
72
Surface (
@@ -77,7 +84,6 @@ public fun Scaffold(
77
84
}
78
85
}
79
86
80
- @OptIn(ExperimentalLayoutApi ::class )
81
87
@Composable
82
88
private fun ScaffoldLayout (
83
89
topBar : @Composable () -> Unit ,
@@ -86,62 +92,88 @@ private fun ScaffoldLayout(
86
92
content : @Composable (contentPadding: PaddingValues ) -> Unit ,
87
93
contentWindowInsets : WindowInsets ,
88
94
) {
89
- val imeOpened = WindowInsets .isImeVisible
90
- SubcomposeLayout { constraints ->
95
+ var topContentPadding by remember { mutableStateOf(0 .dp) }
96
+ var startContentPadding by remember { mutableStateOf(0 .dp) }
97
+ var endContentPadding by remember { mutableStateOf(0 .dp) }
98
+ var bottomContentPadding by remember { mutableStateOf(0 .dp) }
99
+ val contentPadding = remember {
100
+ object : PaddingValues {
101
+ override fun calculateLeftPadding (layoutDirection : LayoutDirection ): Dp =
102
+ when (layoutDirection) {
103
+ LayoutDirection .Ltr -> startContentPadding
104
+ LayoutDirection .Rtl -> endContentPadding
105
+ }
106
+
107
+ override fun calculateTopPadding (): Dp = topContentPadding
108
+ override fun calculateRightPadding (layoutDirection : LayoutDirection ): Dp =
109
+ when (layoutDirection) {
110
+ LayoutDirection .Ltr -> endContentPadding
111
+ LayoutDirection .Rtl -> startContentPadding
112
+ }
113
+
114
+ override fun calculateBottomPadding (): Dp = bottomContentPadding
115
+ }
116
+ }
117
+
118
+ Layout (
119
+ modifier = Modifier .windowInsetsPadding(contentWindowInsets),
120
+ contents = listOf (
121
+ { Spacer (Modifier .windowInsetsTopHeight(WindowInsets .systemBars)) },
122
+ { Spacer (Modifier .windowInsetsBottomHeight(WindowInsets .systemBars)) },
123
+ { Spacer (Modifier .windowInsetsStartWidth(WindowInsets .systemBars)) },
124
+ { Spacer (Modifier .windowInsetsEndWidth(WindowInsets .systemBars)) },
125
+ topBar,
126
+ toast,
127
+ action,
128
+ { content(contentPadding) },
129
+ ),
130
+ ) { measurables, constraints ->
91
131
val layoutWidth = constraints.maxWidth
92
132
val layoutHeight = constraints.maxHeight
93
- val looseConstraints = constraints.copy(minWidth = 0 , minHeight = 0 )
133
+ val looseConstraint = constraints.copy(minWidth = 0 , minHeight = 0 )
134
+
135
+ // Measure insets separately and reuse for computation when needed
136
+ val insetTop = measurables[0 ][0 ].measure(looseConstraint).height
137
+ val insetBottom = measurables[1 ][0 ].measure(looseConstraint).height
138
+ val insetStart = measurables[2 ][0 ].measure(looseConstraint).width
139
+ val insetEnd = measurables[3 ][0 ].measure(looseConstraint).width
140
+
141
+ // Measure top bar
142
+ val topBarPlaceables = measurables[4 ].map { it.measure(looseConstraint) }
94
143
95
- val topBarPlaceables = subcompose( SlotTopAppBar , topBar).map { it.measure(looseConstraints) }
144
+ // Calculate the space for action, it is limited to 80% of the available height.
96
145
val topBarHeight = topBarPlaceables.maxOfOrNull { it.height } ? : 0
146
+ val actionTop = if (topBarHeight != 0 ) topBarHeight else insetTop
147
+ val maxActionHeight = ((layoutHeight - actionTop) * 0.8 ).roundToInt()
148
+ val actionConstraints = looseConstraint.copy(maxHeight = maxActionHeight)
97
149
98
- val maxActionRatio = 0.8f
99
- val maxActionHeight = (layoutHeight - topBarHeight) * maxActionRatio
100
- val actionConstraints = looseConstraints.copy(maxHeight = maxActionHeight.toInt())
101
- val actionPlaceables = subcompose(SlotAction , action).map { it.measure(actionConstraints) }
150
+ // Measure action
151
+ val actionPlaceables = measurables[6 ].map { it.measure(actionConstraints) }
152
+
153
+ // Calculate the space for content and toast.
154
+ val actionFadeHeight = actionPlaceables.firstOrNull()?.get(ActionFadeLine )
155
+ ?.takeIf { it >= 0 } ? : 0
102
156
val actionHeight = actionPlaceables.maxOfOrNull { it.height } ? : 0
103
- val actionFadeHeight = actionPlaceables.firstOrNull()?.get(ActionFadeLine )?.let { value ->
104
- if (value == AlignmentLine .Unspecified ) 0 else value
105
- } ? : 0
106
157
107
- val contentInsets = contentWindowInsets.asPaddingValues(this )
108
- val contentBottom = if (actionHeight > 0 ) {
109
- (actionHeight - actionFadeHeight)
110
- } else {
111
- if (imeOpened) contentInsets.calculateBottomPadding().roundToPx() else 0
112
- }
113
- val contentConstraints = looseConstraints.copy(
114
- maxHeight = layoutHeight - topBarHeight - contentBottom,
115
- )
116
- val innerPadding = PaddingValues (
117
- top = if (topBarHeight > 0 ) {
118
- 0 .dp
119
- } else {
120
- contentInsets.calculateTopPadding()
121
- },
122
- bottom = if (actionHeight > 0 ) {
123
- actionFadeHeight.toDp()
124
- } else {
125
- if (imeOpened) {
126
- 0 .dp
127
- } else {
128
- contentInsets.calculateBottomPadding()
129
- }
130
- },
131
- start = contentInsets.calculateStartPadding(layoutDirection),
132
- end = contentInsets.calculateEndPadding(layoutDirection),
133
- )
134
- val contentPlaceables = subcompose(SlotContent ) {
135
- content(innerPadding)
136
- }.map { it.measure(contentConstraints) }
158
+ // If there is fade, let's do not subtract it, we want content to be shown bellow it.
159
+ val maxContentHeight = layoutHeight - topBarHeight - actionHeight + actionFadeHeight
160
+ val contentConstraints = Constraints (maxHeight = maxContentHeight, maxWidth = layoutWidth)
161
+
162
+ // Update insets paddings for content before measuring it.
163
+ topContentPadding = if (topBarHeight == 0 ) insetTop.toDp() else 0 .dp
164
+ bottomContentPadding = if (actionHeight == 0 ) insetBottom.toDp() else actionFadeHeight.toDp()
165
+ startContentPadding = insetStart.toDp()
166
+ endContentPadding = insetEnd.toDp()
137
167
138
- val toastPlaceables = subcompose(SlotToast , toast).map { it.measure(looseConstraints) }
168
+ // Measure toast and content.
169
+ val toastPlaceables = measurables[5 ].map { it.measure(contentConstraints) }
170
+ val contentPlaceables = measurables[7 ].map { it.measure(contentConstraints) }
139
171
140
172
layout(layoutWidth, layoutHeight) {
141
- contentPlaceables.forEach { it.placeRelative(0 , topBarHeight) }
142
- topBarPlaceables.forEach { it.placeRelative(0 , 0 ) }
143
- actionPlaceables.forEach { it.placeRelative(0 , layoutHeight - actionHeight) }
144
- toastPlaceables.forEach { it.placeRelative(0 , topBarHeight) }
173
+ contentPlaceables.forEach { it.placeRelative(x = 0 , y = topBarHeight) }
174
+ topBarPlaceables.forEach { it.placeRelative(x = 0 , y = 0 ) }
175
+ actionPlaceables.forEach { it.placeRelative(x = 0 , y = layoutHeight - actionHeight) }
176
+ toastPlaceables.forEach { it.placeRelative(x = 0 , y = topBarHeight) }
145
177
}
146
178
}
147
179
}
@@ -154,7 +186,7 @@ private fun ScaffoldLayout(
154
186
public fun ScaffoldAction (
155
187
modifier : Modifier = Modifier ,
156
188
backgroundColor : Color = OrbitTheme .colors.surface.main,
157
- contentWindowInsets : WindowInsets = WindowInsets .systemBars.union( WindowInsets .ime) ,
189
+ contentWindowInsets : WindowInsets = WindowInsets .systemBars,
158
190
fadeHeight : Dp = DefaultActionFadeHeight ,
159
191
content : @Composable () -> Unit ,
160
192
) {
@@ -165,13 +197,13 @@ public fun ScaffoldAction(
165
197
endY = with (density) { fadeHeight.toPx() },
166
198
)
167
199
}
168
- val inset = contentWindowInsets.asPaddingValues()
169
200
Layout (
170
201
modifier = modifier.background(brush),
171
202
content = {
172
203
Box (Modifier .fillMaxWidth(), propagateMinConstraints = true ) {
173
204
content()
174
205
}
206
+ Spacer (Modifier .windowInsetsBottomHeight(contentWindowInsets))
175
207
},
176
208
) { measurables, constraints ->
177
209
val padding = 16 .dp.roundToPx()
@@ -188,21 +220,20 @@ public fun ScaffoldAction(
188
220
return @Layout layout(0 , 0 ) {}
189
221
}
190
222
223
+ val inset = measurables.last().measure(constraints.copy(minWidth = 0 , minHeight = 0 ))
191
224
val width = constraints.maxWidth
192
- val height = top + action.height + padding + inset.calculateBottomPadding().roundToPx()
225
+ val height = top + action.height + padding + inset.height
193
226
194
227
layout(width, height, alignmentLines = mapOf (ActionFadeLine to top)) {
195
228
action.place(x = (width - action.width) / 2 , y = top)
196
229
}
197
230
}
198
231
}
199
232
200
- private val ActionFadeLine = HorizontalAlignmentLine (::minOf)
201
-
202
- private const val SlotTopAppBar = 0
203
- private const val SlotAction = 1
204
- private const val SlotToast = 2
205
- private const val SlotContent = 3
233
+ /* *
234
+ * Models the height of the fading gradient - how much should be the content shifted under the action slot.
235
+ */
236
+ public val ActionFadeLine : HorizontalAlignmentLine = HorizontalAlignmentLine (::min)
206
237
207
238
/* *
208
239
* Action's top gradient currently decreased from 16.dp to minimize contentPadding
0 commit comments