1 /*
<lambda>null2  * 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.ui.layout
18 
19 import androidx.compose.foundation.layout.Box
20 import androidx.compose.foundation.layout.size
21 import androidx.compose.runtime.Composable
22 import androidx.compose.runtime.getValue
23 import androidx.compose.runtime.mutableStateOf
24 import androidx.compose.runtime.setValue
25 import androidx.compose.ui.Modifier
26 import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
27 import androidx.compose.ui.platform.testTag
28 import androidx.compose.ui.test.TestActivity
29 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
30 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
31 import androidx.compose.ui.test.junit4.createAndroidComposeRule
32 import androidx.compose.ui.test.onNodeWithTag
33 import androidx.compose.ui.unit.Constraints
34 import androidx.compose.ui.unit.Dp
35 import androidx.compose.ui.unit.dp
36 import androidx.test.ext.junit.runners.AndroidJUnit4
37 import androidx.test.filters.MediumTest
38 import com.google.common.truth.Truth.assertThat
39 import org.junit.Before
40 import org.junit.Rule
41 import org.junit.Test
42 import org.junit.runner.RunWith
43 
44 @MediumTest
45 @RunWith(AndroidJUnit4::class)
46 class MultiContentLayoutTest {
47 
48     @get:Rule val rule = createAndroidComposeRule<TestActivity>()
49 
50     @get:Rule val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
51 
52     var size: Dp = Dp.Unspecified
53 
54     @Before
55     fun before() {
56         size = with(rule.density) { 10.toDp() }
57     }
58 
59     @Test
60     fun haveOneSlotWithOneItem() {
61         rule.setContent {
62             val first = @Composable { Item(0) }
63             Layout(contents = listOf(first), modifier = Modifier.size(100.dp)) {
64                 (firstSlot),
65                 constraints ->
66                 assertThat(firstSlot.size).isEqualTo(1)
67                 layoutAsRow(constraints, firstSlot)
68             }
69         }
70 
71         assertItemsLaidOutAsRow(0..0)
72     }
73 
74     @Test
75     fun haveOneSlotWithNoItems() {
76         rule.setContent {
77             val first = @Composable {}
78             Layout(contents = listOf(first), modifier = Modifier.size(100.dp)) {
79                 (firstSlot),
80                 constraints ->
81                 assertThat(firstSlot.size).isEqualTo(0)
82                 layoutAsRow(constraints, firstSlot)
83             }
84         }
85     }
86 
87     @Test
88     fun haveOneSlotWithTwoItems() {
89         rule.setContent {
90             val first =
91                 @Composable {
92                     Item(0)
93                     Item(1)
94                 }
95             Layout(contents = listOf(first), modifier = Modifier.size(100.dp)) {
96                 (firstSlot),
97                 constraints ->
98                 assertThat(firstSlot.size).isEqualTo(2)
99                 layoutAsRow(constraints, firstSlot)
100             }
101         }
102 
103         assertItemsLaidOutAsRow(0..1)
104     }
105 
106     @Test
107     fun haveTwoSlotsWithOneItem() {
108         rule.setContent {
109             val first = @Composable { Item(0) }
110             val second = @Composable { Item(1) }
111             Layout(contents = listOf(first, second), modifier = Modifier.size(100.dp)) {
112                 (firstSlot, secondSlot),
113                 constraints ->
114                 assertThat(firstSlot.size).isEqualTo(1)
115                 assertThat(secondSlot.size).isEqualTo(1)
116                 layoutAsRow(constraints, firstSlot + secondSlot)
117             }
118         }
119 
120         assertItemsLaidOutAsRow(0..1)
121     }
122 
123     @Test
124     fun haveTwoSlotsWithNoItemsInOne() {
125         rule.setContent {
126             val first = @Composable { Item(0) }
127             val second = @Composable {}
128             Layout(contents = listOf(first, second), modifier = Modifier.size(100.dp)) {
129                 (firstSlot, secondSlot),
130                 constraints ->
131                 assertThat(firstSlot.size).isEqualTo(1)
132                 assertThat(secondSlot.size).isEqualTo(0)
133                 layoutAsRow(constraints, firstSlot + secondSlot)
134             }
135         }
136 
137         assertItemsLaidOutAsRow(0..0)
138     }
139 
140     @Test
141     fun haveTwoSlotsWithDifferentNumberOfItems() {
142         rule.setContent {
143             val first =
144                 @Composable {
145                     Item(0)
146                     Item(1)
147                 }
148             val second = @Composable { Item(2) }
149             Layout(contents = listOf(first, second), modifier = Modifier.size(100.dp)) {
150                 (firstSlot, secondSlot),
151                 constraints ->
152                 assertThat(firstSlot.size).isEqualTo(2)
153                 assertThat(secondSlot.size).isEqualTo(1)
154                 layoutAsRow(constraints, firstSlot + secondSlot)
155             }
156         }
157 
158         assertItemsLaidOutAsRow(0..2)
159     }
160 
161     @Test
162     fun haveFiveSlots() {
163         rule.setContent {
164             val first = @Composable { Item(0) }
165             val second = @Composable { Item(1) }
166             val third =
167                 @Composable {
168                     Item(2)
169                     Item(3)
170                 }
171             val fourth = @Composable {}
172             val fifth = @Composable { Item(4) }
173             Layout(
174                 contents = listOf(first, second, third, fourth, fifth),
175                 modifier = Modifier.size(100.dp)
176             ) { (firstSlot, secondSlot, thirdSlot, fourthSlot, fifthSlot), constraints ->
177                 assertThat(firstSlot.size).isEqualTo(1)
178                 assertThat(secondSlot.size).isEqualTo(1)
179                 assertThat(thirdSlot.size).isEqualTo(2)
180                 assertThat(fourthSlot.size).isEqualTo(0)
181                 assertThat(fifthSlot.size).isEqualTo(1)
182                 layoutAsRow(
183                     constraints,
184                     firstSlot + secondSlot + thirdSlot + fourthSlot + fifthSlot
185                 )
186             }
187         }
188 
189         assertItemsLaidOutAsRow(0..4)
190     }
191 
192     @Test
193     fun updatingItemCount() {
194         var itemCount by mutableStateOf(1)
195         rule.setContent {
196             val first = @Composable { repeat(itemCount) { Item(it) } }
197             Layout(contents = listOf(first), modifier = Modifier.size(100.dp)) {
198                 (firstSlot),
199                 constraints ->
200                 layoutAsRow(constraints, firstSlot)
201             }
202         }
203 
204         assertItemsLaidOutAsRow(0..0)
205 
206         rule.runOnIdle { itemCount = 3 }
207 
208         assertItemsLaidOutAsRow(0..2)
209     }
210 
211     @Test
212     fun updatingSlotCount() {
213         var slotCount by mutableStateOf(1)
214         rule.setContent {
215             val contents =
216                 buildList<@Composable () -> Unit> { repeat(slotCount) { add { Item(it) } } }
217             Layout(contents = contents, modifier = Modifier.size(100.dp)) { slots, constraints ->
218                 assertThat(slots.size).isEqualTo(contents.size)
219                 layoutAsRow(constraints, slots.flatten())
220             }
221         }
222 
223         assertItemsLaidOutAsRow(0..0)
224 
225         rule.runOnIdle { slotCount = 3 }
226 
227         assertItemsLaidOutAsRow(0..2)
228     }
229 
230     @Test
231     fun defaultIntrinsics() {
232         rule.setContent {
233             Layout({
234                 val first =
235                     @Composable {
236                         BoxWithIntrinsics(1, 2, 100, 200)
237                         BoxWithIntrinsics(4, 3, 300, 400)
238                     }
239                 val second = @Composable { BoxWithIntrinsics(10, 11, 12, 13) }
240                 Layout(contents = listOf(first, second)) { (_, secondSlot), constraints ->
241                     val placeable = secondSlot.first().measure(constraints)
242                     layout(placeable.width, placeable.height) { placeable.place(0, 0) }
243                 }
244             }) { measurables, _ ->
245                 val box = measurables[0]
246                 assertThat(box.minIntrinsicWidth(1000)).isEqualTo(10)
247                 assertThat(box.minIntrinsicHeight(1000)).isEqualTo(11)
248                 assertThat(box.maxIntrinsicWidth(1000)).isEqualTo(12)
249                 assertThat(box.maxIntrinsicHeight(1000)).isEqualTo(13)
250                 layout(10, 10) {}
251             }
252         }
253     }
254 
255     @Test
256     fun customIntrinsics() {
257         rule.setContent {
258             Layout({
259                 val first =
260                     @Composable {
261                         BoxWithIntrinsics(1, 2, 100, 200)
262                         BoxWithIntrinsics(4, 3, 300, 400)
263                     }
264                 val second = @Composable { BoxWithIntrinsics(10, 11, 12, 13) }
265                 Layout(
266                     contents = listOf(first, second),
267                     measurePolicy =
268                         object : MultiContentMeasurePolicy {
269                             override fun MeasureScope.measure(
270                                 measurables: List<List<Measurable>>,
271                                 constraints: Constraints
272                             ) = throw IllegalStateException("shouldn't be called")
273 
274                             override fun IntrinsicMeasureScope.minIntrinsicWidth(
275                                 measurables: List<List<IntrinsicMeasurable>>,
276                                 height: Int
277                             ): Int = measurables[1].first().minIntrinsicWidth(height)
278 
279                             override fun IntrinsicMeasureScope.minIntrinsicHeight(
280                                 measurables: List<List<IntrinsicMeasurable>>,
281                                 width: Int
282                             ): Int = measurables[1].first().minIntrinsicHeight(width)
283 
284                             override fun IntrinsicMeasureScope.maxIntrinsicWidth(
285                                 measurables: List<List<IntrinsicMeasurable>>,
286                                 height: Int
287                             ): Int = measurables[1].first().maxIntrinsicWidth(height)
288 
289                             override fun IntrinsicMeasureScope.maxIntrinsicHeight(
290                                 measurables: List<List<IntrinsicMeasurable>>,
291                                 width: Int
292                             ): Int = measurables[1].first().maxIntrinsicHeight(width)
293                         }
294                 )
295             }) { measurables, _ ->
296                 val box = measurables[0]
297                 assertThat(box.minIntrinsicWidth(1000)).isEqualTo(10)
298                 assertThat(box.minIntrinsicHeight(1000)).isEqualTo(11)
299                 assertThat(box.maxIntrinsicWidth(1000)).isEqualTo(12)
300                 assertThat(box.maxIntrinsicHeight(1000)).isEqualTo(13)
301                 layout(10, 10) {}
302             }
303         }
304     }
305 
306     private fun assertItemsLaidOutAsRow(intRange: IntRange) {
307         var currentX = 0.dp
308         intRange.forEach {
309             rule
310                 .onNodeWithTag("$it")
311                 .assertLeftPositionInRootIsEqualTo(currentX)
312                 .assertTopPositionInRootIsEqualTo(0.dp)
313             currentX += size
314         }
315 
316         rule.onNodeWithTag("${intRange.first - 1}").assertDoesNotExist()
317         rule.onNodeWithTag("${intRange.last + 1}").assertDoesNotExist()
318     }
319 
320     @Composable
321     fun Item(id: Int) {
322         Box(Modifier.size(size).testTag("$id"))
323     }
324 }
325 
layoutAsRownull326 private fun MeasureScope.layoutAsRow(
327     constraints: Constraints,
328     list: List<Measurable>
329 ): MeasureResult {
330     val childConstraints =
331         Constraints(maxWidth = constraints.maxWidth, maxHeight = constraints.maxHeight)
332     return layout(constraints.maxWidth, constraints.maxHeight) {
333         var currentX = 0
334         list.forEach {
335             val placeable = it.measure(childConstraints)
336             placeable.place(currentX, 0)
337             currentX += placeable.width
338         }
339     }
340 }
341 
342 @Composable
BoxWithIntrinsicsnull343 private fun BoxWithIntrinsics(minWidth: Int, minHeight: Int, maxWidth: Int, maxHeight: Int) {
344     Layout(
345         measurePolicy =
346             object : MeasurePolicy {
347                 override fun MeasureScope.measure(
348                     measurables: List<Measurable>,
349                     constraints: Constraints
350                 ): MeasureResult {
351                     TODO("Not yet implemented")
352                 }
353 
354                 override fun IntrinsicMeasureScope.minIntrinsicWidth(
355                     measurables: List<IntrinsicMeasurable>,
356                     height: Int
357                 ) = minWidth
358 
359                 override fun IntrinsicMeasureScope.minIntrinsicHeight(
360                     measurables: List<IntrinsicMeasurable>,
361                     width: Int
362                 ) = minHeight
363 
364                 override fun IntrinsicMeasureScope.maxIntrinsicWidth(
365                     measurables: List<IntrinsicMeasurable>,
366                     height: Int
367                 ) = maxWidth
368 
369                 override fun IntrinsicMeasureScope.maxIntrinsicHeight(
370                     measurables: List<IntrinsicMeasurable>,
371                     width: Int
372                 ) = maxHeight
373             }
374     )
375 }
376