1 /*
<lambda>null2 * Copyright 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.foundation.layout
18
19 import androidx.collection.MutableScatterMap
20 import androidx.compose.runtime.Composable
21 import androidx.compose.runtime.Immutable
22 import androidx.compose.runtime.Stable
23 import androidx.compose.runtime.remember
24 import androidx.compose.ui.Alignment
25 import androidx.compose.ui.Modifier
26 import androidx.compose.ui.layout.Layout
27 import androidx.compose.ui.layout.Measurable
28 import androidx.compose.ui.layout.MeasurePolicy
29 import androidx.compose.ui.layout.MeasureResult
30 import androidx.compose.ui.layout.MeasureScope
31 import androidx.compose.ui.layout.Placeable
32 import androidx.compose.ui.node.ModifierNodeElement
33 import androidx.compose.ui.node.ParentDataModifierNode
34 import androidx.compose.ui.platform.InspectorInfo
35 import androidx.compose.ui.platform.debugInspectorInfo
36 import androidx.compose.ui.unit.Constraints
37 import androidx.compose.ui.unit.Density
38 import androidx.compose.ui.unit.IntSize
39 import androidx.compose.ui.unit.LayoutDirection
40 import androidx.compose.ui.util.fastForEachIndexed
41 import kotlin.math.max
42
43 /**
44 * A layout composable with [content]. The [Box] will size itself to fit the content, subject to the
45 * incoming constraints. When children are smaller than the parent, by default they will be
46 * positioned inside the [Box] according to the [contentAlignment]. For individually specifying the
47 * alignments of the children layouts, use the [BoxScope.align] modifier. By default, the content
48 * will be measured without the [Box]'s incoming min constraints, unless [propagateMinConstraints]
49 * is `true`. As an example, setting [propagateMinConstraints] to `true` can be useful when the
50 * [Box] has content on which modifiers cannot be specified directly and setting a min size on the
51 * content of the [Box] is needed. If [propagateMinConstraints] is set to `true`, the min size set
52 * on the [Box] will also be applied to the content, whereas otherwise the min size will only apply
53 * to the [Box]. When the content has more than one layout child the layout children will be stacked
54 * one on top of the other (positioned as explained above) in the composition order.
55 *
56 * Example usage:
57 *
58 * @sample androidx.compose.foundation.layout.samples.SimpleBox
59 * @param modifier The modifier to be applied to the layout.
60 * @param contentAlignment The default alignment inside the Box.
61 * @param propagateMinConstraints Whether the incoming min constraints should be passed to content.
62 * @param content The content of the [Box].
63 */
64 @Composable
65 inline fun Box(
66 modifier: Modifier = Modifier,
67 contentAlignment: Alignment = Alignment.TopStart,
68 propagateMinConstraints: Boolean = false,
69 content: @Composable BoxScope.() -> Unit
70 ) {
71 val measurePolicy = maybeCachedBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
72 Layout(
73 content = { BoxScopeInstance.content() },
74 measurePolicy = measurePolicy,
75 modifier = modifier
76 )
77 }
78
cacheFornull79 private fun cacheFor(propagate: Boolean) =
80 MutableScatterMap<Alignment, MeasurePolicy>(9).apply {
81 this[Alignment.TopStart] = BoxMeasurePolicy(Alignment.TopStart, propagate)
82 this[Alignment.TopCenter] = BoxMeasurePolicy(Alignment.TopCenter, propagate)
83 this[Alignment.TopEnd] = BoxMeasurePolicy(Alignment.TopEnd, propagate)
84 this[Alignment.CenterStart] = BoxMeasurePolicy(Alignment.CenterStart, propagate)
85 this[Alignment.Center] = BoxMeasurePolicy(Alignment.Center, propagate)
86 this[Alignment.CenterEnd] = BoxMeasurePolicy(Alignment.CenterEnd, propagate)
87 this[Alignment.BottomStart] = BoxMeasurePolicy(Alignment.BottomStart, propagate)
88 this[Alignment.BottomCenter] = BoxMeasurePolicy(Alignment.BottomCenter, propagate)
89 this[Alignment.BottomEnd] = BoxMeasurePolicy(Alignment.BottomEnd, propagate)
90 }
91
92 private val Cache1 = cacheFor(true)
93 private val Cache2 = cacheFor(false)
94
95 @PublishedApi
maybeCachedBoxMeasurePolicynull96 internal fun maybeCachedBoxMeasurePolicy(
97 alignment: Alignment,
98 propagateMinConstraints: Boolean
99 ): MeasurePolicy {
100 val cache = if (propagateMinConstraints) Cache1 else Cache2
101 return cache[alignment] ?: BoxMeasurePolicy(alignment, propagateMinConstraints)
102 }
103
104 @PublishedApi
105 @Composable
rememberBoxMeasurePolicynull106 internal fun rememberBoxMeasurePolicy(
107 alignment: Alignment,
108 propagateMinConstraints: Boolean
109 ): MeasurePolicy =
110 if (alignment == Alignment.TopStart && !propagateMinConstraints) {
111 DefaultBoxMeasurePolicy
112 } else {
<lambda>null113 remember(alignment, propagateMinConstraints) {
114 BoxMeasurePolicy(alignment, propagateMinConstraints)
115 }
116 }
117
118 private val DefaultBoxMeasurePolicy: MeasurePolicy = BoxMeasurePolicy(Alignment.TopStart, false)
119
120 private data class BoxMeasurePolicy(
121 private val alignment: Alignment,
122 private val propagateMinConstraints: Boolean
123 ) : MeasurePolicy {
measurenull124 override fun MeasureScope.measure(
125 measurables: List<Measurable>,
126 constraints: Constraints
127 ): MeasureResult {
128 if (measurables.isEmpty()) {
129 return layout(constraints.minWidth, constraints.minHeight) {}
130 }
131
132 val contentConstraints =
133 if (propagateMinConstraints) {
134 constraints
135 } else {
136 constraints.copyMaxDimensions()
137 }
138
139 if (measurables.size == 1) {
140 val measurable = measurables[0]
141 val boxWidth: Int
142 val boxHeight: Int
143 val placeable: Placeable
144 if (!measurable.matchesParentSize) {
145 placeable = measurable.measure(contentConstraints)
146 boxWidth = max(constraints.minWidth, placeable.width)
147 boxHeight = max(constraints.minHeight, placeable.height)
148 } else {
149 boxWidth = constraints.minWidth
150 boxHeight = constraints.minHeight
151 placeable =
152 measurable.measure(
153 Constraints.fixed(constraints.minWidth, constraints.minHeight)
154 )
155 }
156 return layout(boxWidth, boxHeight) {
157 placeInBox(placeable, measurable, layoutDirection, boxWidth, boxHeight, alignment)
158 }
159 }
160
161 val placeables = arrayOfNulls<Placeable>(measurables.size)
162 // First measure non match parent size children to get the size of the Box.
163 var hasMatchParentSizeChildren = false
164 var boxWidth = constraints.minWidth
165 var boxHeight = constraints.minHeight
166 measurables.fastForEachIndexed { index, measurable ->
167 if (!measurable.matchesParentSize) {
168 val placeable = measurable.measure(contentConstraints)
169 placeables[index] = placeable
170 boxWidth = max(boxWidth, placeable.width)
171 boxHeight = max(boxHeight, placeable.height)
172 } else {
173 hasMatchParentSizeChildren = true
174 }
175 }
176
177 // Now measure match parent size children, if any.
178 if (hasMatchParentSizeChildren) {
179 // The infinity check is needed for default intrinsic measurements.
180 val matchParentSizeConstraints =
181 Constraints(
182 minWidth = if (boxWidth != Constraints.Infinity) boxWidth else 0,
183 minHeight = if (boxHeight != Constraints.Infinity) boxHeight else 0,
184 maxWidth = boxWidth,
185 maxHeight = boxHeight
186 )
187 measurables.fastForEachIndexed { index, measurable ->
188 if (measurable.matchesParentSize) {
189 placeables[index] = measurable.measure(matchParentSizeConstraints)
190 }
191 }
192 }
193
194 // Specify the size of the Box and position its children.
195 return layout(boxWidth, boxHeight) {
196 placeables.forEachIndexed { index, placeable ->
197 placeable as Placeable
198 val measurable = measurables[index]
199 placeInBox(placeable, measurable, layoutDirection, boxWidth, boxHeight, alignment)
200 }
201 }
202 }
203 }
204
placeInBoxnull205 private fun Placeable.PlacementScope.placeInBox(
206 placeable: Placeable,
207 measurable: Measurable,
208 layoutDirection: LayoutDirection,
209 boxWidth: Int,
210 boxHeight: Int,
211 alignment: Alignment
212 ) {
213 val childAlignment = measurable.boxChildDataNode?.alignment ?: alignment
214 val position =
215 childAlignment.align(
216 IntSize(placeable.width, placeable.height),
217 IntSize(boxWidth, boxHeight),
218 layoutDirection
219 )
220 placeable.place(position)
221 }
222
223 /**
224 * A box with no content that can participate in layout, drawing, pointer input due to the
225 * [modifier] applied to it.
226 *
227 * Example usage:
228 *
229 * @sample androidx.compose.foundation.layout.samples.SimpleBox
230 * @param modifier The modifier to be applied to the layout.
231 */
232 @Composable
Boxnull233 fun Box(modifier: Modifier) {
234 Layout(measurePolicy = EmptyBoxMeasurePolicy, modifier = modifier)
235 }
236
constraintsnull237 internal val EmptyBoxMeasurePolicy = MeasurePolicy { _, constraints ->
238 layout(constraints.minWidth, constraints.minHeight) {}
239 }
240
241 /** A BoxScope provides a scope for the children of [Box] and [BoxWithConstraints]. */
242 @LayoutScopeMarker
243 @Immutable
244 interface BoxScope {
245 /**
246 * Pull the content element to a specific [Alignment] within the [Box]. This alignment will have
247 * priority over the [Box]'s `alignment` parameter.
248 */
alignnull249 @Stable fun Modifier.align(alignment: Alignment): Modifier
250
251 /**
252 * Size the element to match the size of the [Box] after all other content elements have been
253 * measured.
254 *
255 * The element using this modifier does not take part in defining the size of the [Box].
256 * Instead, it matches the size of the [Box] after all other children (not using
257 * matchParentSize() modifier) have been measured to obtain the [Box]'s size. In contrast, a
258 * general-purpose [Modifier.fillMaxSize] modifier, which makes an element occupy all available
259 * space, will take part in defining the size of the [Box]. Consequently, using it for an
260 * element inside a [Box] will make the [Box] itself always fill the available space.
261 */
262 @Stable fun Modifier.matchParentSize(): Modifier
263 }
264
265 internal object BoxScopeInstance : BoxScope {
266 @Stable
267 override fun Modifier.align(alignment: Alignment) =
268 this.then(
269 BoxChildDataElement(
270 alignment = alignment,
271 matchParentSize = false,
272 inspectorInfo =
273 debugInspectorInfo {
274 name = "align"
275 value = alignment
276 }
277 )
278 )
279
280 @Stable
281 override fun Modifier.matchParentSize() =
282 this.then(
283 BoxChildDataElement(
284 alignment = Alignment.Center,
285 matchParentSize = true,
286 inspectorInfo = debugInspectorInfo { name = "matchParentSize" }
287 )
288 )
289 }
290
291 private val Measurable.boxChildDataNode: BoxChildDataNode?
292 get() = parentData as? BoxChildDataNode
293 private val Measurable.matchesParentSize: Boolean
294 get() = boxChildDataNode?.matchParentSize ?: false
295
296 private class BoxChildDataElement(
297 val alignment: Alignment,
298 val matchParentSize: Boolean,
299 val inspectorInfo: InspectorInfo.() -> Unit
300 ) : ModifierNodeElement<BoxChildDataNode>() {
createnull301 override fun create(): BoxChildDataNode {
302 return BoxChildDataNode(alignment, matchParentSize)
303 }
304
updatenull305 override fun update(node: BoxChildDataNode) {
306 node.alignment = alignment
307 node.matchParentSize = matchParentSize
308 }
309
inspectablePropertiesnull310 override fun InspectorInfo.inspectableProperties() {
311 inspectorInfo()
312 }
313
hashCodenull314 override fun hashCode(): Int {
315 var result = alignment.hashCode()
316 result = 31 * result + matchParentSize.hashCode()
317 return result
318 }
319
equalsnull320 override fun equals(other: Any?): Boolean {
321 if (this === other) return true
322 val otherModifier = other as? BoxChildDataElement ?: return false
323 return alignment == otherModifier.alignment &&
324 matchParentSize == otherModifier.matchParentSize
325 }
326 }
327
328 private class BoxChildDataNode(
329 var alignment: Alignment,
330 var matchParentSize: Boolean,
331 ) : ParentDataModifierNode, Modifier.Node() {
modifyParentDatanull332 override fun Density.modifyParentData(parentData: Any?) = this@BoxChildDataNode
333 }
334