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