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