• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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