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