[go: nahoru, domu]

blob: 97c63ce668c0f01ee13fc89c5a9d0f13f242c722 [file] [log] [blame]
Andrey Kulikov9ef62df2021-04-26 12:38:02 +01001/*
2 * Copyright 2020 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
Mihai Popa139388e2021-11-16 16:26:12 +000017package androidx.compose.foundation.lazy.list
Andrey Kulikov9ef62df2021-04-26 12:38:02 +010018
19import androidx.compose.foundation.layout.Spacer
Andrey Kulikov8b694352022-04-12 15:17:37 +010020import androidx.compose.foundation.layout.fillMaxWidth
Andrey Kulikov9ef62df2021-04-26 12:38:02 +010021import androidx.compose.foundation.layout.height
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +010022import androidx.compose.foundation.layout.width
Mihai Popa139388e2021-11-16 16:26:12 +000023import androidx.compose.foundation.lazy.LazyColumn
24import androidx.compose.foundation.lazy.LazyListState
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +010025import androidx.compose.foundation.lazy.items
Mihai Popa139388e2021-11-16 16:26:12 +000026import androidx.compose.foundation.lazy.rememberLazyListState
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +010027import androidx.compose.runtime.Composable
Andrey Kulikov9ef62df2021-04-26 12:38:02 +010028import androidx.compose.ui.Modifier
Andrei Shikov69c9a582022-10-11 02:33:32 +010029import androidx.compose.ui.layout.layout
Andrey Kulikov9ef62df2021-04-26 12:38:02 +010030import androidx.compose.ui.platform.testTag
31import androidx.compose.ui.test.assertIsDisplayed
32import androidx.compose.ui.test.assertIsNotDisplayed
33import androidx.compose.ui.test.junit4.createComposeRule
34import androidx.compose.ui.test.onNodeWithTag
Andrei Shikov69c9a582022-10-11 02:33:32 +010035import androidx.compose.ui.unit.IntOffset
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +010036import androidx.compose.ui.unit.dp
Andrey Kulikov9ef62df2021-04-26 12:38:02 +010037import androidx.test.ext.junit.runners.AndroidJUnit4
38import androidx.test.filters.LargeTest
39import com.google.common.truth.Truth
40import kotlinx.coroutines.runBlocking
41import org.junit.Rule
42import org.junit.Test
43import org.junit.runner.RunWith
44
45@LargeTest
46@RunWith(AndroidJUnit4::class)
47class LazyListSlotsReuseTest {
48
49 @get:Rule
50 val rule = createComposeRule()
51
52 val itemsSizePx = 30f
53 val itemsSizeDp = with(rule.density) { itemsSizePx.toDp() }
54
55 @Test
56 fun scroll1ItemScrolledOffItemIsKeptForReuse() {
57 lateinit var state: LazyListState
58 rule.setContent {
59 state = rememberLazyListState()
60 LazyColumn(
61 Modifier.height(itemsSizeDp * 1.5f),
62 state
63 ) {
64 items(100) {
65 Spacer(Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it"))
66 }
67 }
68 }
69
70 rule.onNodeWithTag("0")
71 .assertIsDisplayed()
72
73 rule.runOnIdle {
74 runBlocking {
75 state.scrollToItem(1)
76 }
77 }
78
79 rule.onNodeWithTag("0")
80 .assertExists()
81 .assertIsNotDisplayed()
82 rule.onNodeWithTag("1")
83 .assertIsDisplayed()
84 }
85
86 @Test
87 fun scroll2ItemsScrolledOffItemsAreKeptForReuse() {
88 lateinit var state: LazyListState
89 rule.setContent {
90 state = rememberLazyListState()
91 LazyColumn(
92 Modifier.height(itemsSizeDp * 1.5f),
93 state
94 ) {
95 items(100) {
96 Spacer(Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it"))
97 }
98 }
99 }
100
101 rule.onNodeWithTag("0")
102 .assertIsDisplayed()
103 rule.onNodeWithTag("1")
104 .assertIsDisplayed()
105
106 rule.runOnIdle {
107 runBlocking {
108 state.scrollToItem(2)
109 }
110 }
111
112 rule.onNodeWithTag("0")
113 .assertExists()
114 .assertIsNotDisplayed()
115 rule.onNodeWithTag("1")
116 .assertExists()
117 .assertIsNotDisplayed()
118 rule.onNodeWithTag("2")
119 .assertIsDisplayed()
120 }
121
122 @Test
Andrey Kulikov8b694352022-04-12 15:17:37 +0100123 fun checkMaxItemsKeptForReuse() {
124 lateinit var state: LazyListState
125 rule.setContent {
126 state = rememberLazyListState()
127 LazyColumn(
128 Modifier.height(itemsSizeDp * (DefaultMaxItemsToRetain + 0.5f)),
129 state
130 ) {
131 items(100) {
132 Spacer(Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it"))
133 }
134 }
135 }
136
137 rule.runOnIdle {
138 runBlocking {
139 state.scrollToItem(DefaultMaxItemsToRetain + 1)
140 }
141 }
142
143 repeat(DefaultMaxItemsToRetain) {
144 rule.onNodeWithTag("$it")
145 .assertExists()
146 .assertIsNotDisplayed()
147 }
148 rule.onNodeWithTag("$DefaultMaxItemsToRetain")
149 .assertDoesNotExist()
150 rule.onNodeWithTag("${DefaultMaxItemsToRetain + 1}")
151 .assertIsDisplayed()
152 }
153
154 @Test
Andrey Kulikov9ef62df2021-04-26 12:38:02 +0100155 fun scroll3Items2OfScrolledOffItemsAreKeptForReuse() {
156 lateinit var state: LazyListState
157 rule.setContent {
158 state = rememberLazyListState()
159 LazyColumn(
160 Modifier.height(itemsSizeDp * 1.5f),
161 state
162 ) {
163 items(100) {
164 Spacer(Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it"))
165 }
166 }
167 }
168
169 rule.onNodeWithTag("0")
170 .assertIsDisplayed()
171 rule.onNodeWithTag("1")
172 .assertIsDisplayed()
173
174 rule.runOnIdle {
175 runBlocking {
176 // after this step 0 and 1 are in reusable buffer
177 state.scrollToItem(2)
178
179 // this step requires one item and will take the last item from the buffer - item
180 // 1 plus will put 2 in the buffer. so expected buffer is items 2 and 0
181 state.scrollToItem(3)
182 }
183 }
184
185 // recycled
186 rule.onNodeWithTag("1")
187 .assertDoesNotExist()
188
189 // in buffer
190 rule.onNodeWithTag("0")
191 .assertExists()
192 .assertIsNotDisplayed()
193 rule.onNodeWithTag("2")
194 .assertExists()
195 .assertIsNotDisplayed()
196
197 // visible
198 rule.onNodeWithTag("3")
199 .assertIsDisplayed()
200 rule.onNodeWithTag("4")
201 .assertIsDisplayed()
202 }
203
204 @Test
205 fun doMultipleScrollsOneByOne() {
206 lateinit var state: LazyListState
207 rule.setContent {
208 state = rememberLazyListState()
209 LazyColumn(
210 Modifier.height(itemsSizeDp * 1.5f),
211 state
212 ) {
213 items(100) {
214 Spacer(Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it"))
215 }
216 }
217 }
218 rule.runOnIdle {
219 runBlocking {
220 state.scrollToItem(1) // buffer is [0]
221 state.scrollToItem(2) // 0 used, buffer is [1]
222 state.scrollToItem(3) // 1 used, buffer is [2]
223 state.scrollToItem(4) // 2 used, buffer is [3]
224 }
225 }
226
227 // recycled
228 rule.onNodeWithTag("0")
229 .assertDoesNotExist()
230 rule.onNodeWithTag("1")
231 .assertDoesNotExist()
232 rule.onNodeWithTag("2")
233 .assertDoesNotExist()
234
235 // in buffer
236 rule.onNodeWithTag("3")
237 .assertExists()
238 .assertIsNotDisplayed()
239
240 // visible
241 rule.onNodeWithTag("4")
242 .assertIsDisplayed()
243 rule.onNodeWithTag("5")
244 .assertIsDisplayed()
245 }
246
247 @Test
248 fun scrollBackwardOnce() {
249 lateinit var state: LazyListState
250 rule.setContent {
251 state = rememberLazyListState(10)
252 LazyColumn(
253 Modifier.height(itemsSizeDp * 1.5f),
254 state
255 ) {
256 items(100) {
257 Spacer(Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it"))
258 }
259 }
260 }
261 rule.runOnIdle {
262 runBlocking {
263 state.scrollToItem(8) // buffer is [10, 11]
264 }
265 }
266
267 // in buffer
268 rule.onNodeWithTag("10")
269 .assertExists()
270 .assertIsNotDisplayed()
271 rule.onNodeWithTag("11")
272 .assertExists()
273 .assertIsNotDisplayed()
274
275 // visible
276 rule.onNodeWithTag("8")
277 .assertIsDisplayed()
278 rule.onNodeWithTag("9")
279 .assertIsDisplayed()
280 }
281
282 @Test
283 fun scrollBackwardOneByOne() {
284 lateinit var state: LazyListState
285 rule.setContent {
286 state = rememberLazyListState(10)
287 LazyColumn(
288 Modifier.height(itemsSizeDp * 1.5f),
289 state
290 ) {
291 items(100) {
292 Spacer(Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it"))
293 }
294 }
295 }
296 rule.runOnIdle {
297 runBlocking {
298 state.scrollToItem(9) // buffer is [11]
299 state.scrollToItem(7) // 11 reused, buffer is [9]
300 state.scrollToItem(6) // 9 reused, buffer is [8]
301 }
302 }
303
304 // in buffer
305 rule.onNodeWithTag("8")
306 .assertExists()
307 .assertIsNotDisplayed()
308
309 // visible
310 rule.onNodeWithTag("6")
311 .assertIsDisplayed()
312 rule.onNodeWithTag("7")
313 .assertIsDisplayed()
314 }
315
316 @Test
317 fun scrollingBackReusesTheSameSlot() {
318 lateinit var state: LazyListState
319 var counter0 = 0
Andrei Shikov69c9a582022-10-11 02:33:32 +0100320 var counter1 = 0
321
322 val measureCountModifier0 = Modifier.layout { measurable, constraints ->
323 counter0++
324 val placeable = measurable.measure(constraints)
325 layout(placeable.width, placeable.height) {
326 placeable.place(IntOffset.Zero)
327 }
328 }
329
330 val measureCountModifier1 = Modifier.layout { measurable, constraints ->
331 counter1++
332 val placeable = measurable.measure(constraints)
333 layout(placeable.width, placeable.height) {
334 placeable.place(IntOffset.Zero)
335 }
336 }
337
Andrey Kulikov9ef62df2021-04-26 12:38:02 +0100338 rule.setContent {
339 state = rememberLazyListState()
340 LazyColumn(
341 Modifier.height(itemsSizeDp * 1.5f),
342 state
343 ) {
344 items(100) {
Andrei Shikov69c9a582022-10-11 02:33:32 +0100345 val modifier = when (it) {
346 0 -> measureCountModifier0
347 1 -> measureCountModifier1
348 else -> Modifier
Andrey Kulikov9ef62df2021-04-26 12:38:02 +0100349 }
Andrei Shikov69c9a582022-10-11 02:33:32 +0100350 Spacer(
351 Modifier
352 .height(itemsSizeDp)
353 .fillParentMaxWidth()
354 .testTag("$it")
355 .then(modifier)
356 )
Andrey Kulikov9ef62df2021-04-26 12:38:02 +0100357 }
358 }
359 }
360 rule.runOnIdle {
361 runBlocking {
362 state.scrollToItem(2) // buffer is [0, 1]
363 state.scrollToItem(0) // scrolled back, 0 and 1 are reused back. buffer: [2, 3]
364 }
365 }
366
367 rule.runOnIdle {
Andrei Shikov69c9a582022-10-11 02:33:32 +0100368 Truth.assertWithMessage("Item 0 measured $counter0 times, expected 1.")
369 .that(counter0).isEqualTo(1)
370 Truth.assertWithMessage("Item 1 measured $counter1 times, expected 1.")
371 .that(counter1).isEqualTo(1)
Andrey Kulikov9ef62df2021-04-26 12:38:02 +0100372 }
373
374 rule.onNodeWithTag("0")
375 .assertIsDisplayed()
376 rule.onNodeWithTag("1")
377 .assertIsDisplayed()
378
379 rule.onNodeWithTag("2")
380 .assertExists()
381 .assertIsNotDisplayed()
382 rule.onNodeWithTag("3")
383 .assertExists()
384 .assertIsNotDisplayed()
385 }
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +0100386
387 @Test
388 fun differentContentTypes() {
389 lateinit var state: LazyListState
Andrey Kulikov8b694352022-04-12 15:17:37 +0100390 val visibleItemsCount = (DefaultMaxItemsToRetain + 1) * 2
391 val startOfType1 = DefaultMaxItemsToRetain + 1
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +0100392 rule.setContent {
393 state = rememberLazyListState()
394 LazyColumn(
Andrey Kulikov8b694352022-04-12 15:17:37 +0100395 Modifier.height(itemsSizeDp * (visibleItemsCount - 0.5f)),
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +0100396 state
397 ) {
398 items(
399 100,
Andrey Kulikov8b694352022-04-12 15:17:37 +0100400 contentType = { if (it >= startOfType1) 1 else 0 }
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +0100401 ) {
Andrey Kulikov8b694352022-04-12 15:17:37 +0100402 Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +0100403 }
404 }
405 }
406
Andrey Kulikov8b694352022-04-12 15:17:37 +0100407 for (i in 0 until visibleItemsCount) {
408 rule.onNodeWithTag("$i")
409 .assertIsDisplayed()
410 }
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +0100411
412 rule.runOnIdle {
413 runBlocking {
Andrey Kulikov8b694352022-04-12 15:17:37 +0100414 state.scrollToItem(visibleItemsCount)
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +0100415 }
416 }
417
Andrey Kulikov8b694352022-04-12 15:17:37 +0100418 rule.onNodeWithTag("$visibleItemsCount")
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +0100419 .assertIsDisplayed()
420
Andrey Kulikov8b694352022-04-12 15:17:37 +0100421 // [DefaultMaxItemsToRetain] items of type 0 are left for reuse
422 for (i in 0 until DefaultMaxItemsToRetain) {
423 rule.onNodeWithTag("$i")
424 .assertExists()
425 .assertIsNotDisplayed()
426 }
427 rule.onNodeWithTag("$DefaultMaxItemsToRetain")
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +0100428 .assertDoesNotExist()
429
Andrey Kulikov8b694352022-04-12 15:17:37 +0100430 // and 7 items of type 1
431 for (i in startOfType1 until startOfType1 + DefaultMaxItemsToRetain) {
432 rule.onNodeWithTag("$i")
433 .assertExists()
434 .assertIsNotDisplayed()
435 }
436 rule.onNodeWithTag("${startOfType1 + DefaultMaxItemsToRetain}")
Andrey Kulikov1ca16bc2021-08-06 19:17:32 +0100437 .assertDoesNotExist()
438 }
439
440 @Test
441 fun differentTypesFromDifferentItemCalls() {
442 lateinit var state: LazyListState
443 rule.setContent {
444 state = rememberLazyListState()
445 LazyColumn(
446 Modifier.height(itemsSizeDp * 2.5f),
447 state
448 ) {
449 val content = @Composable { tag: String ->
450 Spacer(Modifier.height(itemsSizeDp).width(10.dp).testTag(tag))
451 }
452 item(contentType = "not-to-reuse-0") {
453 content("0")
454 }
455 item(contentType = "reuse") {
456 content("1")
457 }
458 items(
459 List(100) { it + 2 },
460 contentType = { if (it == 10) "reuse" else "not-to-reuse-$it" }) {
461 content("$it")
462 }
463 }
464 }
465
466 rule.runOnIdle {
467 runBlocking {
468 state.scrollToItem(2)
469 // now items 0 and 1 are put into reusables
470 }
471 }
472
473 rule.onNodeWithTag("0")
474 .assertExists()
475 .assertIsNotDisplayed()
476 rule.onNodeWithTag("1")
477 .assertExists()
478 .assertIsNotDisplayed()
479
480 rule.runOnIdle {
481 runBlocking {
482 state.scrollToItem(9)
483 // item 10 should reuse slot 1
484 }
485 }
486
487 rule.onNodeWithTag("0")
488 .assertExists()
489 .assertIsNotDisplayed()
490 rule.onNodeWithTag("1")
491 .assertDoesNotExist()
492 rule.onNodeWithTag("9")
493 .assertIsDisplayed()
494 rule.onNodeWithTag("10")
495 .assertIsDisplayed()
496 rule.onNodeWithTag("11")
497 .assertIsDisplayed()
498 }
Andrey Kulikov9ef62df2021-04-26 12:38:02 +0100499}
Andrey Kulikov8b694352022-04-12 15:17:37 +0100500
501private val DefaultMaxItemsToRetain = 7