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