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