Skip to content

Commit 645d2b3

Browse files
committed
rework Scaffold to avoid Subscompose & fix insets consumption
closes #500 closes #376
1 parent f28b3eb commit 645d2b3

File tree

1 file changed

+96
-65
lines changed
  • ui/src/androidMain/kotlin/kiwi/orbit/compose/ui/controls

1 file changed

+96
-65
lines changed

ui/src/androidMain/kotlin/kiwi/orbit/compose/ui/controls/Scaffold.kt

+96-65
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,43 @@ package kiwi.orbit.compose.ui.controls
22

33
import androidx.compose.foundation.background
44
import androidx.compose.foundation.layout.Box
5-
import androidx.compose.foundation.layout.ExperimentalLayoutApi
65
import androidx.compose.foundation.layout.PaddingValues
6+
import androidx.compose.foundation.layout.Spacer
77
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
118
import androidx.compose.foundation.layout.fillMaxSize
129
import androidx.compose.foundation.layout.fillMaxWidth
1310
import androidx.compose.foundation.layout.ime
14-
import androidx.compose.foundation.layout.isImeVisible
1511
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
1717
import androidx.compose.foundation.layout.wrapContentSize
1818
import androidx.compose.runtime.Composable
19+
import androidx.compose.runtime.getValue
20+
import androidx.compose.runtime.mutableStateOf
1921
import androidx.compose.runtime.remember
22+
import androidx.compose.runtime.setValue
2023
import androidx.compose.ui.Alignment
2124
import androidx.compose.ui.Modifier
2225
import androidx.compose.ui.graphics.Brush
2326
import androidx.compose.ui.graphics.Color
24-
import androidx.compose.ui.layout.AlignmentLine
2527
import androidx.compose.ui.layout.HorizontalAlignmentLine
2628
import androidx.compose.ui.layout.Layout
27-
import androidx.compose.ui.layout.SubcomposeLayout
2829
import androidx.compose.ui.platform.LocalDensity
30+
import androidx.compose.ui.unit.Constraints
2931
import androidx.compose.ui.unit.Dp
32+
import androidx.compose.ui.unit.LayoutDirection
3033
import androidx.compose.ui.unit.dp
3134
import androidx.compose.ui.unit.offset
3235
import kiwi.orbit.compose.ui.OrbitTheme
3336
import kiwi.orbit.compose.ui.controls.internal.CustomPlaceholder
3437
import kiwi.orbit.compose.ui.controls.internal.OrbitPreviews
3538
import kiwi.orbit.compose.ui.controls.internal.Preview
3639
import kiwi.orbit.compose.ui.foundation.contentColorFor
40+
import kotlin.math.min
41+
import kotlin.math.roundToInt
3742

3843
/**
3944
* Scaffold helps layouting basic screen widgets, such as [TopAppBar], [action] and the [content].
@@ -43,6 +48,8 @@ import kiwi.orbit.compose.ui.foundation.contentColorFor
4348
*
4449
* Utilize contentPadding passed to [content] lambda to handle IME, navigation or status bar paddings.
4550
* 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.
4653
*/
4754
@Composable
4855
public fun Scaffold(
@@ -59,7 +66,7 @@ public fun Scaffold(
5966
},
6067
toastHostState: ToastHostState = remember { ToastHostState() },
6168
toastHost: @Composable (ToastHostState) -> Unit = { ToastHost(it) },
62-
contentWindowInsets: WindowInsets = WindowInsets.systemBars.union(WindowInsets.ime),
69+
contentWindowInsets: WindowInsets = WindowInsets.ime,
6370
content: @Composable (contentPadding: PaddingValues) -> Unit,
6471
) {
6572
Surface(
@@ -77,7 +84,6 @@ public fun Scaffold(
7784
}
7885
}
7986

80-
@OptIn(ExperimentalLayoutApi::class)
8187
@Composable
8288
private fun ScaffoldLayout(
8389
topBar: @Composable () -> Unit,
@@ -86,62 +92,88 @@ private fun ScaffoldLayout(
8692
content: @Composable (contentPadding: PaddingValues) -> Unit,
8793
contentWindowInsets: WindowInsets,
8894
) {
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 ->
91131
val layoutWidth = constraints.maxWidth
92132
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) }
94143

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.
96145
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)
97149

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
102156
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
106157

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()
137167

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) }
139171

140172
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) }
145177
}
146178
}
147179
}
@@ -154,7 +186,7 @@ private fun ScaffoldLayout(
154186
public fun ScaffoldAction(
155187
modifier: Modifier = Modifier,
156188
backgroundColor: Color = OrbitTheme.colors.surface.main,
157-
contentWindowInsets: WindowInsets = WindowInsets.systemBars.union(WindowInsets.ime),
189+
contentWindowInsets: WindowInsets = WindowInsets.systemBars,
158190
fadeHeight: Dp = DefaultActionFadeHeight,
159191
content: @Composable () -> Unit,
160192
) {
@@ -165,13 +197,13 @@ public fun ScaffoldAction(
165197
endY = with(density) { fadeHeight.toPx() },
166198
)
167199
}
168-
val inset = contentWindowInsets.asPaddingValues()
169200
Layout(
170201
modifier = modifier.background(brush),
171202
content = {
172203
Box(Modifier.fillMaxWidth(), propagateMinConstraints = true) {
173204
content()
174205
}
206+
Spacer(Modifier.windowInsetsBottomHeight(contentWindowInsets))
175207
},
176208
) { measurables, constraints ->
177209
val padding = 16.dp.roundToPx()
@@ -188,21 +220,20 @@ public fun ScaffoldAction(
188220
return@Layout layout(0, 0) {}
189221
}
190222

223+
val inset = measurables.last().measure(constraints.copy(minWidth = 0, minHeight = 0))
191224
val width = constraints.maxWidth
192-
val height = top + action.height + padding + inset.calculateBottomPadding().roundToPx()
225+
val height = top + action.height + padding + inset.height
193226

194227
layout(width, height, alignmentLines = mapOf(ActionFadeLine to top)) {
195228
action.place(x = (width - action.width) / 2, y = top)
196229
}
197230
}
198231
}
199232

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)
206237

207238
/**
208239
* Action's top gradient currently decreased from 16.dp to minimize contentPadding

0 commit comments

Comments
 (0)