1 /* <lambda>null2 * Copyright 2021 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.flowlayout 18 19 import androidx.compose.foundation.layout.PaddingValues 20 import androidx.compose.runtime.Composable 21 import androidx.compose.runtime.Immutable 22 import androidx.compose.runtime.Stable 23 import androidx.compose.ui.Alignment 24 import androidx.compose.ui.Modifier 25 import androidx.compose.ui.geometry.Offset 26 import androidx.compose.ui.layout.IntrinsicMeasurable 27 import androidx.compose.ui.layout.IntrinsicMeasureScope 28 import androidx.compose.ui.layout.Layout 29 import androidx.compose.ui.layout.Measurable 30 import androidx.compose.ui.layout.MeasurePolicy 31 import androidx.compose.ui.layout.MeasureResult 32 import androidx.compose.ui.layout.MeasureScope 33 import androidx.compose.ui.layout.Placeable 34 import androidx.compose.ui.layout.onGloballyPositioned 35 import androidx.compose.ui.node.Ref 36 import androidx.compose.ui.platform.LocalDensity 37 import androidx.compose.ui.test.junit4.createComposeRule 38 import androidx.compose.ui.unit.Constraints 39 import androidx.compose.ui.unit.Density 40 import androidx.compose.ui.unit.Dp 41 import androidx.compose.ui.unit.IntSize 42 import androidx.compose.ui.unit.constrain 43 import androidx.compose.ui.unit.constrainHeight 44 import androidx.compose.ui.unit.constrainWidth 45 import androidx.compose.ui.unit.dp 46 import androidx.compose.ui.unit.isFinite 47 import androidx.compose.ui.unit.offset 48 import org.junit.Assert.assertEquals 49 import org.junit.Assert.assertNotNull 50 import org.junit.Assert.fail 51 import org.junit.Rule 52 import java.util.concurrent.CountDownLatch 53 import kotlin.math.max 54 55 open class LayoutTest { 56 @get:Rule 57 val rule = createComposeRule() 58 59 internal fun Modifier.saveLayoutInfo( 60 size: Ref<IntSize>, 61 position: Ref<Offset>, 62 positionedLatch: CountDownLatch 63 ): Modifier = this then onGloballyPositioned { coordinates -> 64 size.value = IntSize(coordinates.size.width, coordinates.size.height) 65 position.value = coordinates.localToRoot(Offset(0f, 0f)) 66 positionedLatch.countDown() 67 } 68 69 @Composable 70 internal fun ConstrainedBox( 71 constraints: DpConstraints, 72 modifier: Modifier = Modifier, 73 content: @Composable () -> Unit 74 ) { 75 with(LocalDensity.current) { 76 val pxConstraints = Constraints(constraints) 77 val measurePolicy = object : MeasurePolicy { 78 @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") 79 override fun MeasureScope.measure( 80 measurables: List<Measurable>, 81 incomingConstraints: Constraints 82 ): MeasureResult { 83 val measurable = measurables.firstOrNull() 84 val childConstraints = incomingConstraints.constrain(Constraints(constraints)) 85 val placeable = measurable?.measure(childConstraints) 86 87 val layoutWidth = placeable?.width ?: childConstraints.minWidth 88 val layoutHeight = placeable?.height ?: childConstraints.minHeight 89 return layout(layoutWidth, layoutHeight) { 90 placeable?.placeRelative(0, 0) 91 } 92 } 93 94 override fun IntrinsicMeasureScope.minIntrinsicWidth( 95 measurables: List<IntrinsicMeasurable>, 96 height: Int 97 ): Int { 98 val width = measurables.firstOrNull()?.minIntrinsicWidth(height) ?: 0 99 return pxConstraints.constrainWidth(width) 100 } 101 102 override fun IntrinsicMeasureScope.minIntrinsicHeight( 103 measurables: List<IntrinsicMeasurable>, 104 width: Int 105 ): Int { 106 val height = measurables.firstOrNull()?.minIntrinsicHeight(width) ?: 0 107 return pxConstraints.constrainHeight(height) 108 } 109 110 override fun IntrinsicMeasureScope.maxIntrinsicWidth( 111 measurables: List<IntrinsicMeasurable>, 112 height: Int 113 ): Int { 114 val width = measurables.firstOrNull()?.maxIntrinsicWidth(height) ?: 0 115 return pxConstraints.constrainWidth(width) 116 } 117 118 override fun IntrinsicMeasureScope.maxIntrinsicHeight( 119 measurables: List<IntrinsicMeasurable>, 120 width: Int 121 ): Int { 122 val height = measurables.firstOrNull()?.maxIntrinsicHeight(width) ?: 0 123 return pxConstraints.constrainHeight(height) 124 } 125 } 126 Layout( 127 content = content, 128 modifier = modifier, 129 measurePolicy = measurePolicy 130 ) 131 } 132 } 133 134 /** 135 * Similar to [Constraints], but with constraint values expressed in [Dp]. 136 */ 137 @Immutable 138 data class DpConstraints( 139 @Stable 140 val minWidth: Dp = 0.dp, 141 @Stable 142 val maxWidth: Dp = Dp.Infinity, 143 @Stable 144 val minHeight: Dp = 0.dp, 145 @Stable 146 val maxHeight: Dp = Dp.Infinity 147 ) { 148 init { 149 require(minWidth.isFinite) { "Constraints#minWidth should be finite" } 150 require(minHeight.isFinite) { "Constraints#minHeight should be finite" } 151 require(!minWidth.value.isNaN()) { "Constraints#minWidth should not be NaN" } 152 require(!maxWidth.value.isNaN()) { "Constraints#maxWidth should not be NaN" } 153 require(!minHeight.value.isNaN()) { "Constraints#minHeight should not be NaN" } 154 require(!maxHeight.value.isNaN()) { "Constraints#maxHeight should not be NaN" } 155 require(minWidth <= maxWidth) { 156 "Constraints should be satisfiable, but minWidth > maxWidth" 157 } 158 require(minHeight <= maxHeight) { 159 "Constraints should be satisfiable, but minHeight > maxHeight" 160 } 161 require(minWidth >= 0.dp) { "Constraints#minWidth should be non-negative" } 162 require(maxWidth >= 0.dp) { "Constraints#maxWidth should be non-negative" } 163 require(minHeight >= 0.dp) { "Constraints#minHeight should be non-negative" } 164 require(maxHeight >= 0.dp) { "Constraints#maxHeight should be non-negative" } 165 } 166 } 167 168 /** 169 * Creates the [Constraints] corresponding to the current [DpConstraints]. 170 */ 171 @Stable 172 fun Density.Constraints(dpConstraints: DpConstraints) = Constraints( 173 minWidth = dpConstraints.minWidth.roundToPx(), 174 maxWidth = dpConstraints.maxWidth.roundToPx(), 175 minHeight = dpConstraints.minHeight.roundToPx(), 176 maxHeight = dpConstraints.maxHeight.roundToPx() 177 ) 178 179 internal fun assertEquals(expected: Offset?, actual: Offset?) { 180 assertNotNull("Null expected position", expected) 181 expected as Offset 182 assertNotNull("Null actual position", actual) 183 actual as Offset 184 185 assertEquals( 186 "Expected x ${expected.x} but obtained ${actual.x}", 187 expected.x, 188 actual.x, 189 0f 190 ) 191 assertEquals( 192 "Expected y ${expected.y} but obtained ${actual.y}", 193 expected.y, 194 actual.y, 195 0f 196 ) 197 if (actual.x != actual.x.toInt().toFloat()) { 198 fail("Expected integer x coordinate") 199 } 200 if (actual.y != actual.y.toInt().toFloat()) { 201 fail("Expected integer y coordinate") 202 } 203 } 204 205 @Composable 206 internal fun Container( 207 modifier: Modifier = Modifier, 208 padding: PaddingValues = PaddingValues(0.dp), 209 alignment: Alignment = Alignment.Center, 210 expanded: Boolean = false, 211 constraints: DpConstraints = DpConstraints(), 212 width: Dp? = null, 213 height: Dp? = null, 214 content: @Composable () -> Unit 215 ) { 216 Layout(content, modifier) { measurables, incomingConstraints -> 217 val containerConstraints = incomingConstraints.constrain( 218 Constraints(constraints) 219 .copy( 220 width?.roundToPx() ?: constraints.minWidth.roundToPx(), 221 width?.roundToPx() ?: constraints.maxWidth.roundToPx(), 222 height?.roundToPx() ?: constraints.minHeight.roundToPx(), 223 height?.roundToPx() ?: constraints.maxHeight.roundToPx() 224 ) 225 ) 226 val totalHorizontal = padding.calculateLeftPadding(layoutDirection).roundToPx() + 227 padding.calculateRightPadding(layoutDirection).roundToPx() 228 val totalVertical = padding.calculateTopPadding().roundToPx() + 229 padding.calculateBottomPadding().roundToPx() 230 val childConstraints = containerConstraints 231 .copy(minWidth = 0, minHeight = 0) 232 .offset(-totalHorizontal, -totalVertical) 233 var placeable: Placeable? = null 234 val containerWidth = if ((containerConstraints.hasFixedWidth || expanded) && 235 containerConstraints.hasBoundedWidth 236 ) { 237 containerConstraints.maxWidth 238 } else { 239 placeable = measurables.firstOrNull()?.measure(childConstraints) 240 max((placeable?.width ?: 0) + totalHorizontal, containerConstraints.minWidth) 241 } 242 val containerHeight = if ((containerConstraints.hasFixedHeight || expanded) && 243 containerConstraints.hasBoundedHeight 244 ) { 245 containerConstraints.maxHeight 246 } else { 247 if (placeable == null) { 248 placeable = measurables.firstOrNull()?.measure(childConstraints) 249 } 250 max((placeable?.height ?: 0) + totalVertical, containerConstraints.minHeight) 251 } 252 layout(containerWidth, containerHeight) { 253 val p = placeable ?: measurables.firstOrNull()?.measure(childConstraints) 254 p?.let { 255 val position = alignment.align( 256 IntSize(it.width + totalHorizontal, it.height + totalVertical), 257 IntSize(containerWidth, containerHeight), 258 layoutDirection 259 ) 260 it.place( 261 padding.calculateLeftPadding(layoutDirection).roundToPx() + position.x, 262 padding.calculateTopPadding().roundToPx() + position.y 263 ) 264 } 265 } 266 } 267 } 268 } 269