1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.foundation.layout
18 
19 import androidx.compose.foundation.layout.internal.checkPrecondition
20 import androidx.compose.ui.layout.AlignmentLine
21 import androidx.compose.ui.layout.Measurable
22 import androidx.compose.ui.layout.MeasureResult
23 import androidx.compose.ui.layout.MeasureScope
24 import androidx.compose.ui.layout.Placeable
25 import androidx.compose.ui.unit.Constraints
26 import androidx.compose.ui.util.fastCoerceAtLeast
27 import androidx.compose.ui.util.fastCoerceIn
28 import androidx.compose.ui.util.fastRoundToInt
29 import kotlin.math.max
30 import kotlin.math.min
31 import kotlin.math.sign
32 
33 internal interface RowColumnMeasurePolicy {
mainAxisSizenull34     fun Placeable.mainAxisSize(): Int
35 
36     fun Placeable.crossAxisSize(): Int
37 
38     fun populateMainAxisPositions(
39         mainAxisLayoutSize: Int,
40         childrenMainAxisSize: IntArray,
41         mainAxisPositions: IntArray,
42         measureScope: MeasureScope
43     )
44 
45     fun placeHelper(
46         placeables: Array<Placeable?>,
47         measureScope: MeasureScope,
48         beforeCrossAxisAlignmentLine: Int,
49         mainAxisPositions: IntArray,
50         mainAxisLayoutSize: Int,
51         crossAxisLayoutSize: Int,
52         crossAxisOffset: IntArray?,
53         currentLineIndex: Int,
54         startIndex: Int,
55         endIndex: Int
56     ): MeasureResult
57 
58     fun createConstraints(
59         mainAxisMin: Int,
60         crossAxisMin: Int,
61         mainAxisMax: Int,
62         crossAxisMax: Int,
63         isPrioritizing: Boolean = false
64     ): Constraints
65 }
66 
67 /**
68  * Measures the row and column
69  *
70  * @param measureScope The measure scope to retrieve density
71  * @param startIndex The startIndex (inclusive) when examining measurables, placeable and parentData
72  * @param endIndex The ending index (exclusive) when examining measurable, placeable and parentData
73  * @param crossAxisOffset The offset to apply to the cross axis when placing
74  * @param currentLineIndex The index of the current line if in a multi-row/column setting like
75  *   [FlowRow]
76  */
77 internal fun RowColumnMeasurePolicy.measure(
78     mainAxisMin: Int,
79     crossAxisMin: Int,
80     mainAxisMax: Int,
81     crossAxisMax: Int,
82     arrangementSpacingInt: Int,
83     measureScope: MeasureScope,
84     measurables: List<Measurable>,
85     placeables: Array<Placeable?>,
86     startIndex: Int,
87     endIndex: Int,
88     crossAxisOffset: IntArray? = null,
89     currentLineIndex: Int = 0,
90 ): MeasureResult {
91     val arrangementSpacingPx = arrangementSpacingInt.toLong()
92 
93     var totalWeight = 0f
94     var fixedSpace = 0
95     var crossAxisSpace = 0
96     var weightChildrenCount = 0
97 
98     var anyAlignBy = false
99     val subSize = endIndex - startIndex
100     val childrenMainAxisSize = IntArray(subSize)
101 
102     var beforeCrossAxisAlignmentLine = 0
103     var afterCrossAxisAlignmentLine = 0
104     // First measure children with zero weight.
105     var spaceAfterLastNoWeight = 0
106 
107     for (i in startIndex until endIndex) {
108         val child = measurables[i]
109         val parentData = child.rowColumnParentData
110         val weight = parentData.weight
111         anyAlignBy = anyAlignBy || parentData.isRelative
112 
113         if (weight > 0f) {
114             totalWeight += weight
115             ++weightChildrenCount
116         } else {
117             val crossAxisDesiredSize =
118                 if (crossAxisMax == Constraints.Infinity) null
119                 else
120                     parentData?.flowLayoutData?.let {
121                         (it.fillCrossAxisFraction * crossAxisMax).fastRoundToInt()
122                     }
123             val remaining = mainAxisMax - fixedSpace
124             val placeable =
125                 placeables[i]
126                     ?: child.measure(
127                         // Ask for preferred main axis size.
128                         createConstraints(
129                             mainAxisMin = 0,
130                             crossAxisMin = crossAxisDesiredSize ?: 0,
131                             mainAxisMax =
132                                 if (mainAxisMax == Constraints.Infinity) {
133                                     Constraints.Infinity
134                                 } else {
135                                     remaining.fastCoerceAtLeast(0)
136                                 },
137                             crossAxisMax = crossAxisDesiredSize ?: crossAxisMax
138                         )
139                     )
140             val placeableMainAxisSize = placeable.mainAxisSize()
141             val placeableCrossAxisSize = placeable.crossAxisSize()
142             childrenMainAxisSize[i - startIndex] = placeableMainAxisSize
143             spaceAfterLastNoWeight =
144                 min(arrangementSpacingInt, (remaining - placeableMainAxisSize).fastCoerceAtLeast(0))
145             fixedSpace += placeableMainAxisSize + spaceAfterLastNoWeight
146             crossAxisSpace = max(crossAxisSpace, placeableCrossAxisSize)
147             placeables[i] = placeable
148         }
149     }
150 
151     var weightedSpace = 0
152     if (weightChildrenCount == 0) {
153         // fixedSpace contains an extra spacing after the last non-weight child.
154         fixedSpace -= spaceAfterLastNoWeight
155     } else {
156         // Measure the rest according to their weights in the remaining main axis space.
157         val targetSpace =
158             if (mainAxisMax != Constraints.Infinity) {
159                 mainAxisMax
160             } else {
161                 mainAxisMin
162             }
163         val arrangementSpacingTotal = arrangementSpacingPx * (weightChildrenCount - 1)
164         val remainingToTarget =
165             (targetSpace - fixedSpace - arrangementSpacingTotal).fastCoerceAtLeast(0)
166 
167         val weightUnitSpace = remainingToTarget / totalWeight
168         var remainder = remainingToTarget
169         for (i in startIndex until endIndex) {
170             val measurable = measurables[i]
171             val itemWeight = measurable.rowColumnParentData.weight
172             val weightedSize = (weightUnitSpace * itemWeight)
173             remainder -= weightedSize.fastRoundToInt()
174         }
175 
176         for (i in startIndex until endIndex) {
177             if (placeables[i] == null) {
178                 val child = measurables[i]
179                 val parentData = child.rowColumnParentData
180                 val weight = parentData.weight
181                 val crossAxisDesiredSize =
182                     if (crossAxisMax == Constraints.Infinity) null
183                     else
184                         parentData?.flowLayoutData?.let {
185                             (it.fillCrossAxisFraction * crossAxisMax).fastRoundToInt()
186                         }
187                 checkPrecondition(weight > 0) { "All weights <= 0 should have placeables" }
188                 // After the weightUnitSpace rounding, the total space going to be occupied
189                 // can be smaller or larger than remainingToTarget. Here we distribute the
190                 // loss or gain remainder evenly to the first children.
191                 val remainderUnit = remainder.sign
192                 remainder -= remainderUnit
193                 val weightedSize = (weightUnitSpace * weight)
194                 val childMainAxisSize = max(0, weightedSize.fastRoundToInt() + remainderUnit)
195                 val childConstraints: Constraints =
196                     createConstraints(
197                         mainAxisMin =
198                             if (parentData.fill && childMainAxisSize != Constraints.Infinity) {
199                                 childMainAxisSize
200                             } else {
201                                 0
202                             },
203                         crossAxisMin = crossAxisDesiredSize ?: 0,
204                         mainAxisMax = childMainAxisSize,
205                         crossAxisMax = crossAxisDesiredSize ?: crossAxisMax,
206                         isPrioritizing = true
207                     )
208                 val placeable = child.measure(childConstraints)
209                 val placeableMainAxisSize = placeable.mainAxisSize()
210                 val placeableCrossAxisSize = placeable.crossAxisSize()
211                 childrenMainAxisSize[i - startIndex] = placeableMainAxisSize
212                 weightedSpace += placeableMainAxisSize
213                 crossAxisSpace = max(crossAxisSpace, placeableCrossAxisSize)
214                 placeables[i] = placeable
215             }
216         }
217         weightedSpace =
218             (weightedSpace + arrangementSpacingTotal)
219                 .toInt()
220                 .fastCoerceIn(0, mainAxisMax - fixedSpace)
221     }
222 
223     // we've done this check in weights as to avoid going through another loop
224     if (anyAlignBy) {
225         for (i in startIndex until endIndex) {
226             val placeable = placeables[i]
227             val parentData = placeable!!.rowColumnParentData
228             val alignmentLinePosition =
229                 parentData.crossAxisAlignment?.calculateAlignmentLinePosition(placeable)
230             alignmentLinePosition?.let {
231                 val placeableCrossAxisSize = placeable.crossAxisSize()
232                 beforeCrossAxisAlignmentLine =
233                     max(
234                         beforeCrossAxisAlignmentLine,
235                         if (it != AlignmentLine.Unspecified) alignmentLinePosition else 0
236                     )
237                 afterCrossAxisAlignmentLine =
238                     max(
239                         afterCrossAxisAlignmentLine,
240                         placeableCrossAxisSize -
241                             if (it != AlignmentLine.Unspecified) {
242                                 it
243                             } else {
244                                 placeableCrossAxisSize
245                             }
246                     )
247             }
248         }
249     }
250 
251     // Compute the Row or Column size and position the children.
252     val mainAxisLayoutSize = max((fixedSpace + weightedSpace).fastCoerceAtLeast(0), mainAxisMin)
253     val crossAxisLayoutSize =
254         maxOf(
255             crossAxisSpace,
256             crossAxisMin,
257             beforeCrossAxisAlignmentLine + afterCrossAxisAlignmentLine
258         )
259     val mainAxisPositions = IntArray(subSize)
260     populateMainAxisPositions(
261         mainAxisLayoutSize,
262         childrenMainAxisSize,
263         mainAxisPositions,
264         measureScope
265     )
266 
267     return placeHelper(
268         placeables,
269         measureScope,
270         beforeCrossAxisAlignmentLine,
271         mainAxisPositions,
272         mainAxisLayoutSize,
273         crossAxisLayoutSize,
274         crossAxisOffset,
275         currentLineIndex,
276         startIndex,
277         endIndex
278     )
279 }
280