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  *      https://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 com.google.accompanist.adaptive
18 
19 import androidx.compose.ui.layout.AlignmentLine
20 import androidx.compose.ui.layout.Measurable
21 import androidx.compose.ui.layout.MeasureScope
22 import androidx.compose.ui.layout.Placeable
23 import androidx.compose.ui.unit.Constraints
24 import androidx.compose.ui.unit.Density
25 import androidx.compose.ui.unit.Dp
26 import androidx.compose.ui.unit.LayoutDirection
27 import kotlin.math.max
28 import kotlin.math.min
29 import kotlin.math.roundToInt
30 import kotlin.math.sign
31 
32 /**
33  * Copied from:
34  * RowColumnMeasurementHelper.kt
35  * https://android-review.googlesource.com/c/platform/frameworks/support/+/2260390/27/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnMeasurementHelper.kt
36  *
37  * The only changes were updating access modifiers and making RowColumnMeasurementHelper an open class
38  */
39 
40 /**
41  * This is a data class that holds the determined width, height of a row,
42  * and information on how to retrieve main axis and cross axis positions.
43  */
44 internal class RowColumnMeasureHelperResult(
45     val crossAxisSize: Int,
46     val mainAxisSize: Int,
47     val startIndex: Int,
48     val endIndex: Int,
49     val beforeCrossAxisAlignmentLine: Int,
50     val mainAxisPositions: IntArray,
51 )
52 
53 /**
54  * RowColumnMeasurementHelper
55  * Measures the row and column without placing, useful for reusing row/column logic
56  */
57 internal open class RowColumnMeasurementHelper(
58     val orientation: LayoutOrientation,
59     val arrangement: (Int, IntArray, LayoutDirection, Density, IntArray) -> Unit,
60     val arrangementSpacing: Dp,
61     val crossAxisSize: SizeMode,
62     val crossAxisAlignment: CrossAxisAlignment,
63     val measurables: List<Measurable>,
64     val placeables: Array<Placeable?>
65 ) {
66 
67     private val rowColumnParentData = Array(measurables.size) {
68         measurables[it].rowColumnParentData
69     }
70 
71     fun Placeable.mainAxisSize() =
72         if (orientation == LayoutOrientation.Horizontal) width else height
73 
74     fun Placeable.crossAxisSize() =
75         if (orientation == LayoutOrientation.Horizontal) height else width
76 
77     /**
78      * Measures the row and column without placing, useful for reusing row/column logic
79      *
80      * @param measureScope The measure scope to retrieve density
81      * @param constraints The desired constraints for the startIndex and endIndex
82      * can hold null items if not measured.
83      * @param startIndex The startIndex (inclusive) when examining measurables, placeable
84      * and parentData
85      * @param endIndex The ending index (exclusive) when examinning measurable, placeable
86      * and parentData
87      */
88     fun measureWithoutPlacing(
89         measureScope: MeasureScope,
90         constraints: Constraints,
91         startIndex: Int,
92         endIndex: Int
93     ): RowColumnMeasureHelperResult {
94         @Suppress("NAME_SHADOWING")
95         val constraints = OrientationIndependentConstraints(constraints, orientation)
96         val arrangementSpacingPx = with(measureScope) {
97             arrangementSpacing.roundToPx()
98         }
99 
100         var totalWeight = 0f
101         var fixedSpace = 0
102         var crossAxisSpace = 0
103         var weightChildrenCount = 0
104 
105         var anyAlignBy = false
106         val subSize = endIndex - startIndex
107 
108         // First measure children with zero weight.
109         var spaceAfterLastNoWeight = 0
110         for (i in startIndex until endIndex) {
111             val child = measurables[i]
112             val parentData = rowColumnParentData[i]
113             val weight = parentData.weight
114 
115             if (weight > 0f) {
116                 totalWeight += weight
117                 ++weightChildrenCount
118             } else {
119                 val mainAxisMax = constraints.mainAxisMax
120                 val placeable = placeables[i] ?: child.measure(
121                     // Ask for preferred main axis size.
122                     constraints.copy(
123                         mainAxisMin = 0,
124                         mainAxisMax = if (mainAxisMax == Constraints.Infinity) {
125                             Constraints.Infinity
126                         } else {
127                             mainAxisMax - fixedSpace
128                         },
129                         crossAxisMin = 0
130                     ).toBoxConstraints(orientation)
131                 )
132                 spaceAfterLastNoWeight = min(
133                     arrangementSpacingPx,
134                     mainAxisMax - fixedSpace - placeable.mainAxisSize()
135                 )
136                 fixedSpace += placeable.mainAxisSize() + spaceAfterLastNoWeight
137                 crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
138                 anyAlignBy = anyAlignBy || parentData.isRelative
139                 placeables[i] = placeable
140             }
141         }
142 
143         var weightedSpace = 0
144         if (weightChildrenCount == 0) {
145             // fixedSpace contains an extra spacing after the last non-weight child.
146             fixedSpace -= spaceAfterLastNoWeight
147         } else {
148             // Measure the rest according to their weights in the remaining main axis space.
149             val targetSpace =
150                 if (totalWeight > 0f && constraints.mainAxisMax != Constraints.Infinity) {
151                     constraints.mainAxisMax
152                 } else {
153                     constraints.mainAxisMin
154                 }
155             val remainingToTarget =
156                 targetSpace - fixedSpace - arrangementSpacingPx * (weightChildrenCount - 1)
157 
158             val weightUnitSpace = if (totalWeight > 0) remainingToTarget / totalWeight else 0f
159             var remainder = remainingToTarget - (startIndex until endIndex).sumOf {
160                 (weightUnitSpace * rowColumnParentData[it].weight).roundToInt()
161             }
162 
163             for (i in startIndex until endIndex) {
164                 if (placeables[i] == null) {
165                     val child = measurables[i]
166                     val parentData = rowColumnParentData[i]
167                     val weight = parentData.weight
168                     check(weight > 0) { "All weights <= 0 should have placeables" }
169                     // After the weightUnitSpace rounding, the total space going to be occupied
170                     // can be smaller or larger than remainingToTarget. Here we distribute the
171                     // loss or gain remainder evenly to the first children.
172                     val remainderUnit = remainder.sign
173                     remainder -= remainderUnit
174                     val childMainAxisSize = max(
175                         0,
176                         (weightUnitSpace * weight).roundToInt() + remainderUnit
177                     )
178                     val placeable = child.measure(
179                         OrientationIndependentConstraints(
180                             if (parentData.fill && childMainAxisSize != Constraints.Infinity) {
181                                 childMainAxisSize
182                             } else {
183                                 0
184                             },
185                             childMainAxisSize,
186                             0,
187                             constraints.crossAxisMax
188                         ).toBoxConstraints(orientation)
189                     )
190                     weightedSpace += placeable.mainAxisSize()
191                     crossAxisSpace = max(crossAxisSpace, placeable.crossAxisSize())
192                     anyAlignBy = anyAlignBy || parentData.isRelative
193                     placeables[i] = placeable
194                 }
195             }
196             weightedSpace = (weightedSpace + arrangementSpacingPx * (weightChildrenCount - 1))
197                 .coerceAtMost(constraints.mainAxisMax - fixedSpace)
198         }
199 
200         var beforeCrossAxisAlignmentLine = 0
201         var afterCrossAxisAlignmentLine = 0
202         if (anyAlignBy) {
203             for (i in startIndex until endIndex) {
204                 val placeable = placeables[i]!!
205                 val parentData = rowColumnParentData[i]
206                 val alignmentLinePosition = parentData.crossAxisAlignment
207                     ?.calculateAlignmentLinePosition(placeable)
208                 if (alignmentLinePosition != null) {
209                     beforeCrossAxisAlignmentLine = max(
210                         beforeCrossAxisAlignmentLine,
211                         alignmentLinePosition.let {
212                             if (it != AlignmentLine.Unspecified) it else 0
213                         }
214                     )
215                     afterCrossAxisAlignmentLine = max(
216                         afterCrossAxisAlignmentLine,
217                         placeable.crossAxisSize() -
218                             (
219                                 alignmentLinePosition.let {
220                                     if (it != AlignmentLine.Unspecified) {
221                                         it
222                                     } else {
223                                         placeable.crossAxisSize()
224                                     }
225                                 }
226                                 )
227                     )
228                 }
229             }
230         }
231 
232         // Compute the Row or Column size and position the children.
233         val mainAxisLayoutSize = max(fixedSpace + weightedSpace, constraints.mainAxisMin)
234         val crossAxisLayoutSize = if (constraints.crossAxisMax != Constraints.Infinity &&
235             crossAxisSize == SizeMode.Expand
236         ) {
237             constraints.crossAxisMax
238         } else {
239             max(
240                 crossAxisSpace,
241                 max(
242                     constraints.crossAxisMin,
243                     beforeCrossAxisAlignmentLine + afterCrossAxisAlignmentLine
244                 )
245             )
246         }
247         val mainAxisPositions = IntArray(subSize) { 0 }
248         val childrenMainAxisSize = IntArray(subSize) { index ->
249             placeables[index + startIndex]!!.mainAxisSize()
250         }
251 
252         return RowColumnMeasureHelperResult(
253             mainAxisSize = mainAxisLayoutSize,
254             crossAxisSize = crossAxisLayoutSize,
255             startIndex = startIndex,
256             endIndex = endIndex,
257             beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine,
258             mainAxisPositions = mainAxisPositions(
259                 mainAxisLayoutSize,
260                 childrenMainAxisSize,
261                 mainAxisPositions,
262                 measureScope
263             )
264         )
265     }
266 
267     private fun mainAxisPositions(
268         mainAxisLayoutSize: Int,
269         childrenMainAxisSize: IntArray,
270         mainAxisPositions: IntArray,
271         measureScope: MeasureScope
272     ): IntArray {
273         arrangement(
274             mainAxisLayoutSize,
275             childrenMainAxisSize,
276             measureScope.layoutDirection,
277             measureScope,
278             mainAxisPositions
279         )
280         return mainAxisPositions
281     }
282 
283     protected fun getCrossAxisPosition(
284         placeable: Placeable,
285         parentData: RowColumnParentData?,
286         crossAxisLayoutSize: Int,
287         layoutDirection: LayoutDirection,
288         beforeCrossAxisAlignmentLine: Int
289     ): Int {
290         val childCrossAlignment = parentData?.crossAxisAlignment ?: crossAxisAlignment
291         return childCrossAlignment.align(
292             size = crossAxisLayoutSize - placeable.crossAxisSize(),
293             layoutDirection = if (orientation == LayoutOrientation.Horizontal) {
294                 LayoutDirection.Ltr
295             } else {
296                 layoutDirection
297             },
298             placeable = placeable,
299             beforeCrossAxisAlignmentLine = beforeCrossAxisAlignmentLine
300         )
301     }
302 
303     fun placeHelper(
304         placeableScope: Placeable.PlacementScope,
305         measureResult: RowColumnMeasureHelperResult,
306         crossAxisOffset: Int,
307         layoutDirection: LayoutDirection,
308     ) {
309         with(placeableScope) {
310             for (i in measureResult.startIndex until measureResult.endIndex) {
311                 val placeable = placeables[i]
312                 placeable!!
313                 val mainAxisPositions = measureResult.mainAxisPositions
314                 val crossAxisPosition = getCrossAxisPosition(
315                     placeable,
316                     (measurables[i].parentData as? RowColumnParentData),
317                     measureResult.crossAxisSize,
318                     layoutDirection,
319                     measureResult.beforeCrossAxisAlignmentLine
320                 ) + crossAxisOffset
321                 if (orientation == LayoutOrientation.Horizontal) {
322                     placeable.place(
323                         mainAxisPositions[i - measureResult.startIndex],
324                         crossAxisPosition
325                     )
326                 } else {
327                     placeable.place(
328                         crossAxisPosition,
329                         mainAxisPositions[i - measureResult.startIndex]
330                     )
331                 }
332             }
333         }
334     }
335 }
336