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.compose.foundation.layout.internal.requirePrecondition
23 import androidx.compose.runtime.Composable
24 import androidx.compose.runtime.Stable
25 import androidx.compose.runtime.remember
26 import androidx.compose.ui.Alignment
27 import androidx.compose.ui.Modifier
28 import androidx.compose.ui.layout.Measurable
29 import androidx.compose.ui.layout.MeasureResult
30 import androidx.compose.ui.layout.SubcomposeLayout
31 import androidx.compose.ui.layout.SubcomposeMeasureScope
32 import androidx.compose.ui.unit.Constraints
33 import androidx.compose.ui.unit.Dp
34 import androidx.compose.ui.unit.dp
35
36 /**
37 * [ContextualFlowRow] is a specialized version of the [FlowRow] layout. It is designed to enable
38 * users to make contextual decisions during the construction of [FlowRow] layouts.
39 *
40 * This component is particularly advantageous when dealing with a large collection of items,
41 * allowing for efficient management and display. Unlike traditional [FlowRow] that composes all
42 * items regardless of their visibility, ContextualFlowRow smartly limits composition to only those
43 * items that are visible within its constraints, such as [maxLines] or `maxHeight`. This approach
44 * ensures optimal performance and resource utilization by composing fewer items than the total
45 * number available, based on the current context and display parameters.
46 *
47 * While maintaining the core functionality of the standard [FlowRow], [ContextualFlowRow] operates
48 * on an index-based system and composes items sequentially, one after another. This approach
49 * provides a perfect way to make contextual decisions and can be an easier way to handle problems
50 * such as dynamic see more buttons such as (N+ buttons).
51 *
52 * Example:
53 *
54 * @sample androidx.compose.foundation.layout.samples.ContextualFlowRowMaxLineDynamicSeeMore
55 * @param itemCount The total number of item composable
56 * @param modifier The modifier to be applied to the Row.
57 * @param horizontalArrangement The horizontal arrangement of the layout's children.
58 * @param verticalArrangement The vertical arrangement of the layout's virtual rows.
59 * @param itemVerticalAlignment The cross axis/vertical alignment of an item in the column.
60 * @param maxItemsInEachRow The maximum number of items per row
61 * @param maxLines The maximum number of rows
62 * @param overflow The strategy to handle overflowing items
63 * @param content The indexed-based content of [ContextualFlowRowScope]
64 * @see FlowRow
65 * @see ContextualFlowColumn
66 */
67 @Deprecated("ContextualFlowLayouts are no longer maintained")
68 @Composable
69 @ExperimentalLayoutApi
70 fun ContextualFlowRow(
71 itemCount: Int,
72 modifier: Modifier = Modifier,
73 horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
74 verticalArrangement: Arrangement.Vertical = Arrangement.Top,
75 itemVerticalAlignment: Alignment.Vertical = Alignment.Top,
76 maxItemsInEachRow: Int = Int.MAX_VALUE,
77 maxLines: Int = Int.MAX_VALUE,
78 overflow: ContextualFlowRowOverflow = ContextualFlowRowOverflow.Clip,
79 content: @Composable ContextualFlowRowScope.(index: Int) -> Unit,
80 ) {
81 val overflowState = remember(overflow) { overflow.createOverflowState() }
82 val list: List<@Composable () -> Unit> =
83 remember(overflow) {
84 val mutableList: MutableList<@Composable () -> Unit> = mutableListOf()
85 overflow.addOverflowComposables(overflowState, mutableList)
86 mutableList
87 }
88 val measurePolicy =
89 contextualRowMeasurementHelper(
90 horizontalArrangement,
91 verticalArrangement,
92 itemVerticalAlignment,
93 maxItemsInEachRow,
94 maxLines,
95 overflowState,
96 itemCount,
97 list
98 ) { index, info ->
99 val scope =
100 ContextualFlowRowScopeImpl(
101 info.lineIndex,
102 info.positionInLine,
103 maxWidthInLine = info.maxMainAxisSize,
104 maxHeight = info.maxCrossAxisSize
105 )
106 scope.content(index)
107 }
108 SubcomposeLayout(modifier = modifier, measurePolicy = measurePolicy)
109 }
110
111 /**
112 * [ContextualFlowColumn] is a specialized version of the [FlowColumn] layout. It is designed to
113 * enable users to make contextual decisions during the construction of [FlowColumn] layouts.
114 *
115 * This component is particularly advantageous when dealing with a large collection of items,
116 * allowing for efficient management and display. Unlike traditional [FlowColumn] that composes all
117 * items regardless of their visibility, ContextualFlowColumn smartly limits composition to only
118 * those items that are visible within its constraints, such as [maxLines] or `maxWidth`. This
119 * approach ensures optimal performance and resource utilization by composing fewer items than the
120 * total number available, based on the current context and display parameters.
121 *
122 * While maintaining the core functionality of the standard [FlowColumn], [ContextualFlowColumn]
123 * operates on an index-based system and composes items sequentially, one after another. This
124 * approach provides a perfect way to make contextual decisions and can be an easier way to handle
125 * problems such as dynamic see more buttons such as (N+ buttons).
126 *
127 * Example:
128 *
129 * @sample androidx.compose.foundation.layout.samples.ContextualFlowColMaxLineDynamicSeeMore
130 * @param itemCount The total number of item composable
131 * @param modifier The modifier to be applied to the Row.
132 * @param verticalArrangement The vertical arrangement of the layout's virtual column.
133 * @param horizontalArrangement The horizontal arrangement of the layout's children.
134 * @param itemHorizontalAlignment The cross axis/horizontal alignment of an item in the column.
135 * @param maxItemsInEachColumn The maximum number of items per column
136 * @param maxLines The maximum number of columns
137 * @param overflow The straoadtegy to handle overflowing items
138 * @param content The indexed-based content of [ContextualFlowColumnScope]
139 * @see FlowColumn
140 * @see ContextualFlowRow
141 */
142 @Deprecated("ContextualFlowLayouts are no longer maintained")
143 @Composable
144 @ExperimentalLayoutApi
ContextualFlowColumnnull145 fun ContextualFlowColumn(
146 itemCount: Int,
147 modifier: Modifier = Modifier,
148 verticalArrangement: Arrangement.Vertical = Arrangement.Top,
149 horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
150 itemHorizontalAlignment: Alignment.Horizontal = Alignment.Start,
151 maxItemsInEachColumn: Int = Int.MAX_VALUE,
152 maxLines: Int = Int.MAX_VALUE,
153 overflow: ContextualFlowColumnOverflow = ContextualFlowColumnOverflow.Clip,
154 content: @Composable ContextualFlowColumnScope.(index: Int) -> Unit,
155 ) {
156 val overflowState = remember(overflow) { overflow.createOverflowState() }
157 val list: List<@Composable () -> Unit> =
158 remember(overflow) {
159 val mutableList: MutableList<@Composable () -> Unit> = mutableListOf()
160 overflow.addOverflowComposables(overflowState, mutableList)
161 mutableList
162 }
163 val measurePolicy =
164 contextualColumnMeasureHelper(
165 verticalArrangement,
166 horizontalArrangement,
167 itemHorizontalAlignment,
168 maxItemsInEachColumn,
169 maxLines,
170 overflowState,
171 itemCount,
172 list,
173 ) { index, info ->
174 val scope =
175 ContextualFlowColumnScopeImpl(
176 info.lineIndex,
177 info.positionInLine,
178 maxHeightInLine = info.maxMainAxisSize,
179 maxWidth = info.maxCrossAxisSize
180 )
181 scope.content(index)
182 }
183
184 SubcomposeLayout(modifier = modifier, measurePolicy = measurePolicy)
185 }
186
187 /** Defines the scope for items within a [ContextualFlowRow]. */
188 @Deprecated("ContextualFlowLayouts are no longer maintained")
189 @LayoutScopeMarker
190 @Stable
191 @ExperimentalLayoutApi
192 interface ContextualFlowRowScope : RowScope {
193 /**
194 * Have the item fill (possibly only partially) the max height of the tallest item in the row it
195 * was placed in, within the [FlowRow].
196 *
197 * @param fraction The fraction of the max height of the tallest item between `0` and `1`,
198 * inclusive.
199 *
200 * Example usage:
201 *
202 * @sample androidx.compose.foundation.layout.samples.SimpleFlowRow_EqualHeight
203 */
204 @ExperimentalLayoutApi
fillMaxRowHeightnull205 fun Modifier.fillMaxRowHeight(
206 @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f,
207 ): Modifier
208
209 /**
210 * Identifies the row or column index where the UI component(s) are to be placed, provided they
211 * do not exceed the specified [maxWidthInLine] and [maxHeight] for that row or column.
212 *
213 * Should the component(s) surpass these dimensions, their placement may shift to the subsequent
214 * row/column or they may be omitted from display, contingent upon the defined constraints.
215 *
216 * Example:
217 *
218 * @sample androidx.compose.foundation.layout.samples.ContextualFlowRow_ItemPosition
219 * @sample androidx.compose.foundation.layout.samples.ContextualFlowColumn_ItemPosition
220 */
221 val lineIndex: Int
222
223 /**
224 * Marks the index within the current row/column where the next component is to be inserted,
225 * assuming it conforms to the row's or column's [maxWidthInLine] and [maxHeight] limitations.
226 *
227 * In scenarios where multiple UI components are returned in one index call, this parameter is
228 * relevant solely to the first returned UI component, presuming it complies with the row's or
229 * column's defined constraints.
230 *
231 * Example:
232 *
233 * @sample androidx.compose.foundation.layout.samples.ContextualFlowRow_ItemPosition
234 * @sample androidx.compose.foundation.layout.samples.ContextualFlowColumn_ItemPosition
235 */
236 val indexInLine: Int
237
238 /**
239 * Specifies the maximum permissible width (main-axis) for the upcoming UI component at the
240 * given [lineIndex] and [indexInLine]. Exceeding this width may result in the component being
241 * reallocated to the following row within the [ContextualFlowRow] structure, subject to
242 * existing constraints.
243 */
244 val maxWidthInLine: Dp
245
246 /**
247 * Determines the maximum allowable height (cross-axis) for the forthcoming UI component,
248 * aligned with its [lineIndex] and [indexInLine]. Should this height threshold be exceeded, the
249 * component's visibility will depend on the overflow settings, potentially leading to its
250 * exclusion.
251 */
252 val maxHeight: Dp
253 }
254
255 /** Scope for the overflow [ContextualFlowRow]. */
256 @Deprecated("ContextualFlowLayouts are no longer maintained")
257 @LayoutScopeMarker
258 @Stable
259 @ExperimentalLayoutApi
260 interface ContextualFlowRowOverflowScope : FlowRowOverflowScope
261
262 /** Scope for the overflow [ContextualFlowColumn]. */
263 @Deprecated("ContextualFlowLayouts are no longer maintained")
264 @LayoutScopeMarker
265 @Stable
266 @ExperimentalLayoutApi
267 interface ContextualFlowColumnOverflowScope : FlowColumnOverflowScope
268
269 /** Provides a scope for items within a [ContextualFlowColumn]. */
270 @Deprecated("ContextualFlowLayouts are no longer maintained")
271 @LayoutScopeMarker
272 @Stable
273 @ExperimentalLayoutApi
274 interface ContextualFlowColumnScope : ColumnScope {
275 /**
276 * Have the item fill (possibly only partially) the max width of the widest item in the column
277 * it was placed in, within the [FlowColumn].
278 *
279 * @param fraction The fraction of the max width of the widest item between `0` and `1`,
280 * inclusive.
281 *
282 * Example usage:
283 *
284 * @sample androidx.compose.foundation.layout.samples.SimpleFlowColumn_EqualWidth
285 */
286 @ExperimentalLayoutApi
287 fun Modifier.fillMaxColumnWidth(
288 @FloatRange(from = 0.0, to = 1.0) fraction: Float = 1f,
289 ): Modifier
290
291 /**
292 * Identifies the row or column index where the UI component(s) are to be placed, provided they
293 * do not exceed the specified [maxWidth] and [maxHeightInLine] for that row or column.
294 *
295 * Should the component(s) surpass these dimensions, their placement may shift to the subsequent
296 * row/column or they may be omitted from display, contingent upon the defined constraints.
297 *
298 * Example:
299 *
300 * @sample androidx.compose.foundation.layout.samples.ContextualFlowRow_ItemPosition
301 * @sample androidx.compose.foundation.layout.samples.ContextualFlowColumn_ItemPosition
302 */
303 val lineIndex: Int
304
305 /**
306 * Marks the index within the current row/column where the next component is to be inserted,
307 * assuming it conforms to the row's or column's [maxWidth] and [maxHeightInLine] limitations.
308 *
309 * In scenarios where multiple UI components are returned in one index call, this parameter is
310 * relevant solely to the first returned UI component, presuming it complies with the row's or
311 * column's defined constraints.
312 *
313 * Example:
314 *
315 * @sample androidx.compose.foundation.layout.samples.ContextualFlowRow_ItemPosition
316 * @sample androidx.compose.foundation.layout.samples.ContextualFlowColumn_ItemPosition
317 */
318 val indexInLine: Int
319
320 /**
321 * Sets the maximum width (cross-axis dimension) that the upcoming UI component can occupy,
322 * based on its [lineIndex] and [indexInLine]. Exceeding this width might result in the
323 * component not being displayed, depending on the [ContextualFlowColumnOverflow.Visible]
324 * overflow configuration.
325 */
326 val maxWidth: Dp
327
328 /**
329 * Establishes the maximum height (main-axis dimension) permissible for the next UI component,
330 * aligned with its [lineIndex] and [indexInLine]. Should the component's height exceed this
331 * limit, it may be shifted to the subsequent column in [ContextualFlowColumn], subject to the
332 * predefined constraints.
333 */
334 val maxHeightInLine: Dp
335 }
336
337 @OptIn(ExperimentalLayoutApi::class)
338 internal class ContextualFlowRowScopeImpl(
339 override val lineIndex: Int,
340 override val indexInLine: Int,
341 override val maxWidthInLine: Dp,
342 override val maxHeight: Dp
343 ) : RowScope by RowScopeInstance, ContextualFlowRowScope {
fillMaxRowHeightnull344 override fun Modifier.fillMaxRowHeight(fraction: Float): Modifier {
345 requirePrecondition(fraction in 0.0f..1.0f) {
346 "invalid fraction $fraction; must be >= 0 and <= 1.0"
347 }
348 return this.then(
349 FillCrossAxisSizeElement(
350 fraction = fraction,
351 )
352 )
353 }
354 }
355
356 @OptIn(ExperimentalLayoutApi::class)
357 internal class ContextualFlowColumnScopeImpl(
358 override val lineIndex: Int,
359 override val indexInLine: Int,
360 override val maxWidth: Dp,
361 override val maxHeightInLine: Dp
362 ) : ColumnScope by ColumnScopeInstance, ContextualFlowColumnScope {
fillMaxColumnWidthnull363 override fun Modifier.fillMaxColumnWidth(fraction: Float): Modifier {
364 requirePrecondition(fraction in 0.0f..1.0f) {
365 "invalid fraction $fraction; must be >= 0 and <= 1.0"
366 }
367 return this.then(
368 FillCrossAxisSizeElement(
369 fraction = fraction,
370 )
371 )
372 }
373 }
374
375 @ExperimentalLayoutApi
376 internal class ContextualFlowRowOverflowScopeImpl(private val state: FlowLayoutOverflowState) :
377 FlowRowOverflowScope by FlowRowOverflowScopeImpl(state), ContextualFlowRowOverflowScope
378
379 @ExperimentalLayoutApi
380 internal class ContextualFlowColumnOverflowScopeImpl(private val state: FlowLayoutOverflowState) :
381 FlowColumnOverflowScope by FlowColumnOverflowScopeImpl(state),
382 ContextualFlowColumnOverflowScope
383
384 @Composable
385 internal fun contextualRowMeasurementHelper(
386 horizontalArrangement: Arrangement.Horizontal,
387 verticalArrangement: Arrangement.Vertical,
388 itemVerticalAlignment: Alignment.Vertical,
389 maxItemsInMainAxis: Int,
390 maxLines: Int,
391 overflowState: FlowLayoutOverflowState,
392 itemCount: Int,
393 overflowComposables: List<@Composable () -> Unit>,
394 getComposable: @Composable (index: Int, info: FlowLineInfo) -> Unit
395 ): (SubcomposeMeasureScope, Constraints) -> MeasureResult {
396 return remember(
397 horizontalArrangement,
398 verticalArrangement,
399 itemVerticalAlignment,
400 maxItemsInMainAxis,
401 maxLines,
402 overflowState,
403 itemCount,
404 getComposable
<lambda>null405 ) {
406 FlowMeasureLazyPolicy(
407 isHorizontal = true,
408 horizontalArrangement = horizontalArrangement,
409 mainAxisSpacing = horizontalArrangement.spacing,
410 crossAxisAlignment = CrossAxisAlignment.vertical(itemVerticalAlignment),
411 verticalArrangement = verticalArrangement,
412 crossAxisArrangementSpacing = verticalArrangement.spacing,
413 maxItemsInMainAxis = maxItemsInMainAxis,
414 itemCount = itemCount,
415 overflow = overflowState,
416 maxLines = maxLines,
417 getComposable = getComposable,
418 overflowComposables = overflowComposables
419 )
420 .getMeasurePolicy()
421 }
422 }
423
424 @Composable
425 internal fun contextualColumnMeasureHelper(
426 verticalArrangement: Arrangement.Vertical,
427 horizontalArrangement: Arrangement.Horizontal,
428 itemHorizontalAlignment: Alignment.Horizontal,
429 maxItemsInMainAxis: Int,
430 maxLines: Int,
431 overflowState: FlowLayoutOverflowState,
432 itemCount: Int,
433 overflowComposables: List<@Composable () -> Unit>,
434 getComposable: @Composable (index: Int, info: FlowLineInfo) -> Unit
435 ): (SubcomposeMeasureScope, Constraints) -> MeasureResult {
436 return remember(
437 verticalArrangement,
438 horizontalArrangement,
439 itemHorizontalAlignment,
440 maxItemsInMainAxis,
441 maxLines,
442 overflowState,
443 itemCount,
444 getComposable
<lambda>null445 ) {
446 FlowMeasureLazyPolicy(
447 isHorizontal = false,
448 verticalArrangement = verticalArrangement,
449 mainAxisSpacing = verticalArrangement.spacing,
450 crossAxisAlignment = CrossAxisAlignment.horizontal(itemHorizontalAlignment),
451 horizontalArrangement = horizontalArrangement,
452 crossAxisArrangementSpacing = horizontalArrangement.spacing,
453 maxItemsInMainAxis = maxItemsInMainAxis,
454 itemCount = itemCount,
455 overflow = overflowState,
456 maxLines = maxLines,
457 overflowComposables = overflowComposables,
458 getComposable = getComposable
459 )
460 .getMeasurePolicy()
461 }
462 }
463
464 /** Returns a Flow Measure Policy */
465 @OptIn(ExperimentalLayoutApi::class)
466 private data class FlowMeasureLazyPolicy(
467 override val isHorizontal: Boolean,
468 override val horizontalArrangement: Arrangement.Horizontal,
469 override val verticalArrangement: Arrangement.Vertical,
470 private val mainAxisSpacing: Dp,
471 override val crossAxisAlignment: CrossAxisAlignment,
472 private val crossAxisArrangementSpacing: Dp,
473 private val itemCount: Int,
474 private val maxLines: Int,
475 private val maxItemsInMainAxis: Int,
476 private val overflow: FlowLayoutOverflowState,
477 private val overflowComposables: List<@Composable () -> Unit>,
478 private val getComposable: @Composable (index: Int, info: FlowLineInfo) -> Unit
479 ) : FlowLineMeasurePolicy {
480
getMeasurePolicynull481 fun getMeasurePolicy(): (SubcomposeMeasureScope, Constraints) -> MeasureResult {
482 return { measureScope, constraints -> measureScope.measure(constraints) }
483 }
484
measurenull485 private fun SubcomposeMeasureScope.measure(constraints: Constraints): MeasureResult {
486 if (
487 itemCount <= 0 ||
488 (maxLines == 0 ||
489 maxItemsInMainAxis == 0 ||
490 constraints.maxHeight == 0 &&
491 overflow.type != FlowLayoutOverflow.OverflowType.Visible)
492 ) {
493 return layout(0, 0) {}
494 }
495 val measurablesIterator =
496 ContextualFlowItemIterator(itemCount) { index, info ->
497 this.subcompose(index) { getComposable(index, info) }
498 }
499 overflow.itemCount = itemCount
500 overflow.setOverflowMeasurables(this@FlowMeasureLazyPolicy, constraints) {
501 canExpand,
502 shownItemCount ->
503 val composableIndex = if (canExpand) 0 else 1
504 overflowComposables.getOrNull(composableIndex)?.run {
505 this@measure.subcompose("$canExpand$itemCount$shownItemCount", this).getOrNull(0)
506 }
507 }
508 return breakDownItems(
509 this@FlowMeasureLazyPolicy,
510 measurablesIterator,
511 mainAxisSpacing,
512 crossAxisArrangementSpacing,
513 OrientationIndependentConstraints(
514 constraints,
515 if (isHorizontal) {
516 LayoutOrientation.Horizontal
517 } else {
518 LayoutOrientation.Vertical
519 }
520 ),
521 maxItemsInMainAxis,
522 maxLines,
523 overflow
524 )
525 }
526 }
527
528 internal class ContextualFlowItemIterator(
529 private val itemCount: Int,
530 private val getMeasurables: (index: Int, info: FlowLineInfo) -> List<Measurable>
531 ) : Iterator<Measurable> {
532 private val _list: MutableList<Measurable> = mutableListOf()
533 private var itemIndex: Int = 0
534 private var listIndex = 0
535 val list: List<Measurable>
536 get() = _list
537
hasNextnull538 override fun hasNext(): Boolean {
539 return listIndex < list.size || itemIndex < itemCount
540 }
541
nextnull542 override fun next(): Measurable {
543 return getNext()
544 }
545
getNextnull546 internal fun getNext(info: FlowLineInfo = FlowLineInfo()): Measurable {
547 // when we are at the end of the list, we fetch a new item from getMeasurables
548 // and add to the list.
549 // otherwise, we continue through the list.
550 return if (listIndex < list.size) {
551 val measurable = list[listIndex]
552 listIndex++
553 measurable
554 } else if (itemIndex < itemCount) {
555 val measurables = getMeasurables(itemIndex, info)
556 itemIndex++
557 if (measurables.isEmpty()) {
558 next()
559 } else {
560 val measurable = measurables.first()
561 _list.addAll(measurables)
562 listIndex++
563 measurable
564 }
565 } else {
566 throw IndexOutOfBoundsException("No item returned at index call. Index: $itemIndex")
567 }
568 }
569 }
570
571 /**
572 * Contextual Line Info for the current lazy call for [ContextualFlowRow] or [ContextualFlowColumn]
573 */
574 internal class FlowLineInfo(
575 internal var lineIndex: Int = 0,
576 internal var positionInLine: Int = 0,
577 internal var maxMainAxisSize: Dp = 0.dp,
578 internal var maxCrossAxisSize: Dp = 0.dp,
579 ) {
580
581 /** To allow reuse of the same object to reduce allocation, simply update the same value */
updatenull582 internal fun update(
583 lineIndex: Int,
584 positionInLine: Int,
585 maxMainAxisSize: Dp,
586 maxCrossAxisSize: Dp,
587 ) {
588 this.lineIndex = lineIndex
589 this.positionInLine = positionInLine
590 this.maxMainAxisSize = maxMainAxisSize
591 this.maxCrossAxisSize = maxCrossAxisSize
592 }
593 }
594