1 /*
<lambda>null2 * Copyright 2023 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 @file:Suppress("DEPRECATION")
18
19 package androidx.compose.foundation.layout
20
21 import androidx.annotation.FloatRange
22 import androidx.collection.IntIntPair
23 import androidx.collection.mutableIntListOf
24 import androidx.collection.mutableIntObjectMapOf
25 import androidx.compose.foundation.layout.internal.requirePrecondition
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.Stable
28 import androidx.compose.runtime.collection.MutableVector
29 import androidx.compose.runtime.collection.mutableVectorOf
30 import androidx.compose.runtime.remember
31 import androidx.compose.ui.Alignment
32 import androidx.compose.ui.Modifier
33 import androidx.compose.ui.layout.IntrinsicMeasurable
34 import androidx.compose.ui.layout.IntrinsicMeasureScope
35 import androidx.compose.ui.layout.Layout
36 import androidx.compose.ui.layout.Measurable
37 import androidx.compose.ui.layout.MeasurePolicy
38 import androidx.compose.ui.layout.MeasureResult
39 import androidx.compose.ui.layout.MeasureScope
40 import androidx.compose.ui.layout.MultiContentMeasurePolicy
41 import androidx.compose.ui.layout.Placeable
42 import androidx.compose.ui.node.ModifierNodeElement
43 import androidx.compose.ui.node.ParentDataModifierNode
44 import androidx.compose.ui.platform.InspectorInfo
45 import androidx.compose.ui.unit.Constraints
46 import androidx.compose.ui.unit.Density
47 import androidx.compose.ui.unit.Dp
48 import androidx.compose.ui.unit.LayoutDirection
49 import androidx.compose.ui.util.fastCoerceAtLeast
50 import androidx.compose.ui.util.fastCoerceIn
51 import androidx.compose.ui.util.fastForEachIndexed
52 import kotlin.math.ceil
53 import kotlin.math.max
54 import kotlin.math.min
55
56 /**
57 * [FlowRow] is a layout that fills items from left to right (ltr) in LTR layouts or right to left
58 * (rtl) in RTL layouts and when it runs out of space, moves to the next "row" or "line" positioned
59 * on the bottom, and then continues filling items until the items run out.
60 *
61 * Example:
62 *
63 * @sample androidx.compose.foundation.layout.samples.SimpleFlowRow
64 *
65 * When a Modifier [RowScope.weight] is provided, it scales the item based on the number items that
66 * fall on the row it was placed in.
67 *
68 * Note that if two or more Text components are placed in a [Row], normally they should be aligned
69 * by their first baselines. [FlowRow] as a general purpose container does not do it automatically
70 * so developers need to handle this manually. This is achieved by adding a
71 * [RowScope.alignByBaseline] modifier to every such Text component. By default this modifier aligns
72 * by [androidx.compose.ui.layout.FirstBaseline]. If, however, you need to align Texts by
73 * [androidx.compose.ui.layout.LastBaseline] for example, use a more general [RowScope.alignBy]
74 * modifier.
75 *
76 * @param modifier The modifier to be applied to the Row.
77 * @param horizontalArrangement The horizontal arrangement of the layout's children.
78 * @param verticalArrangement The vertical arrangement of the layout's virtual rows.
79 * @param itemVerticalAlignment The cross axis/vertical alignment of an item in the column.
80 * @param maxItemsInEachRow The maximum number of items per row
81 * @param maxLines The max number of rows
82 * @param overflow The strategy to handle overflowing items
83 * @param content The content as a [RowScope]
84 * @see FlowColumn
85 * @see [androidx.compose.foundation.layout.Row]
86 */
87 @Deprecated("The overflow parameter has been deprecated")
88 @Composable
89 @ExperimentalLayoutApi
90 fun FlowRow(
91 modifier: Modifier = Modifier,
92 horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
93 verticalArrangement: Arrangement.Vertical = Arrangement.Top,
94 itemVerticalAlignment: Alignment.Vertical = Alignment.Top,
95 maxItemsInEachRow: Int = Int.MAX_VALUE,
96 maxLines: Int = Int.MAX_VALUE,
97 overflow: FlowRowOverflow = FlowRowOverflow.Clip,
98 content: @Composable FlowRowScope.() -> Unit
99 ) {
100 val overflowState = remember(overflow) { overflow.createOverflowState() }
101 val measurePolicy =
102 rowMeasurementMultiContentHelper(
103 horizontalArrangement,
104 verticalArrangement,
105 itemVerticalAlignment,
106 maxItemsInEachRow,
107 maxLines,
108 overflowState
109 )
110 val list: List<@Composable () -> Unit> =
111 remember(overflow, content, maxLines) {
112 val mutableList: MutableList<@Composable () -> Unit> = mutableListOf()
113 mutableList.add { FlowRowScopeInstance.content() }
114 overflow.addOverflowComposables(overflowState, mutableList)
115 mutableList
116 }
117
118 Layout(contents = list, measurePolicy = measurePolicy, modifier = modifier)
119 }
120
121 /**
122 * [FlowRow] is a layout that fills items from left to right (ltr) in LTR layouts or right to left
123 * (rtl) in RTL layouts and when it runs out of space, moves to the next "row" or "line" positioned
124 * on the bottom, and then continues filling items until the items run out.
125 *
126 * Example:
127 *
128 * @sample androidx.compose.foundation.layout.samples.SimpleFlowRow
129 *
130 * When a Modifier [RowScope.weight] is provided, it scales the item based on the number items that
131 * fall on the row it was placed in.
132 *
133 * Note that if two or more Text components are placed in a [Row], normally they should be aligned
134 * by their first baselines. [FlowRow] as a general purpose container does not do it automatically
135 * so developers need to handle this manually. This is achieved by adding a
136 * [RowScope.alignByBaseline] modifier to every such Text component. By default this modifier aligns
137 * by [androidx.compose.ui.layout.FirstBaseline]. If, however, you need to align Texts by
138 * [androidx.compose.ui.layout.LastBaseline] for example, use a more general [RowScope.alignBy]
139 * modifier.
140 *
141 * @param modifier The modifier to be applied to the Row.
142 * @param horizontalArrangement The horizontal arrangement of the layout's children.
143 * @param verticalArrangement The vertical arrangement of the layout's virtual rows.
144 * @param itemVerticalAlignment The cross axis/vertical alignment of an item in the column.
145 * @param maxItemsInEachRow The maximum number of items per row
146 * @param maxLines The max number of rows
147 * @param content The content as a [RowScope]
148 * @see FlowColumn
149 * @see [androidx.compose.foundation.layout.Row]
150 */
151 @OptIn(ExperimentalLayoutApi::class)
152 @Composable
FlowRownull153 fun FlowRow(
154 modifier: Modifier = Modifier,
155 horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
156 verticalArrangement: Arrangement.Vertical = Arrangement.Top,
157 itemVerticalAlignment: Alignment.Vertical = Alignment.Top,
158 maxItemsInEachRow: Int = Int.MAX_VALUE,
159 maxLines: Int = Int.MAX_VALUE,
160 content: @Composable FlowRowScope.() -> Unit
161 ) =
162 FlowRow(
163 modifier,
164 horizontalArrangement,
165 verticalArrangement,
166 itemVerticalAlignment,
167 maxItemsInEachRow,
168 maxLines,
169 FlowRowOverflow.Clip,
170 content,
171 )
172
173 /**
174 * [FlowColumn] is a layout that fills items from top to bottom, and when it runs out of space on
175 * the bottom, moves to the next "column" or "line" on the right or left based on ltr or rtl
176 * layouts, and then continues filling items from top to bottom.
177 *
178 * It supports ltr in LTR layouts, by placing the first column to the left, and then moving to the
179 * right It supports rtl in RTL layouts, by placing the first column to the right, and then moving
180 * to the left
181 *
182 * Example:
183 *
184 * @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn
185 *
186 * When a Modifier [ColumnScope.weight] is provided, it scales the item based on the number items
187 * that fall on the column it was placed in.
188 *
189 * @param modifier The modifier to be applied to the Row.
190 * @param verticalArrangement The vertical arrangement of the layout's children.
191 * @param horizontalArrangement The horizontal arrangement of the layout's virtual columns
192 * @param itemHorizontalAlignment The cross axis/horizontal alignment of an item in the column.
193 * @param maxItemsInEachColumn The maximum number of items per column
194 * @param maxLines The max number of rows
195 * @param overflow The strategy to handle overflowing items
196 * @param content The content as a [ColumnScope]
197 * @see FlowRow
198 * @see ContextualFlowColumn
199 * @see [androidx.compose.foundation.layout.Column]
200 */
201 @Deprecated("The overflow parameter has been deprecated")
202 @Composable
203 @ExperimentalLayoutApi
204 fun FlowColumn(
205 modifier: Modifier = Modifier,
206 verticalArrangement: Arrangement.Vertical = Arrangement.Top,
207 horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
208 itemHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
209 maxItemsInEachColumn: Int = Int.MAX_VALUE,
210 maxLines: Int = Int.MAX_VALUE,
211 overflow: FlowColumnOverflow = FlowColumnOverflow.Clip,
212 content: @Composable FlowColumnScope.() -> Unit
213 ) {
214 val overflowState = remember(overflow) { overflow.createOverflowState() }
215 val measurePolicy =
216 columnMeasurementMultiContentHelper(
217 verticalArrangement,
218 horizontalArrangement,
219 itemHorizontalAlignment,
220 maxItemsInEachColumn,
221 maxLines,
222 overflowState
223 )
224 val list: List<@Composable () -> Unit> =
225 remember(overflow, content, maxLines) {
226 val mutableList: MutableList<@Composable () -> Unit> = mutableListOf()
227 mutableList.add { FlowColumnScopeInstance.content() }
228 overflow.addOverflowComposables(overflowState, mutableList)
229 mutableList
230 }
231 Layout(contents = list, measurePolicy = measurePolicy, modifier = modifier)
232 }
233
234 /**
235 * [FlowColumn] is a layout that fills items from top to bottom, and when it runs out of space on
236 * the bottom, moves to the next "column" or "line" on the right or left based on ltr or rtl
237 * layouts, and then continues filling items from top to bottom.
238 *
239 * It supports ltr in LTR layouts, by placing the first column to the left, and then moving to the
240 * right It supports rtl in RTL layouts, by placing the first column to the right, and then moving
241 * to the left
242 *
243 * Example:
244 *
245 * @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn
246 *
247 * When a Modifier [ColumnScope.weight] is provided, it scales the item based on the number items
248 * that fall on the column it was placed in.
249 *
250 * @param modifier The modifier to be applied to the Row.
251 * @param verticalArrangement The vertical arrangement of the layout's children.
252 * @param horizontalArrangement The horizontal arrangement of the layout's virtual columns
253 * @param itemHorizontalAlignment The cross axis/horizontal alignment of an item in the column.
254 * @param maxItemsInEachColumn The maximum number of items per column
255 * @param maxLines The max number of rows
256 * @param content The content as a [ColumnScope]
257 * @see FlowRow
258 * @see [androidx.compose.foundation.layout.Column]
259 */
260 @OptIn(ExperimentalLayoutApi::class)
261 @Composable
FlowColumnnull262 fun FlowColumn(
263 modifier: Modifier = Modifier,
264 verticalArrangement: Arrangement.Vertical = Arrangement.Top,
265 horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
266 itemHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
267 maxItemsInEachColumn: Int = Int.MAX_VALUE,
268 maxLines: Int = Int.MAX_VALUE,
269 content: @Composable FlowColumnScope.() -> Unit
270 ) =
271 FlowColumn(
272 modifier,
273 verticalArrangement,
274 horizontalArrangement,
275 itemHorizontalAlignment,
276 maxItemsInEachColumn,
277 maxLines,
278 FlowColumnOverflow.Clip,
279 content,
280 )
281
282 /** Scope for the children of [FlowRow]. */
283 @LayoutScopeMarker
284 @Stable
285 interface FlowRowScope : RowScope {
286 /**
287 * Have the item fill (possibly only partially) the max height of the tallest item in the row it
288 * was placed in, within the [FlowRow].
289 *
290 * @param fraction The fraction of the max height of the tallest item between `0` and `1`,
291 * inclusive.
292 *
293 * Example usage:
294 *
295 * @sample androidx.compose.foundation.layout.samples.SimpleFlowRow_EqualHeight
296 */
297 @ExperimentalLayoutApi
298 fun Modifier.fillMaxRowHeight(
299 @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f,
300 ): Modifier
301 }
302
303 /** Scope for the overflow [FlowRow]. */
304 @LayoutScopeMarker
305 @Stable
306 @ExperimentalLayoutApi
307 interface FlowRowOverflowScope : FlowRowScope {
308 /**
309 * Total Number of Items available to show in [FlowRow] This includes items that may not be
310 * displayed.
311 *
312 * In [ContextualFlowRow], this matches the [ContextualFlowRow]'s `itemCount` parameter
313 */
314 @ExperimentalLayoutApi val totalItemCount: Int
315
316 /** Total Number of Items displayed in the [FlowRow] */
317 @ExperimentalLayoutApi val shownItemCount: Int
318 }
319
320 /** Scope for the children of [FlowColumn]. */
321 @LayoutScopeMarker
322 @Stable
323 interface FlowColumnScope : ColumnScope {
324 /**
325 * Have the item fill (possibly only partially) the max width of the widest item in the column
326 * it was placed in, within the [FlowColumn].
327 *
328 * @param fraction The fraction of the max width of the widest item between `0` and `1`,
329 * inclusive.
330 *
331 * Example usage:
332 *
333 * @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn_EqualWidth
334 */
335 @ExperimentalLayoutApi
fillMaxColumnWidthnull336 fun Modifier.fillMaxColumnWidth(
337 @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f,
338 ): Modifier
339 }
340
341 /** Scope for the overflow [FlowColumn]. */
342 @LayoutScopeMarker
343 @Stable
344 @ExperimentalLayoutApi
345 interface FlowColumnOverflowScope : FlowColumnScope {
346 /**
347 * Total Number of Items available to show in [FlowColumn] This includes items that may not be
348 * displayed.
349 *
350 * In [ContextualFlowColumn], this matches the [ContextualFlowColumn]'s `itemCount` parameter
351 */
352 @ExperimentalLayoutApi val totalItemCount: Int
353
354 /** Total Number of Items displayed in the [FlowColumn] */
355 @ExperimentalLayoutApi val shownItemCount: Int
356 }
357
358 @OptIn(ExperimentalLayoutApi::class)
359 internal object FlowRowScopeInstance : RowScope by RowScopeInstance, FlowRowScope {
fillMaxRowHeightnull360 override fun Modifier.fillMaxRowHeight(fraction: Float): Modifier {
361 requirePrecondition(fraction >= 0.0f && fraction <= 1.0f) {
362 "invalid fraction $fraction; must be >= 0 and <= 1.0"
363 }
364 return this.then(
365 FillCrossAxisSizeElement(
366 fraction = fraction,
367 )
368 )
369 }
370 }
371
372 @OptIn(ExperimentalLayoutApi::class)
373 internal class FlowRowOverflowScopeImpl(private val state: FlowLayoutOverflowState) :
374 FlowRowScope by FlowRowScopeInstance, FlowRowOverflowScope {
<lambda>null375 override val totalItemCount: Int by lazyInt { state.itemCount }
376
<lambda>null377 override val shownItemCount: Int by lazyInt(state.shownItemLazyErrorMessage) { state.itemShown }
378 }
379
380 @OptIn(ExperimentalLayoutApi::class)
381 internal class FlowColumnOverflowScopeImpl(private val state: FlowLayoutOverflowState) :
382 FlowColumnScope by FlowColumnScopeInstance, FlowColumnOverflowScope {
<lambda>null383 override val totalItemCount: Int by lazyInt { state.itemCount }
384
<lambda>null385 override val shownItemCount: Int by lazyInt(state.shownItemLazyErrorMessage) { state.itemShown }
386 }
387
388 @OptIn(ExperimentalLayoutApi::class)
389 internal object FlowColumnScopeInstance : ColumnScope by ColumnScopeInstance, FlowColumnScope {
fillMaxColumnWidthnull390 override fun Modifier.fillMaxColumnWidth(fraction: Float): Modifier {
391 requirePrecondition(fraction >= 0.0f && fraction <= 1.0f) {
392 "invalid fraction $fraction; must be >= 0 and <= 1.0"
393 }
394 return this.then(
395 FillCrossAxisSizeElement(
396 fraction = fraction,
397 )
398 )
399 }
400 }
401
402 internal data class FlowLayoutData(var fillCrossAxisFraction: Float)
403
404 internal class FillCrossAxisSizeNode(
405 var fraction: Float,
406 ) : ParentDataModifierNode, Modifier.Node() {
modifyParentDatanull407 override fun Density.modifyParentData(parentData: Any?) =
408 ((parentData as? RowColumnParentData) ?: RowColumnParentData()).also {
409 it.flowLayoutData = it.flowLayoutData ?: FlowLayoutData(fraction)
410 it.flowLayoutData!!.fillCrossAxisFraction = fraction
411 }
412 }
413
414 internal class FillCrossAxisSizeElement(val fraction: Float) :
415 ModifierNodeElement<FillCrossAxisSizeNode>() {
createnull416 override fun create(): FillCrossAxisSizeNode {
417 return FillCrossAxisSizeNode(fraction)
418 }
419
updatenull420 override fun update(node: FillCrossAxisSizeNode) {
421 node.fraction = fraction
422 }
423
inspectablePropertiesnull424 override fun InspectorInfo.inspectableProperties() {
425 name = "fraction"
426 value = fraction
427 properties["fraction"] = fraction
428 }
429
hashCodenull430 override fun hashCode(): Int {
431 var result = fraction.hashCode()
432 result *= 31
433 return result
434 }
435
equalsnull436 override fun equals(other: Any?): Boolean {
437 if (this === other) return true
438 val otherModifier = other as? FillCrossAxisSizeNode ?: return false
439 return fraction == otherModifier.fraction
440 }
441 }
442
443 @OptIn(ExperimentalLayoutApi::class)
444 @PublishedApi
445 @Composable
rowMeasurementHelpernull446 internal fun rowMeasurementHelper(
447 horizontalArrangement: Arrangement.Horizontal,
448 verticalArrangement: Arrangement.Vertical,
449 maxItemsInMainAxis: Int,
450 ): MeasurePolicy {
451 return remember(
452 horizontalArrangement,
453 verticalArrangement,
454 maxItemsInMainAxis,
455 ) {
456 val measurePolicy =
457 FlowMeasurePolicy(
458 isHorizontal = true,
459 horizontalArrangement = horizontalArrangement,
460 mainAxisSpacing = horizontalArrangement.spacing,
461 crossAxisAlignment = CROSS_AXIS_ALIGNMENT_TOP,
462 verticalArrangement = verticalArrangement,
463 crossAxisArrangementSpacing = verticalArrangement.spacing,
464 maxItemsInMainAxis = maxItemsInMainAxis,
465 maxLines = Int.MAX_VALUE,
466 overflow = FlowRowOverflow.Visible.createOverflowState()
467 )
468 as MultiContentMeasurePolicy
469
470 MeasurePolicy { measurables, constraints ->
471 with(measurePolicy) { this@MeasurePolicy.measure(listOf(measurables), constraints) }
472 }
473 }
474 }
475
476 @OptIn(ExperimentalLayoutApi::class)
477 @Composable
rowMeasurementMultiContentHelpernull478 internal fun rowMeasurementMultiContentHelper(
479 horizontalArrangement: Arrangement.Horizontal,
480 verticalArrangement: Arrangement.Vertical,
481 itemVerticalAlignment: Alignment.Vertical,
482 maxItemsInMainAxis: Int,
483 maxLines: Int,
484 overflowState: FlowLayoutOverflowState,
485 ): MultiContentMeasurePolicy {
486 return remember(
487 horizontalArrangement,
488 verticalArrangement,
489 itemVerticalAlignment,
490 maxItemsInMainAxis,
491 maxLines,
492 overflowState
493 ) {
494 FlowMeasurePolicy(
495 isHorizontal = true,
496 horizontalArrangement = horizontalArrangement,
497 mainAxisSpacing = horizontalArrangement.spacing,
498 crossAxisAlignment = CrossAxisAlignment.vertical(itemVerticalAlignment),
499 verticalArrangement = verticalArrangement,
500 crossAxisArrangementSpacing = verticalArrangement.spacing,
501 maxItemsInMainAxis = maxItemsInMainAxis,
502 maxLines = maxLines,
503 overflow = overflowState
504 )
505 }
506 }
507
508 @OptIn(ExperimentalLayoutApi::class)
509 @PublishedApi
510 @Composable
columnMeasurementHelpernull511 internal fun columnMeasurementHelper(
512 verticalArrangement: Arrangement.Vertical,
513 horizontalArrangement: Arrangement.Horizontal,
514 maxItemsInMainAxis: Int,
515 ): MeasurePolicy {
516 return remember(
517 verticalArrangement,
518 horizontalArrangement,
519 maxItemsInMainAxis,
520 ) {
521 val measurePolicy =
522 FlowMeasurePolicy(
523 isHorizontal = false,
524 verticalArrangement = verticalArrangement,
525 mainAxisSpacing = verticalArrangement.spacing,
526 crossAxisAlignment = CROSS_AXIS_ALIGNMENT_START,
527 horizontalArrangement = horizontalArrangement,
528 crossAxisArrangementSpacing = horizontalArrangement.spacing,
529 maxItemsInMainAxis = maxItemsInMainAxis,
530 maxLines = Int.MAX_VALUE,
531 overflow = FlowRowOverflow.Visible.createOverflowState()
532 )
533 MeasurePolicy { measurables, constraints ->
534 with(measurePolicy) { this@MeasurePolicy.measure(listOf(measurables), constraints) }
535 }
536 }
537 }
538
539 @Composable
columnMeasurementMultiContentHelpernull540 internal fun columnMeasurementMultiContentHelper(
541 verticalArrangement: Arrangement.Vertical,
542 horizontalArrangement: Arrangement.Horizontal,
543 itemHorizontalAlignment: Alignment.Horizontal,
544 maxItemsInMainAxis: Int,
545 maxLines: Int,
546 overflowState: FlowLayoutOverflowState
547 ): MultiContentMeasurePolicy {
548 return remember(
549 verticalArrangement,
550 horizontalArrangement,
551 itemHorizontalAlignment,
552 maxItemsInMainAxis,
553 maxLines,
554 overflowState
555 ) {
556 FlowMeasurePolicy(
557 isHorizontal = false,
558 verticalArrangement = verticalArrangement,
559 mainAxisSpacing = verticalArrangement.spacing,
560 crossAxisAlignment = CrossAxisAlignment.horizontal(itemHorizontalAlignment),
561 horizontalArrangement = horizontalArrangement,
562 crossAxisArrangementSpacing = horizontalArrangement.spacing,
563 maxItemsInMainAxis = maxItemsInMainAxis,
564 maxLines = maxLines,
565 overflow = overflowState
566 )
567 }
568 }
569
570 internal interface FlowLineMeasurePolicy : RowColumnMeasurePolicy {
571 val isHorizontal: Boolean
572 val horizontalArrangement: Arrangement.Horizontal
573 val verticalArrangement: Arrangement.Vertical
574 val crossAxisAlignment: CrossAxisAlignment
575
mainAxisSizenull576 override fun Placeable.mainAxisSize() = if (isHorizontal) measuredWidth else measuredHeight
577
578 override fun Placeable.crossAxisSize() = if (isHorizontal) measuredHeight else measuredWidth
579
580 override fun createConstraints(
581 mainAxisMin: Int,
582 crossAxisMin: Int,
583 mainAxisMax: Int,
584 crossAxisMax: Int,
585 isPrioritizing: Boolean
586 ): Constraints {
587 return if (isHorizontal) {
588 createRowConstraints(
589 isPrioritizing,
590 mainAxisMin,
591 crossAxisMin,
592 mainAxisMax,
593 crossAxisMax,
594 )
595 } else {
596 createColumnConstraints(
597 isPrioritizing,
598 mainAxisMin,
599 crossAxisMin,
600 mainAxisMax,
601 crossAxisMax,
602 )
603 }
604 }
605
placeHelpernull606 override fun placeHelper(
607 placeables: Array<Placeable?>,
608 measureScope: MeasureScope,
609 beforeCrossAxisAlignmentLine: Int,
610 mainAxisPositions: IntArray,
611 mainAxisLayoutSize: Int,
612 crossAxisLayoutSize: Int,
613 crossAxisOffset: IntArray?,
614 currentLineIndex: Int,
615 startIndex: Int,
616 endIndex: Int
617 ): MeasureResult {
618 with(measureScope) {
619 val width: Int
620 val height: Int
621 if (isHorizontal) {
622 width = mainAxisLayoutSize
623 height = crossAxisLayoutSize
624 } else {
625 width = crossAxisLayoutSize
626 height = mainAxisLayoutSize
627 }
628 val layoutDirection =
629 if (isHorizontal) {
630 LayoutDirection.Ltr
631 } else {
632 layoutDirection
633 }
634 return layout(width, height) {
635 val crossAxisLineOffset = crossAxisOffset?.get(currentLineIndex) ?: 0
636 for (i in startIndex until endIndex) {
637 val placeable = placeables[i]!!
638 val crossAxisPosition =
639 getCrossAxisPosition(
640 placeable,
641 crossAxisLayoutSize,
642 layoutDirection,
643 beforeCrossAxisAlignmentLine
644 ) + crossAxisLineOffset
645 if (isHorizontal) {
646 placeable.place(mainAxisPositions[i - startIndex], crossAxisPosition)
647 } else {
648 placeable.place(crossAxisPosition, mainAxisPositions[i - startIndex])
649 }
650 }
651 }
652 }
653 }
654
getCrossAxisPositionnull655 fun getCrossAxisPosition(
656 placeable: Placeable,
657 crossAxisLayoutSize: Int,
658 layoutDirection: LayoutDirection,
659 beforeCrossAxisAlignmentLine: Int
660 ): Int {
661 val childCrossAlignment =
662 placeable.rowColumnParentData?.crossAxisAlignment ?: crossAxisAlignment
663 return childCrossAlignment.align(
664 size = crossAxisLayoutSize - placeable.crossAxisSize(),
665 layoutDirection = layoutDirection,
666 placeable = placeable,
667 beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine
668 )
669 }
670
populateMainAxisPositionsnull671 override fun populateMainAxisPositions(
672 mainAxisLayoutSize: Int,
673 childrenMainAxisSize: IntArray,
674 mainAxisPositions: IntArray,
675 measureScope: MeasureScope
676 ) {
677 if (isHorizontal) {
678 with(horizontalArrangement) {
679 measureScope.arrange(
680 mainAxisLayoutSize,
681 childrenMainAxisSize,
682 measureScope.layoutDirection,
683 mainAxisPositions
684 )
685 }
686 } else {
687 with(verticalArrangement) {
688 measureScope.arrange(
689 mainAxisLayoutSize,
690 childrenMainAxisSize,
691 mainAxisPositions,
692 )
693 }
694 }
695 }
696 }
697
698 /** Returns a Flow Measure Policy */
699 @OptIn(ExperimentalLayoutApi::class)
700 private data class FlowMeasurePolicy(
701 override val isHorizontal: Boolean,
702 override val horizontalArrangement: Arrangement.Horizontal,
703 override val verticalArrangement: Arrangement.Vertical,
704 private val mainAxisSpacing: Dp,
705 override val crossAxisAlignment: CrossAxisAlignment,
706 private val crossAxisArrangementSpacing: Dp,
707 private val maxItemsInMainAxis: Int,
708 private val maxLines: Int,
709 private val overflow: FlowLayoutOverflowState,
710 ) : MultiContentMeasurePolicy, FlowLineMeasurePolicy {
711
measurenull712 override fun MeasureScope.measure(
713 measurables: List<List<Measurable>>,
714 constraints: Constraints
715 ): MeasureResult {
716 if (
717 maxLines == 0 ||
718 maxItemsInMainAxis == 0 ||
719 measurables.isEmpty() ||
720 constraints.maxHeight == 0 &&
721 overflow.type != FlowLayoutOverflow.OverflowType.Visible
722 ) {
723 return layout(0, 0) {}
724 }
725 val list = measurables.first()
726 if (list.isEmpty()) {
727 return layout(0, 0) {}
728 }
729 val seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull()
730 val collapseMeasurable = measurables.getOrNull(2)?.firstOrNull()
731 overflow.itemCount = list.size
732 overflow.setOverflowMeasurables(
733 this@FlowMeasurePolicy,
734 seeMoreMeasurable,
735 collapseMeasurable,
736 constraints,
737 )
738 return breakDownItems(
739 this@FlowMeasurePolicy,
740 list.iterator(),
741 mainAxisSpacing,
742 crossAxisArrangementSpacing,
743 OrientationIndependentConstraints(
744 constraints,
745 if (isHorizontal) {
746 LayoutOrientation.Horizontal
747 } else {
748 LayoutOrientation.Vertical
749 }
750 ),
751 maxItemsInMainAxis,
752 maxLines,
753 overflow
754 )
755 }
756
minIntrinsicWidthnull757 override fun IntrinsicMeasureScope.minIntrinsicWidth(
758 measurables: List<List<IntrinsicMeasurable>>,
759 height: Int
760 ): Int {
761 overflow.setOverflowMeasurables(
762 seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
763 collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
764 isHorizontal,
765 constraints = Constraints(maxHeight = height)
766 )
767 return if (isHorizontal) {
768 minIntrinsicMainAxisSize(
769 measurables.firstOrNull() ?: listOf(),
770 height,
771 mainAxisSpacing.roundToPx(),
772 crossAxisArrangementSpacing.roundToPx(),
773 maxLines = maxLines,
774 maxItemsInMainAxis = maxItemsInMainAxis,
775 overflow = overflow
776 )
777 } else {
778 intrinsicCrossAxisSize(
779 measurables.firstOrNull() ?: listOf(),
780 height,
781 mainAxisSpacing.roundToPx(),
782 crossAxisArrangementSpacing.roundToPx(),
783 maxLines = maxLines,
784 maxItemsInMainAxis = maxItemsInMainAxis,
785 overflow = overflow
786 )
787 }
788 }
789
minIntrinsicHeightnull790 override fun IntrinsicMeasureScope.minIntrinsicHeight(
791 measurables: List<List<IntrinsicMeasurable>>,
792 width: Int
793 ): Int {
794 overflow.setOverflowMeasurables(
795 seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
796 collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
797 isHorizontal,
798 constraints = Constraints(maxWidth = width)
799 )
800 return if (isHorizontal) {
801 intrinsicCrossAxisSize(
802 measurables.firstOrNull() ?: listOf(),
803 width,
804 mainAxisSpacing.roundToPx(),
805 crossAxisArrangementSpacing.roundToPx(),
806 maxLines = maxLines,
807 maxItemsInMainAxis = maxItemsInMainAxis,
808 overflow = overflow
809 )
810 } else {
811 minIntrinsicMainAxisSize(
812 measurables.firstOrNull() ?: listOf(),
813 width,
814 mainAxisSpacing.roundToPx(),
815 crossAxisArrangementSpacing.roundToPx(),
816 maxLines = maxLines,
817 maxItemsInMainAxis = maxItemsInMainAxis,
818 overflow = overflow
819 )
820 }
821 }
822
maxIntrinsicHeightnull823 override fun IntrinsicMeasureScope.maxIntrinsicHeight(
824 measurables: List<List<IntrinsicMeasurable>>,
825 width: Int
826 ): Int {
827 overflow.setOverflowMeasurables(
828 seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
829 collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
830 isHorizontal,
831 constraints = Constraints(maxWidth = width)
832 )
833 return if (isHorizontal) {
834 intrinsicCrossAxisSize(
835 measurables.firstOrNull() ?: listOf(),
836 width,
837 mainAxisSpacing.roundToPx(),
838 crossAxisArrangementSpacing.roundToPx(),
839 maxLines = maxLines,
840 maxItemsInMainAxis = maxItemsInMainAxis,
841 overflow = overflow
842 )
843 } else {
844 maxIntrinsicMainAxisSize(
845 measurables.firstOrNull() ?: listOf(),
846 width,
847 mainAxisSpacing.roundToPx(),
848 )
849 }
850 }
851
maxIntrinsicWidthnull852 override fun IntrinsicMeasureScope.maxIntrinsicWidth(
853 measurables: List<List<IntrinsicMeasurable>>,
854 height: Int
855 ): Int {
856 overflow.setOverflowMeasurables(
857 seeMoreMeasurable = measurables.getOrNull(1)?.firstOrNull(),
858 collapseMeasurable = measurables.getOrNull(2)?.firstOrNull(),
859 isHorizontal,
860 constraints = Constraints(maxHeight = height)
861 )
862 return if (isHorizontal) {
863 maxIntrinsicMainAxisSize(
864 measurables.firstOrNull() ?: listOf(),
865 height,
866 mainAxisSpacing.roundToPx(),
867 )
868 } else {
869 intrinsicCrossAxisSize(
870 measurables.firstOrNull() ?: listOf(),
871 height,
872 mainAxisSpacing.roundToPx(),
873 crossAxisArrangementSpacing.roundToPx(),
874 maxLines = maxLines,
875 maxItemsInMainAxis = maxItemsInMainAxis,
876 overflow = overflow
877 )
878 }
879 }
880
minIntrinsicMainAxisSizenull881 fun minIntrinsicMainAxisSize(
882 measurables: List<IntrinsicMeasurable>,
883 crossAxisAvailable: Int,
884 mainAxisSpacing: Int,
885 crossAxisSpacing: Int,
886 maxItemsInMainAxis: Int,
887 maxLines: Int,
888 overflow: FlowLayoutOverflowState
889 ) =
890 minIntrinsicMainAxisSize(
891 measurables,
892 mainAxisSize = { _, size -> minMainAxisIntrinsicItemSize(size) },
sizenull893 crossAxisSize = { _, size -> minCrossAxisIntrinsicItemSize(size) },
894 crossAxisAvailable,
895 mainAxisSpacing,
896 crossAxisSpacing,
897 maxItemsInMainAxis,
898 maxLines,
899 overflow
900 )
901
maxIntrinsicMainAxisSizenull902 fun maxIntrinsicMainAxisSize(
903 measurables: List<IntrinsicMeasurable>,
904 height: Int,
905 arrangementSpacing: Int
906 ) =
907 maxIntrinsicMainAxisSize(
908 measurables,
909 { _, size -> maxMainAxisIntrinsicItemSize(size) },
910 height,
911 arrangementSpacing,
912 maxItemsInMainAxis
913 )
914
intrinsicCrossAxisSizenull915 fun intrinsicCrossAxisSize(
916 measurables: List<IntrinsicMeasurable>,
917 mainAxisAvailable: Int,
918 mainAxisSpacing: Int,
919 crossAxisSpacing: Int,
920 maxItemsInMainAxis: Int,
921 maxLines: Int,
922 overflow: FlowLayoutOverflowState
923 ) =
924 intrinsicCrossAxisSize(
925 measurables,
926 mainAxisSize = { _, size -> minMainAxisIntrinsicItemSize(size) },
sizenull927 crossAxisSize = { _, size -> minCrossAxisIntrinsicItemSize(size) },
928 mainAxisAvailable,
929 mainAxisSpacing,
930 crossAxisSpacing,
931 maxItemsInMainAxis = maxItemsInMainAxis,
932 overflow = overflow,
933 maxLines = maxLines
934 )
935 .first
936
maxMainAxisIntrinsicItemSizenull937 fun IntrinsicMeasurable.maxMainAxisIntrinsicItemSize(size: Int): Int =
938 if (isHorizontal) maxIntrinsicWidth(size) else maxIntrinsicHeight(size)
939
940 fun IntrinsicMeasurable.minCrossAxisIntrinsicItemSize(size: Int): Int =
941 if (isHorizontal) minIntrinsicHeight(size) else minIntrinsicWidth(size)
942
943 fun IntrinsicMeasurable.minMainAxisIntrinsicItemSize(size: Int): Int =
944 if (isHorizontal) minIntrinsicWidth(size) else minIntrinsicHeight(size)
945 }
946
947 private inline fun maxIntrinsicMainAxisSize(
948 children: List<IntrinsicMeasurable>,
949 mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
950 crossAxisAvailable: Int,
951 mainAxisSpacing: Int,
952 maxItemsInMainAxis: Int
953 ): Int {
954 var fixedSpace = 0
955 var currentFixedSpace = 0
956 var lastBreak = 0
957 children.fastForEachIndexed { index, child ->
958 val size = child.mainAxisSize(index, crossAxisAvailable) + mainAxisSpacing
959 if (index + 1 - lastBreak == maxItemsInMainAxis || index + 1 == children.size) {
960 lastBreak = index
961 currentFixedSpace += size
962 currentFixedSpace -= mainAxisSpacing // no mainAxisSpacing for last item in main axis
963 fixedSpace = max(fixedSpace, currentFixedSpace)
964 currentFixedSpace = 0
965 } else {
966 currentFixedSpace += size
967 }
968 }
969 return fixedSpace
970 }
971
972 /**
973 * Slower algorithm but needed to determine the minimum main axis size Uses a binary search to
974 * search different scenarios to see the minimum main axis size
975 */
976 @Suppress("BanInlineOptIn")
977 @OptIn(ExperimentalLayoutApi::class)
minIntrinsicMainAxisSizenull978 private inline fun minIntrinsicMainAxisSize(
979 children: List<IntrinsicMeasurable>,
980 mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
981 crossAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
982 crossAxisAvailable: Int,
983 mainAxisSpacing: Int,
984 crossAxisSpacing: Int,
985 maxItemsInMainAxis: Int,
986 maxLines: Int,
987 overflow: FlowLayoutOverflowState
988 ): Int {
989 if (children.isEmpty()) {
990 return 0
991 }
992 val mainAxisSizes = IntArray(children.size)
993 val crossAxisSizes = IntArray(children.size)
994
995 for (index in children.indices) {
996 val child = children[index]
997 val mainAxisItemSize = child.mainAxisSize(index, crossAxisAvailable)
998 mainAxisSizes[index] = mainAxisItemSize
999 crossAxisSizes[index] = child.crossAxisSize(index, mainAxisItemSize)
1000 }
1001
1002 var maxItemsThatCanBeShown =
1003 if (maxLines != Int.MAX_VALUE && maxItemsInMainAxis != Int.MAX_VALUE) {
1004 maxItemsInMainAxis * maxLines
1005 } else {
1006 Int.MAX_VALUE
1007 }
1008 val mustHaveEllipsis =
1009 when {
1010 maxItemsThatCanBeShown < children.size &&
1011 (overflow.type == FlowLayoutOverflow.OverflowType.ExpandIndicator ||
1012 overflow.type == FlowLayoutOverflow.OverflowType.ExpandOrCollapseIndicator) ->
1013 true
1014 maxItemsThatCanBeShown >= children.size &&
1015 maxLines >= overflow.minLinesToShowCollapse &&
1016 overflow.type == FlowLayoutOverflow.OverflowType.ExpandOrCollapseIndicator -> true
1017 else -> false
1018 }
1019 maxItemsThatCanBeShown -= if (mustHaveEllipsis) 1 else 0
1020 maxItemsThatCanBeShown = min(maxItemsThatCanBeShown, children.size)
1021 val maxMainAxisSize = mainAxisSizes.sum().run { this + ((children.size - 1) * mainAxisSpacing) }
1022 var mainAxisUsed = maxMainAxisSize
1023 var crossAxisUsed = crossAxisSizes.maxOf { it }
1024
1025 val minimumItemSize = mainAxisSizes.maxOf { it }
1026 var low = minimumItemSize
1027 var high = maxMainAxisSize
1028 while (low <= high) {
1029 if (crossAxisUsed == crossAxisAvailable) {
1030 return mainAxisUsed
1031 }
1032 val mid = (low + high) / 2
1033 mainAxisUsed = mid
1034 val pair =
1035 intrinsicCrossAxisSize(
1036 children,
1037 mainAxisSizes,
1038 crossAxisSizes,
1039 mainAxisUsed,
1040 mainAxisSpacing,
1041 crossAxisSpacing,
1042 maxItemsInMainAxis,
1043 maxLines,
1044 overflow
1045 )
1046 crossAxisUsed = pair.first
1047 val itemShown = pair.second
1048
1049 if (crossAxisUsed > crossAxisAvailable || itemShown < maxItemsThatCanBeShown) {
1050 low = mid + 1
1051 if (low > high) {
1052 return low
1053 }
1054 } else if (crossAxisUsed < crossAxisAvailable) {
1055 high = mid - 1
1056 } else {
1057 return mainAxisUsed
1058 }
1059 }
1060
1061 return mainAxisUsed
1062 }
1063
1064 /**
1065 * FlowRow: Intrinsic height (cross Axis) is based on a specified width FlowColumn: Intrinsic width
1066 * (crossAxis) based on a specified height
1067 */
intrinsicCrossAxisSizenull1068 private fun intrinsicCrossAxisSize(
1069 children: List<IntrinsicMeasurable>,
1070 mainAxisSizes: IntArray,
1071 crossAxisSizes: IntArray,
1072 mainAxisAvailable: Int,
1073 mainAxisSpacing: Int,
1074 crossAxisSpacing: Int,
1075 maxItemsInMainAxis: Int,
1076 maxLines: Int,
1077 overflow: FlowLayoutOverflowState
1078 ): IntIntPair {
1079 return intrinsicCrossAxisSize(
1080 children,
1081 { index, _ -> mainAxisSizes[index] },
1082 { index, _ -> crossAxisSizes[index] },
1083 mainAxisAvailable,
1084 mainAxisSpacing,
1085 crossAxisSpacing,
1086 maxItemsInMainAxis,
1087 maxLines,
1088 overflow
1089 )
1090 }
1091
1092 /**
1093 * FlowRow: Intrinsic height (cross Axis) is based on a specified width
1094 * * FlowColumn: Intrinsic width (crossAxis) based on a specified height
1095 */
intrinsicCrossAxisSizenull1096 private inline fun intrinsicCrossAxisSize(
1097 children: List<IntrinsicMeasurable>,
1098 mainAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
1099 crossAxisSize: IntrinsicMeasurable.(Int, Int) -> Int,
1100 mainAxisAvailable: Int,
1101 mainAxisSpacing: Int,
1102 crossAxisSpacing: Int,
1103 maxItemsInMainAxis: Int,
1104 maxLines: Int,
1105 overflow: FlowLayoutOverflowState
1106 ): IntIntPair {
1107 if (children.isEmpty()) {
1108 return IntIntPair(0, 0)
1109 }
1110 val buildingBlocks =
1111 FlowLayoutBuildingBlocks(
1112 maxItemsInMainAxis = maxItemsInMainAxis,
1113 overflow = overflow,
1114 maxLines = maxLines,
1115 constraints =
1116 OrientationIndependentConstraints(
1117 mainAxisMin = 0,
1118 mainAxisMax = mainAxisAvailable,
1119 crossAxisMin = 0,
1120 crossAxisMax = Constraints.Infinity
1121 ),
1122 mainAxisSpacing = mainAxisSpacing,
1123 crossAxisSpacing = crossAxisSpacing,
1124 )
1125 var nextChild = children.getOrNull(0)
1126 var nextCrossAxisSize = nextChild?.crossAxisSize(0, mainAxisAvailable) ?: 0
1127 var nextMainAxisSize = nextChild?.mainAxisSize(0, nextCrossAxisSize) ?: 0
1128
1129 var remaining = mainAxisAvailable
1130 var currentCrossAxisSize = 0
1131 var totalCrossAxisSize = 0
1132 var lastBreak = 0
1133 var lineIndex = 0
1134
1135 var wrapInfo =
1136 buildingBlocks.getWrapInfo(
1137 nextItemHasNext = children.size > 1,
1138 nextIndexInLine = 0,
1139 leftOver = IntIntPair(remaining, Constraints.Infinity),
1140 nextSize =
1141 if (nextChild == null) null else IntIntPair(nextMainAxisSize, nextCrossAxisSize),
1142 lineIndex = lineIndex,
1143 totalCrossAxisSize = totalCrossAxisSize,
1144 currentLineCrossAxisSize = currentCrossAxisSize,
1145 isWrappingRound = false,
1146 isEllipsisWrap = false
1147 )
1148
1149 if (wrapInfo.isLastItemInContainer) {
1150 val size =
1151 overflow
1152 .ellipsisSize(
1153 hasNext = nextChild != null,
1154 lineIndex = 0,
1155 totalCrossAxisSize = 0,
1156 )
1157 ?.second ?: 0
1158 val noOfItemsShown = 0
1159 return IntIntPair(size, noOfItemsShown)
1160 }
1161
1162 var noOfItemsShown = 0
1163 for (index in children.indices) {
1164 val childCrossAxisSize = nextCrossAxisSize
1165 val childMainAxisSize = nextMainAxisSize
1166 remaining -= childMainAxisSize
1167 noOfItemsShown = index + 1
1168 currentCrossAxisSize = maxOf(currentCrossAxisSize, childCrossAxisSize)
1169
1170 // look ahead to simplify logic
1171 nextChild = children.getOrNull(index + 1)
1172 nextCrossAxisSize = nextChild?.crossAxisSize(index + 1, mainAxisAvailable) ?: 0
1173 nextMainAxisSize =
1174 nextChild?.mainAxisSize(index + 1, nextCrossAxisSize)?.plus(mainAxisSpacing) ?: 0
1175
1176 wrapInfo =
1177 buildingBlocks.getWrapInfo(
1178 nextItemHasNext = index + 2 < children.size,
1179 nextIndexInLine = (index + 1) - lastBreak,
1180 leftOver = IntIntPair(remaining, Constraints.Infinity),
1181 nextSize =
1182 if (nextChild == null) {
1183 null
1184 } else {
1185 IntIntPair(nextMainAxisSize, nextCrossAxisSize)
1186 },
1187 lineIndex = lineIndex,
1188 totalCrossAxisSize = totalCrossAxisSize,
1189 currentLineCrossAxisSize = currentCrossAxisSize,
1190 isWrappingRound = false,
1191 isEllipsisWrap = false
1192 )
1193 if (wrapInfo.isLastItemInLine) {
1194 totalCrossAxisSize += currentCrossAxisSize + crossAxisSpacing
1195 val ellipsisWrapInfo =
1196 buildingBlocks.getWrapEllipsisInfo(
1197 wrapInfo,
1198 hasNext = nextChild != null,
1199 leftOverMainAxis = remaining,
1200 lastContentLineIndex = lineIndex,
1201 totalCrossAxisSize = totalCrossAxisSize,
1202 nextIndexInLine = (index + 1) - lastBreak,
1203 )
1204 currentCrossAxisSize = 0
1205 remaining = mainAxisAvailable
1206 lastBreak = index + 1
1207 nextMainAxisSize -= mainAxisSpacing
1208 lineIndex++
1209 if (wrapInfo.isLastItemInContainer) {
1210 ellipsisWrapInfo?.ellipsisSize?.let {
1211 if (!ellipsisWrapInfo.placeEllipsisOnLastContentLine) {
1212 totalCrossAxisSize += it.second + crossAxisSpacing
1213 }
1214 }
1215 break
1216 }
1217 }
1218 }
1219 // remove the last spacing for the last row or column
1220 totalCrossAxisSize -= crossAxisSpacing
1221 return IntIntPair(totalCrossAxisSize, noOfItemsShown)
1222 }
1223
1224 /**
1225 * Breaks down items based on space, size and maximum items in main axis. When items run out of
1226 * space or the maximum items to fit in the main axis is reached, it moves to the next "line" and
1227 * moves the next batch of items to a new list of items
1228 */
breakDownItemsnull1229 internal fun MeasureScope.breakDownItems(
1230 measurePolicy: FlowLineMeasurePolicy,
1231 measurablesIterator: Iterator<Measurable>,
1232 mainAxisSpacingDp: Dp,
1233 crossAxisSpacingDp: Dp,
1234 constraints: OrientationIndependentConstraints,
1235 maxItemsInMainAxis: Int,
1236 maxLines: Int,
1237 overflow: FlowLayoutOverflowState,
1238 ): MeasureResult {
1239 val items = mutableVectorOf<MeasureResult>()
1240 val mainAxisMax = constraints.mainAxisMax
1241 val mainAxisMin = constraints.mainAxisMin
1242 val crossAxisMax = constraints.crossAxisMax
1243 val placeables = mutableIntObjectMapOf<Placeable?>()
1244 val measurables = mutableListOf<Measurable>()
1245
1246 val spacing = ceil(mainAxisSpacingDp.toPx()).toInt()
1247 val crossAxisSpacing = ceil(crossAxisSpacingDp.toPx()).toInt()
1248 val subsetConstraints = OrientationIndependentConstraints(0, mainAxisMax, 0, crossAxisMax)
1249 val measureConstraints =
1250 subsetConstraints
1251 .copy(mainAxisMin = 0)
1252 .toBoxConstraints(
1253 if (measurePolicy.isHorizontal) LayoutOrientation.Horizontal
1254 else LayoutOrientation.Vertical
1255 )
1256
1257 var index = 0
1258 var measurable: Measurable?
1259 var placeableItem: Placeable? = null
1260
1261 var lineIndex = 0
1262 var leftOver = mainAxisMax
1263 var leftOverCrossAxis = crossAxisMax
1264 val lineInfo =
1265 if (measurablesIterator is ContextualFlowItemIterator) {
1266 FlowLineInfo(
1267 lineIndex = lineIndex,
1268 positionInLine = 0,
1269 maxMainAxisSize = leftOver.toDp(),
1270 maxCrossAxisSize = leftOverCrossAxis.toDp()
1271 )
1272 } else {
1273 null
1274 }
1275
1276 var nextSize =
1277 measurablesIterator.hasNext().run {
1278 measurable = if (!this) null else measurablesIterator.safeNext(lineInfo)
1279 measurable?.measureAndCache(measurePolicy, measureConstraints) { placeable ->
1280 placeableItem = placeable
1281 }
1282 }
1283 var nextMainAxisSize: Int? = nextSize?.first
1284 var nextCrossAxisSize: Int? = nextSize?.second
1285
1286 var startBreakLineIndex = 0
1287 val endBreakLineList = mutableIntListOf()
1288 val crossAxisSizes = mutableIntListOf()
1289
1290 val buildingBlocks =
1291 FlowLayoutBuildingBlocks(
1292 maxItemsInMainAxis = maxItemsInMainAxis,
1293 mainAxisSpacing = spacing,
1294 crossAxisSpacing = crossAxisSpacing,
1295 constraints = constraints,
1296 maxLines = maxLines,
1297 overflow = overflow
1298 )
1299 var ellipsisWrapInfo: FlowLayoutBuildingBlocks.WrapEllipsisInfo? = null
1300 var wrapInfo =
1301 buildingBlocks
1302 .getWrapInfo(
1303 nextItemHasNext = measurablesIterator.hasNext(),
1304 leftOver = IntIntPair(leftOver, leftOverCrossAxis),
1305 totalCrossAxisSize = 0,
1306 nextSize = nextSize,
1307 currentLineCrossAxisSize = 0,
1308 nextIndexInLine = 0,
1309 isWrappingRound = false,
1310 isEllipsisWrap = false,
1311 lineIndex = 0
1312 )
1313 .also { wrapInfo ->
1314 if (wrapInfo.isLastItemInContainer) {
1315 ellipsisWrapInfo =
1316 buildingBlocks.getWrapEllipsisInfo(
1317 wrapInfo,
1318 nextSize != null,
1319 lastContentLineIndex = -1,
1320 totalCrossAxisSize = 0,
1321 leftOver,
1322 nextIndexInLine = 0
1323 )
1324 }
1325 }
1326
1327 // figure out the mainAxisTotalSize which will be minMainAxis when measuring the row/column
1328 var mainAxisTotalSize = mainAxisMin
1329 var crossAxisTotalSize = 0
1330 var currentLineMainAxisSize = 0
1331 var currentLineCrossAxisSize = 0
1332 while (!wrapInfo.isLastItemInContainer && measurable != null) {
1333 val itemMainAxisSize = nextMainAxisSize!!
1334 val itemCrossAxisSize = nextCrossAxisSize!!
1335 currentLineMainAxisSize += itemMainAxisSize
1336 currentLineCrossAxisSize = maxOf(currentLineCrossAxisSize, itemCrossAxisSize)
1337 leftOver -= itemMainAxisSize
1338 overflow.itemShown = index + 1
1339 measurables.add(measurable!!)
1340 placeables[index] = placeableItem
1341
1342 val nextIndexInLine = (index + 1) - startBreakLineIndex
1343 val willFitLine = nextIndexInLine < maxItemsInMainAxis
1344
1345 lineInfo?.update(
1346 lineIndex = if (willFitLine) lineIndex else lineIndex + 1,
1347 positionInLine = if (willFitLine) nextIndexInLine else 0,
1348 maxMainAxisSize =
1349 if (willFitLine) {
1350 (leftOver - spacing).fastCoerceAtLeast(0)
1351 } else {
1352 mainAxisMax
1353 }
1354 .toDp(),
1355 maxCrossAxisSize =
1356 if (willFitLine) {
1357 leftOverCrossAxis
1358 } else {
1359 (leftOverCrossAxis - currentLineCrossAxisSize - crossAxisSpacing)
1360 .fastCoerceAtLeast(0)
1361 }
1362 .toDp()
1363 )
1364
1365 nextSize =
1366 measurablesIterator.hasNext().run {
1367 measurable = if (!this) null else measurablesIterator.safeNext(lineInfo)
1368 placeableItem = null
1369 measurable?.measureAndCache(measurePolicy, measureConstraints) { placeable ->
1370 placeableItem = placeable
1371 }
1372 }
1373 nextMainAxisSize = nextSize?.first?.plus(spacing)
1374 nextCrossAxisSize = nextSize?.second
1375
1376 wrapInfo =
1377 buildingBlocks.getWrapInfo(
1378 nextItemHasNext = measurablesIterator.hasNext(),
1379 leftOver = IntIntPair(leftOver, leftOverCrossAxis),
1380 totalCrossAxisSize = crossAxisTotalSize,
1381 nextSize =
1382 if (nextSize == null) null
1383 else IntIntPair(nextMainAxisSize!!, nextCrossAxisSize!!),
1384 currentLineCrossAxisSize = currentLineCrossAxisSize,
1385 nextIndexInLine = nextIndexInLine,
1386 isWrappingRound = false,
1387 isEllipsisWrap = false,
1388 lineIndex = lineIndex
1389 )
1390 if (wrapInfo.isLastItemInLine) {
1391 mainAxisTotalSize = maxOf(mainAxisTotalSize, currentLineMainAxisSize)
1392 mainAxisTotalSize = minOf(mainAxisTotalSize, mainAxisMax)
1393 crossAxisTotalSize += currentLineCrossAxisSize
1394 ellipsisWrapInfo =
1395 buildingBlocks.getWrapEllipsisInfo(
1396 wrapInfo,
1397 nextSize != null,
1398 lastContentLineIndex = lineIndex,
1399 totalCrossAxisSize = crossAxisTotalSize,
1400 leftOver,
1401 (index + 1) - startBreakLineIndex
1402 )
1403 crossAxisSizes.add(currentLineCrossAxisSize)
1404 leftOver = mainAxisMax
1405 leftOverCrossAxis = crossAxisMax - crossAxisTotalSize - crossAxisSpacing
1406 startBreakLineIndex = index + 1
1407 endBreakLineList.add(index + 1)
1408 currentLineMainAxisSize = 0
1409 currentLineCrossAxisSize = 0
1410 // only add spacing for next items in the row or column, not the starting indexes
1411 nextMainAxisSize = nextMainAxisSize?.minus(spacing)
1412 lineIndex++
1413 crossAxisTotalSize += crossAxisSpacing
1414 }
1415 index++
1416 }
1417
1418 ellipsisWrapInfo?.let {
1419 measurables.add(it.ellipsis)
1420 placeables[measurables.size - 1] = it.placeable
1421 lineIndex = endBreakLineList.lastIndex
1422 if (it.placeEllipsisOnLastContentLine) {
1423 val lastIndex = endBreakLineList.size - 1
1424 val lastLineCrossAxis = crossAxisSizes[lineIndex]
1425 crossAxisSizes[lineIndex] = max(lastLineCrossAxis, it.ellipsisSize.second)
1426 endBreakLineList[lastIndex] = endBreakLineList.last() + 1
1427 } else {
1428 crossAxisSizes.add(it.ellipsisSize.second)
1429 endBreakLineList.add(endBreakLineList.last() + 1)
1430 }
1431 }
1432
1433 val arrayOfPlaceables: Array<Placeable?> = Array(measurables.size) { placeables[it] }
1434 val crossAxisOffsets = IntArray(endBreakLineList.size)
1435 val crossAxisSizesArray = IntArray(endBreakLineList.size)
1436 crossAxisTotalSize = 0
1437
1438 var startIndex = 0
1439 endBreakLineList.forEachIndexed { currentLineIndex, endIndex ->
1440 var crossAxisSize = crossAxisSizes[currentLineIndex]
1441 val result =
1442 measurePolicy.measure(
1443 mainAxisMin = mainAxisTotalSize,
1444 crossAxisMin = subsetConstraints.crossAxisMin,
1445 mainAxisMax = subsetConstraints.mainAxisMax,
1446 crossAxisMax = crossAxisSize,
1447 spacing,
1448 this,
1449 measurables,
1450 arrayOfPlaceables,
1451 startIndex,
1452 endIndex,
1453 crossAxisOffsets,
1454 currentLineIndex
1455 )
1456 val mainAxisSize: Int
1457 if (measurePolicy.isHorizontal) {
1458 mainAxisSize = result.width
1459 crossAxisSize = result.height
1460 } else {
1461 mainAxisSize = result.height
1462 crossAxisSize = result.width
1463 }
1464 crossAxisSizesArray[currentLineIndex] = crossAxisSize
1465 crossAxisTotalSize += crossAxisSize
1466 mainAxisTotalSize = maxOf(mainAxisTotalSize, mainAxisSize)
1467 items.add(result)
1468 startIndex = endIndex
1469 }
1470
1471 if (items.isEmpty()) {
1472 mainAxisTotalSize = 0
1473 crossAxisTotalSize = 0
1474 }
1475
1476 return placeHelper(
1477 constraints,
1478 mainAxisTotalSize,
1479 crossAxisTotalSize,
1480 crossAxisSizesArray,
1481 items,
1482 measurePolicy,
1483 crossAxisOffsets
1484 )
1485 }
1486
safeNextnull1487 private fun Iterator<Measurable>.safeNext(info: FlowLineInfo?): Measurable? {
1488 return try {
1489 if (this is ContextualFlowItemIterator) {
1490 this.getNext(info!!)
1491 } else {
1492 next()
1493 }
1494 } catch (e: IndexOutOfBoundsException) {
1495 null
1496 }
1497 }
1498
mainAxisMinnull1499 internal fun IntrinsicMeasurable.mainAxisMin(isHorizontal: Boolean, crossAxisSize: Int) =
1500 if (isHorizontal) {
1501 minIntrinsicWidth(crossAxisSize)
1502 } else {
1503 minIntrinsicHeight(crossAxisSize)
1504 }
1505
crossAxisMinnull1506 internal fun IntrinsicMeasurable.crossAxisMin(isHorizontal: Boolean, mainAxisSize: Int) =
1507 if (isHorizontal) {
1508 minIntrinsicHeight(mainAxisSize)
1509 } else {
1510 minIntrinsicWidth(mainAxisSize)
1511 }
1512
1513 internal val CROSS_AXIS_ALIGNMENT_TOP = CrossAxisAlignment.vertical(Alignment.Top)
1514 internal val CROSS_AXIS_ALIGNMENT_START = CrossAxisAlignment.horizontal(Alignment.Start)
1515
1516 // We measure and cache to improve performance dramatically, instead of using intrinsics
1517 // This only works so far for fixed size items.
1518 // For weighted items, we continue to use their intrinsic widths.
1519 // This is because their fixed sizes are only determined after we determine
1520 // the number of items that can fit in the row/column it only lies on.
measureAndCachenull1521 internal fun Measurable.measureAndCache(
1522 measurePolicy: FlowLineMeasurePolicy,
1523 constraints: Constraints,
1524 storePlaceable: (Placeable?) -> Unit
1525 ): IntIntPair {
1526 return if (
1527 rowColumnParentData.weight == 0f &&
1528 rowColumnParentData?.flowLayoutData?.fillCrossAxisFraction == null
1529 ) {
1530 // fixed sizes: measure once
1531 val placeable = measure(constraints).also(storePlaceable)
1532 with(measurePolicy) {
1533 val mainAxis = placeable.mainAxisSize()
1534 val crossAxis = placeable.crossAxisSize()
1535 IntIntPair(mainAxis, crossAxis)
1536 }
1537 } else {
1538 val mainAxis = mainAxisMin(measurePolicy.isHorizontal, Constraints.Infinity)
1539 val crossAxis = crossAxisMin(measurePolicy.isHorizontal, mainAxis)
1540 IntIntPair(mainAxis, crossAxis)
1541 }
1542 }
1543
placeHelpernull1544 internal fun MeasureScope.placeHelper(
1545 constraints: OrientationIndependentConstraints,
1546 mainAxisTotalSize: Int,
1547 crossAxisTotalSize: Int,
1548 crossAxisSizes: IntArray,
1549 items: MutableVector<MeasureResult>,
1550 measureHelper: FlowLineMeasurePolicy,
1551 outPosition: IntArray,
1552 ): MeasureResult {
1553 val isHorizontal = measureHelper.isHorizontal
1554 val verticalArrangement = measureHelper.verticalArrangement
1555 val horizontalArrangement = measureHelper.horizontalArrangement
1556 // space in between children, except for the last child
1557 var totalCrossAxisSize = crossAxisTotalSize
1558 // cross axis arrangement
1559 if (isHorizontal) {
1560 with(verticalArrangement) {
1561 val totalCrossAxisSpacing = spacing.roundToPx() * (items.size - 1)
1562 totalCrossAxisSize += totalCrossAxisSpacing
1563 totalCrossAxisSize =
1564 totalCrossAxisSize.fastCoerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
1565 arrange(totalCrossAxisSize, crossAxisSizes, outPosition)
1566 }
1567 } else {
1568 with(horizontalArrangement) {
1569 val totalCrossAxisSpacing = spacing.roundToPx() * (items.size - 1)
1570 totalCrossAxisSize += totalCrossAxisSpacing
1571 totalCrossAxisSize =
1572 totalCrossAxisSize.fastCoerceIn(constraints.crossAxisMin, constraints.crossAxisMax)
1573 arrange(totalCrossAxisSize, crossAxisSizes, layoutDirection, outPosition)
1574 }
1575 }
1576
1577 val finalMainAxisTotalSize =
1578 mainAxisTotalSize.fastCoerceIn(constraints.mainAxisMin, constraints.mainAxisMax)
1579
1580 val layoutWidth: Int
1581 val layoutHeight: Int
1582 if (isHorizontal) {
1583 layoutWidth = finalMainAxisTotalSize
1584 layoutHeight = totalCrossAxisSize
1585 } else {
1586 layoutWidth = totalCrossAxisSize
1587 layoutHeight = finalMainAxisTotalSize
1588 }
1589
1590 return layout(layoutWidth, layoutHeight) {
1591 items.forEach { measureResult -> measureResult.placeChildren() }
1592 }
1593 }
1594