blob: 00870c1d9d92227b19a9926f652620d85c4655ef [file] [log] [blame]
jnichol5e1f7902021-08-04 18:38:26 +01001/*
2 * 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 * 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
17package androidx.wear.compose.material
18
jnicholda0477e2021-08-10 17:15:45 +010019import androidx.compose.foundation.gestures.animateScrollBy
20import androidx.compose.foundation.gestures.scrollBy
jnichol5e1f7902021-08-04 18:38:26 +010021import androidx.compose.foundation.layout.Arrangement
22import androidx.compose.foundation.layout.Box
23import androidx.compose.foundation.layout.PaddingValues
24import androidx.compose.foundation.layout.requiredSize
25import androidx.compose.runtime.Composable
jnicholda0477e2021-08-10 17:15:45 +010026import androidx.compose.runtime.Stable
jnichol5e1f7902021-08-04 18:38:26 +010027import androidx.compose.runtime.getValue
28import androidx.compose.runtime.mutableStateOf
jnicholda0477e2021-08-10 17:15:45 +010029import androidx.compose.runtime.rememberCoroutineScope
jnichol5e1f7902021-08-04 18:38:26 +010030import androidx.compose.runtime.setValue
31import androidx.compose.ui.Modifier
jnichol79900f52021-09-01 09:57:34 +010032import androidx.compose.ui.platform.testTag
33import androidx.compose.ui.test.assertIsDisplayed
jnichol5e1f7902021-08-04 18:38:26 +010034import androidx.compose.ui.test.junit4.createComposeRule
jnichol79900f52021-09-01 09:57:34 +010035import androidx.compose.ui.test.onNodeWithTag
jnichol5e1f7902021-08-04 18:38:26 +010036import androidx.compose.ui.unit.Dp
37import androidx.compose.ui.unit.dp
38import androidx.test.ext.junit.runners.AndroidJUnit4
39import androidx.test.filters.MediumTest
jnichol5e1f7902021-08-04 18:38:26 +010040import com.google.common.truth.Truth.assertThat
jnicholda0477e2021-08-10 17:15:45 +010041import kotlinx.coroutines.CoroutineScope
42import kotlinx.coroutines.launch
43import kotlinx.coroutines.runBlocking
jnicholcdbe7e22021-12-01 19:04:23 +000044import org.junit.Before
45import org.junit.Rule
46import org.junit.Test
47import org.junit.runner.RunWith
jnichol5e1f7902021-08-04 18:38:26 +010048import kotlin.math.roundToInt
49
50@MediumTest
51@RunWith(AndroidJUnit4::class)
jnichold40ad762021-08-31 10:36:41 +010052public class ScalingLazyListLayoutInfoTest {
jnichol5e1f7902021-08-04 18:38:26 +010053 @get:Rule
54 val rule = createComposeRule()
55
56 private var itemSizePx: Int = 50
57 private var itemSizeDp: Dp = Dp.Infinity
58 private var defaultItemSpacingDp: Dp = 4.dp
59 private var defaultItemSpacingPx = Int.MAX_VALUE
60
61 @Before
62 fun before() {
63 with(rule.density) {
64 itemSizeDp = itemSizePx.toDp()
65 defaultItemSpacingPx = defaultItemSpacingDp.roundToPx()
66 }
67 }
68
69 @Test
70 fun visibleItemsAreCorrect() {
jnichold40ad762021-08-31 10:36:41 +010071 lateinit var state: ScalingLazyListState
jnichol5e1f7902021-08-04 18:38:26 +010072 rule.setContent {
73 ScalingLazyColumn(
jnichold40ad762021-08-31 10:36:41 +010074 state = rememberScalingLazyListState().also { state = it },
jnichol5e1f7902021-08-04 18:38:26 +010075 modifier = Modifier.requiredSize(
76 itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
77 ),
jnichol50c68822022-01-17 15:08:52 +000078 autoCentering = false
jnichol5e1f7902021-08-04 18:38:26 +010079 ) {
80 items(5) {
81 Box(Modifier.requiredSize(itemSizeDp))
82 }
83 }
84 }
85
jnicholfd4289f2022-01-25 12:36:59 +000086 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
87 rule.waitUntil { state.initialized.value }
jnichol5e1f7902021-08-04 18:38:26 +010088 rule.runOnIdle {
89 state.layoutInfo.assertVisibleItems(count = 4)
90 }
91 }
92
93 @Test
jnichol79900f52021-09-01 09:57:34 +010094 fun visibleItemsAreCorrectForReverseLayout() {
95 lateinit var state: ScalingLazyListState
96 rule.setContent {
97 ScalingLazyColumn(
98 state = rememberScalingLazyListState().also { state = it },
99 modifier = Modifier.requiredSize(
100 itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
101 ),
jnichol50c68822022-01-17 15:08:52 +0000102 reverseLayout = true,
103 autoCentering = false
jnichol79900f52021-09-01 09:57:34 +0100104 ) {
105 items(5) {
106 Box(Modifier.requiredSize(itemSizeDp))
107 }
108 }
109 }
110
jnicholfd4289f2022-01-25 12:36:59 +0000111 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
112 rule.waitUntil { state.initialized.value }
jnichol79900f52021-09-01 09:57:34 +0100113 rule.runOnIdle {
jnichol50c68822022-01-17 15:08:52 +0000114 assertThat(state.centerItemIndex).isEqualTo(1)
jnichol79900f52021-09-01 09:57:34 +0100115 state.layoutInfo.assertVisibleItems(count = 4)
116 }
117 }
118
119 @Test
jnichol50c68822022-01-17 15:08:52 +0000120 fun visibleItemsAreCorrectForReverseLayoutWithAutoCentering() {
121 lateinit var state: ScalingLazyListState
122 rule.setContent {
123 ScalingLazyColumn(
124 state = rememberScalingLazyListState().also { state = it },
125 modifier = Modifier.requiredSize(
126 itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
127 ),
128 reverseLayout = true,
129 autoCentering = true
130 ) {
131 items(5) {
132 Box(Modifier.requiredSize(itemSizeDp))
133 }
134 }
135 }
136
jnicholfd4289f2022-01-25 12:36:59 +0000137 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
138 rule.waitUntil { state.initialized.value }
jnichol50c68822022-01-17 15:08:52 +0000139 rule.runOnIdle {
140 assertThat(state.centerItemIndex).isEqualTo(0)
141 assertThat(state.centerItemScrollOffset).isEqualTo(0)
142 state.layoutInfo.assertVisibleItems(count = 3)
143 }
144 }
145
146 @Test
jnicholda0477e2021-08-10 17:15:45 +0100147 fun visibleItemsAreCorrectAfterScrolling() {
jnichold40ad762021-08-31 10:36:41 +0100148 lateinit var state: ScalingLazyListState
jnicholda0477e2021-08-10 17:15:45 +0100149 rule.setContent {
150 ScalingLazyColumn(
jnichold40ad762021-08-31 10:36:41 +0100151 state = rememberScalingLazyListState().also { state = it },
jnicholda0477e2021-08-10 17:15:45 +0100152 modifier = Modifier.requiredSize(
153 itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
154 ),
jnichol50c68822022-01-17 15:08:52 +0000155 autoCentering = false
jnicholda0477e2021-08-10 17:15:45 +0100156 ) {
157 items(5) {
158 Box(Modifier.requiredSize(itemSizeDp))
159 }
160 }
161 }
162
jnicholfd4289f2022-01-25 12:36:59 +0000163 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
164 rule.waitUntil { state.initialized.value }
jnicholda0477e2021-08-10 17:15:45 +0100165 rule.runOnIdle {
166 runBlocking {
167 state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
168 }
169 state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
170 }
171 }
172
173 @Test
jnicholefc72842021-10-05 18:51:17 +0100174 fun itemLargerThanViewPortDoesNotGetScaled() {
175 lateinit var state: ScalingLazyListState
176 rule.setContent {
177 ScalingLazyColumn(
178 state = rememberScalingLazyListState().also { state = it },
179 modifier = Modifier.requiredSize(
180 itemSizeDp
181 ),
jnichol50c68822022-01-17 15:08:52 +0000182 autoCentering = false
jnicholefc72842021-10-05 18:51:17 +0100183 ) {
184 items(5) {
185 Box(Modifier.requiredSize(itemSizeDp * 5))
186 }
187 }
188 }
189
jnicholfd4289f2022-01-25 12:36:59 +0000190 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
191 rule.waitUntil { state.initialized.value }
jnicholefc72842021-10-05 18:51:17 +0100192 rule.runOnIdle {
193 runBlocking {
194 state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
195 }
196 val firstItem = state.layoutInfo.visibleItemsInfo.first()
197 assertThat(firstItem.offset).isLessThan(0)
198 assertThat(firstItem.offset + firstItem.size).isGreaterThan(itemSizePx)
199 assertThat(state.layoutInfo.visibleItemsInfo.first().scale).isEqualTo(1.0f)
200 }
201 }
202
203 @Test
204 fun itemStraddlingCenterLineDoesNotGetScaled() {
205 lateinit var state: ScalingLazyListState
jnicholcdbe7e22021-12-01 19:04:23 +0000206 val centerItemIndex = 2
jnicholefc72842021-10-05 18:51:17 +0100207 rule.setContent {
208 ScalingLazyColumn(
jnicholcdbe7e22021-12-01 19:04:23 +0000209 state = rememberScalingLazyListState(centerItemIndex).also { state = it },
jnicholefc72842021-10-05 18:51:17 +0100210 modifier = Modifier.requiredSize(
211 itemSizeDp * 3
jnicholcdbe7e22021-12-01 19:04:23 +0000212 ),
jnicholefc72842021-10-05 18:51:17 +0100213 ) {
214 items(5) {
215 Box(Modifier.requiredSize(itemSizeDp))
216 }
217 }
218 }
219
jnicholfd4289f2022-01-25 12:36:59 +0000220 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
221 rule.waitUntil { state.initialized.value }
jnicholefc72842021-10-05 18:51:17 +0100222 rule.runOnIdle {
223 // Get the middle item on the screen
jnicholcdbe7e22021-12-01 19:04:23 +0000224 val centerScreenItem =
225 state.layoutInfo.visibleItemsInfo.find { it.index == centerItemIndex }
226 // and confirm its offset is 0
227 assertThat(centerScreenItem!!.offset).isEqualTo(0)
jnicholefc72842021-10-05 18:51:17 +0100228 // And that it is not scaled
jnicholcdbe7e22021-12-01 19:04:23 +0000229 assertThat(centerScreenItem.scale).isEqualTo(1.0f)
jnicholefc72842021-10-05 18:51:17 +0100230 }
231 }
232
233 @Test
jnichol79900f52021-09-01 09:57:34 +0100234 fun visibleItemsAreCorrectAfterScrollingReverseLayout() {
235 lateinit var state: ScalingLazyListState
236 rule.setContent {
237 ScalingLazyColumn(
238 state = rememberScalingLazyListState().also { state = it },
239 modifier = Modifier.requiredSize(
240 itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
241 ),
242 reverseLayout = true,
jnichol50c68822022-01-17 15:08:52 +0000243 autoCentering = false
jnichol79900f52021-09-01 09:57:34 +0100244 ) {
245 items(5) {
246 Box(Modifier.requiredSize(itemSizeDp))
247 }
248 }
249 }
250
jnicholfd4289f2022-01-25 12:36:59 +0000251 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
252 rule.waitUntil { state.initialized.value }
jnichol79900f52021-09-01 09:57:34 +0100253 rule.runOnIdle {
254 runBlocking {
255 state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
256 }
257 state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
258 }
259 }
260
261 @Test
jnicholcdbe7e22021-12-01 19:04:23 +0000262 fun visibleItemsAreCorrectCenterPivotNoOffset() {
jnichold40ad762021-08-31 10:36:41 +0100263 lateinit var state: ScalingLazyListState
jnichol5e1f7902021-08-04 18:38:26 +0100264 rule.setContent {
265 ScalingLazyColumn(
jnicholcdbe7e22021-12-01 19:04:23 +0000266 state = rememberScalingLazyListState(2).also { state = it },
jnichol5e1f7902021-08-04 18:38:26 +0100267 modifier = Modifier.requiredSize(
jnicholcdbe7e22021-12-01 19:04:23 +0000268 itemSizeDp * 2f + defaultItemSpacingDp * 1f
jnichol5e1f7902021-08-04 18:38:26 +0100269 ),
270 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
271 ) {
272 items(5) {
273 Box(Modifier.requiredSize(itemSizeDp))
274 }
275 }
276 }
277
jnicholfd4289f2022-01-25 12:36:59 +0000278 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
279 rule.waitUntil { state.initialized.value }
jnichol5e1f7902021-08-04 18:38:26 +0100280 rule.runOnIdle {
jnicholcdbe7e22021-12-01 19:04:23 +0000281 state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
282 assertThat(state.centerItemIndex).isEqualTo(2)
283 assertThat(state.centerItemScrollOffset).isEqualTo(0)
284 }
285 }
286
287 @Test
288 fun visibleItemsAreCorrectCenterPivotWithOffset() {
289 lateinit var state: ScalingLazyListState
290 rule.setContent {
291 ScalingLazyColumn(
292 state = rememberScalingLazyListState(2, -5).also { state = it },
293 modifier = Modifier.requiredSize(
294 itemSizeDp * 2f + defaultItemSpacingDp * 1f
295 ),
296 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f)
297 ) {
298 items(5) {
299 Box(Modifier.requiredSize(itemSizeDp))
300 }
301 }
302 }
303
jnicholfd4289f2022-01-25 12:36:59 +0000304 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
305 rule.waitUntil { state.initialized.value }
jnicholcdbe7e22021-12-01 19:04:23 +0000306 rule.runOnIdle {
307 state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
308 assertThat(state.centerItemIndex).isEqualTo(2)
309 assertThat(state.centerItemScrollOffset).isEqualTo(-5)
310 }
311 }
312
313 @Test
314 fun visibleItemsAreCorrectCenterPivotNoOffsetReverseLayout() {
315 lateinit var state: ScalingLazyListState
316 rule.setContent {
317 ScalingLazyColumn(
318 state = rememberScalingLazyListState(2).also { state = it },
319 modifier = Modifier.requiredSize(
320 itemSizeDp * 2f + defaultItemSpacingDp * 1f
321 ),
322 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
323 reverseLayout = true
324 ) {
325 items(5) {
326 Box(Modifier.requiredSize(itemSizeDp))
327 }
328 }
329 }
330
jnicholfd4289f2022-01-25 12:36:59 +0000331 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
332 rule.waitUntil { state.initialized.value }
jnicholcdbe7e22021-12-01 19:04:23 +0000333 rule.runOnIdle {
334 state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
335 assertThat(state.centerItemIndex).isEqualTo(2)
336 assertThat(state.centerItemScrollOffset).isEqualTo(0)
337 }
338 }
339
340 @Test
341 fun visibleItemsAreCorrectCenterPivotWithOffsetReverseLayout() {
342 lateinit var state: ScalingLazyListState
343 rule.setContent {
344 ScalingLazyColumn(
345 state = rememberScalingLazyListState(2, -5).also { state = it },
346 modifier = Modifier.requiredSize(
347 itemSizeDp * 2f + defaultItemSpacingDp * 1f
348 ),
349 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
350 reverseLayout = true
351 ) {
352 items(5) {
353 Box(Modifier.requiredSize(itemSizeDp))
354 }
355 }
356 }
357
jnicholfd4289f2022-01-25 12:36:59 +0000358 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
359 rule.waitUntil { state.initialized.value }
jnicholcdbe7e22021-12-01 19:04:23 +0000360 rule.runOnIdle {
361 state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1)
362 assertThat(state.centerItemIndex).isEqualTo(2)
363 assertThat(state.centerItemScrollOffset).isEqualTo(-5)
jnichol5e1f7902021-08-04 18:38:26 +0100364 }
365 }
366
367 @Test
jnichol79900f52021-09-01 09:57:34 +0100368 fun visibleItemsAreCorrectNoScalingForReverseLayout() {
369 lateinit var state: ScalingLazyListState
370 rule.setContent {
371 ScalingLazyColumn(
jnicholcdbe7e22021-12-01 19:04:23 +0000372 state = rememberScalingLazyListState(8).also { state = it },
jnichol79900f52021-09-01 09:57:34 +0100373 modifier = Modifier.requiredSize(
jnicholcdbe7e22021-12-01 19:04:23 +0000374 itemSizeDp * 4f + defaultItemSpacingDp * 3f
jnichol79900f52021-09-01 09:57:34 +0100375 ),
376 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
377 reverseLayout = true
378 ) {
jnicholcdbe7e22021-12-01 19:04:23 +0000379 items(15) {
380 Box(Modifier.requiredSize(itemSizeDp).testTag("Item:$it"))
jnichol79900f52021-09-01 09:57:34 +0100381 }
382 }
383 }
384
jnicholfd4289f2022-01-25 12:36:59 +0000385 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
386 rule.waitUntil { state.initialized.value }
jnichol79900f52021-09-01 09:57:34 +0100387 rule.waitForIdle()
388
389 // Assert that items are being shown at the end of the parent as this is reverseLayout
jnicholcdbe7e22021-12-01 19:04:23 +0000390 rule.onNodeWithTag(testTag = "Item:8").assertIsDisplayed()
jnichol79900f52021-09-01 09:57:34 +0100391
392 rule.runOnIdle {
jnicholcdbe7e22021-12-01 19:04:23 +0000393 state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
jnichol79900f52021-09-01 09:57:34 +0100394 }
395 }
396
397 @Test
jnicholda0477e2021-08-10 17:15:45 +0100398 fun visibleItemsAreCorrectAfterScrollNoScaling() {
jnichold40ad762021-08-31 10:36:41 +0100399 lateinit var state: ScalingLazyListState
jnicholda0477e2021-08-10 17:15:45 +0100400 rule.setContent {
401 ScalingLazyColumn(
jnichold40ad762021-08-31 10:36:41 +0100402 state = rememberScalingLazyListState().also { state = it },
jnicholda0477e2021-08-10 17:15:45 +0100403 modifier = Modifier.requiredSize(
404 itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
405 ),
jnicholcdbe7e22021-12-01 19:04:23 +0000406 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
407 contentPadding = PaddingValues(vertical = 100.dp),
jnicholda0477e2021-08-10 17:15:45 +0100408 ) {
409 items(5) {
jnicholcdbe7e22021-12-01 19:04:23 +0000410 Box(
411 Modifier
412 .requiredSize(itemSizeDp)
413 .testTag("Item:$it"))
jnicholda0477e2021-08-10 17:15:45 +0100414 }
415 }
416 }
417
jnicholfd4289f2022-01-25 12:36:59 +0000418 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
419 rule.waitUntil { state.initialized.value }
jnichol79900f52021-09-01 09:57:34 +0100420 rule.waitForIdle()
421
422 rule.onNodeWithTag(testTag = "Item:0").assertIsDisplayed()
jnichol79900f52021-09-01 09:57:34 +0100423
jnicholcdbe7e22021-12-01 19:04:23 +0000424 val scrollAmount = (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()).roundToInt()
jnichol79900f52021-09-01 09:57:34 +0100425 rule.runOnIdle {
jnicholcdbe7e22021-12-01 19:04:23 +0000426 assertThat(state.centerItemIndex).isEqualTo(0)
427 assertThat(state.centerItemScrollOffset).isEqualTo(0)
428
jnichol79900f52021-09-01 09:57:34 +0100429 runBlocking {
jnicholcdbe7e22021-12-01 19:04:23 +0000430 state.scrollBy(scrollAmount.toFloat())
jnichol79900f52021-09-01 09:57:34 +0100431 }
jnicholcdbe7e22021-12-01 19:04:23 +0000432 state.layoutInfo.assertVisibleItems(count = 4)
433 assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(-scrollAmount)
jnichol79900f52021-09-01 09:57:34 +0100434 }
435
436 rule.runOnIdle {
437 runBlocking {
jnicholcdbe7e22021-12-01 19:04:23 +0000438 state.scrollBy(-scrollAmount.toFloat())
jnichol79900f52021-09-01 09:57:34 +0100439 }
jnicholcdbe7e22021-12-01 19:04:23 +0000440 state.layoutInfo.assertVisibleItems(count = 3)
jnichol79900f52021-09-01 09:57:34 +0100441 assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
442 }
443 }
444
445 @Test
446 fun visibleItemsAreCorrectAfterScrollNoScalingForReverseLayout() {
447 lateinit var state: ScalingLazyListState
448 rule.setContent {
449 ScalingLazyColumn(
jnicholcdbe7e22021-12-01 19:04:23 +0000450 state = rememberScalingLazyListState(8).also { state = it },
jnichol79900f52021-09-01 09:57:34 +0100451 modifier = Modifier.requiredSize(
jnicholcdbe7e22021-12-01 19:04:23 +0000452 itemSizeDp * 4f + defaultItemSpacingDp * 3f
jnichol79900f52021-09-01 09:57:34 +0100453 ),
454 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
455 reverseLayout = true
456 ) {
jnicholcdbe7e22021-12-01 19:04:23 +0000457 items(15) {
458 Box(Modifier.requiredSize(itemSizeDp).testTag("Item:$it"))
jnichol79900f52021-09-01 09:57:34 +0100459 }
460 }
461 }
462
jnicholfd4289f2022-01-25 12:36:59 +0000463 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
464 rule.waitUntil { state.initialized.value }
jnichol79900f52021-09-01 09:57:34 +0100465 rule.waitForIdle()
466
jnicholcdbe7e22021-12-01 19:04:23 +0000467 rule.onNodeWithTag(testTag = "Item:8").assertIsDisplayed()
jnichol79900f52021-09-01 09:57:34 +0100468
jnicholcdbe7e22021-12-01 19:04:23 +0000469 val scrollAmount = (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()).roundToInt()
jnicholda0477e2021-08-10 17:15:45 +0100470 rule.runOnIdle {
jnicholcdbe7e22021-12-01 19:04:23 +0000471 state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
472 assertThat(state.centerItemIndex).isEqualTo(8)
473 assertThat(state.centerItemScrollOffset).isEqualTo(0)
474
jnicholda0477e2021-08-10 17:15:45 +0100475 runBlocking {
jnicholcdbe7e22021-12-01 19:04:23 +0000476 state.scrollBy(scrollAmount.toFloat())
jnicholda0477e2021-08-10 17:15:45 +0100477 }
jnicholcdbe7e22021-12-01 19:04:23 +0000478 state.layoutInfo.assertVisibleItems(count = 5, startIndex = 7)
jnicholda0477e2021-08-10 17:15:45 +0100479 }
480
481 rule.runOnIdle {
482 runBlocking {
jnicholcdbe7e22021-12-01 19:04:23 +0000483 state.scrollBy(-scrollAmount.toFloat())
jnicholda0477e2021-08-10 17:15:45 +0100484 }
jnicholcdbe7e22021-12-01 19:04:23 +0000485 state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6)
jnicholda0477e2021-08-10 17:15:45 +0100486 }
487 }
488
489 @Test
490 fun visibleItemsAreCorrectAfterDispatchRawDeltaScrollNoScaling() {
jnichold40ad762021-08-31 10:36:41 +0100491 lateinit var state: ScalingLazyListState
jnicholda0477e2021-08-10 17:15:45 +0100492 rule.setContent {
493 ScalingLazyColumn(
jnichold40ad762021-08-31 10:36:41 +0100494 state = rememberScalingLazyListState().also { state = it },
jnicholda0477e2021-08-10 17:15:45 +0100495 modifier = Modifier.requiredSize(
496 itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
497 ),
jnicholcdbe7e22021-12-01 19:04:23 +0000498 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
499 contentPadding = PaddingValues(vertical = 100.dp)
jnicholda0477e2021-08-10 17:15:45 +0100500 ) {
501 items(5) {
502 Box(Modifier.requiredSize(itemSizeDp))
503 }
504 }
505 }
506
jnicholfd4289f2022-01-25 12:36:59 +0000507 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
508 rule.waitUntil { state.initialized.value }
jnicholcdbe7e22021-12-01 19:04:23 +0000509 val scrollAmount = itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()
jnicholda0477e2021-08-10 17:15:45 +0100510 rule.runOnIdle {
511 runBlocking {
jnicholcdbe7e22021-12-01 19:04:23 +0000512 state.dispatchRawDelta(scrollAmount)
jnicholda0477e2021-08-10 17:15:45 +0100513 }
jnicholcdbe7e22021-12-01 19:04:23 +0000514 state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
515 assertThat(state.layoutInfo.visibleItemsInfo.first().offset)
516 .isEqualTo(-scrollAmount.roundToInt())
jnicholda0477e2021-08-10 17:15:45 +0100517 }
518
519 rule.runOnIdle {
520 runBlocking {
jnicholcdbe7e22021-12-01 19:04:23 +0000521 state.dispatchRawDelta(-scrollAmount)
jnicholda0477e2021-08-10 17:15:45 +0100522 }
jnicholcdbe7e22021-12-01 19:04:23 +0000523 state.layoutInfo.assertVisibleItems(count = 3, startIndex = 0)
jnicholda0477e2021-08-10 17:15:45 +0100524 assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0)
525 }
526 }
527
528 @Test
jnichol79900f52021-09-01 09:57:34 +0100529 fun visibleItemsAreCorrectAfterDispatchRawDeltaScrollNoScalingForReverseLayout() {
530 lateinit var state: ScalingLazyListState
531 rule.setContent {
532 ScalingLazyColumn(
533 state = rememberScalingLazyListState().also { state = it },
534 modifier = Modifier.requiredSize(
535 itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
536 ),
537 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
jnichol50c68822022-01-17 15:08:52 +0000538 reverseLayout = true,
539 autoCentering = false
jnichol79900f52021-09-01 09:57:34 +0100540 ) {
541 items(5) {
542 Box(Modifier.requiredSize(itemSizeDp))
543 }
544 }
545 }
jnicholfd4289f2022-01-25 12:36:59 +0000546 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
547 rule.waitUntil { state.initialized.value }
jnicholcdbe7e22021-12-01 19:04:23 +0000548 val firstItemOffset = state.layoutInfo.visibleItemsInfo.first().offset
jnichol79900f52021-09-01 09:57:34 +0100549 rule.runOnIdle {
550 runBlocking {
551 state.dispatchRawDelta(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
552 }
553 state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1)
jnicholcdbe7e22021-12-01 19:04:23 +0000554 assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(firstItemOffset)
jnichol79900f52021-09-01 09:57:34 +0100555 }
556
557 rule.runOnIdle {
558 runBlocking {
559 state.dispatchRawDelta(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()))
560 }
561 state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0)
jnicholcdbe7e22021-12-01 19:04:23 +0000562 assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(firstItemOffset)
jnichol79900f52021-09-01 09:57:34 +0100563 }
564 }
565
566 @Test
jnichol5e1f7902021-08-04 18:38:26 +0100567 fun visibleItemsAreCorrectWithCustomSpacing() {
jnichold40ad762021-08-31 10:36:41 +0100568 lateinit var state: ScalingLazyListState
jnichol5e1f7902021-08-04 18:38:26 +0100569 val spacing: Dp = 10.dp
570 rule.setContent {
571 ScalingLazyColumn(
jnichold40ad762021-08-31 10:36:41 +0100572 state = rememberScalingLazyListState().also { state = it },
jnichol5e1f7902021-08-04 18:38:26 +0100573 modifier = Modifier.requiredSize(itemSizeDp * 3.5f + spacing * 2.5f),
jnichol50c68822022-01-17 15:08:52 +0000574 verticalArrangement = Arrangement.spacedBy(spacing),
575 autoCentering = false
jnichol5e1f7902021-08-04 18:38:26 +0100576 ) {
577 items(5) {
578 Box(Modifier.requiredSize(itemSizeDp))
579 }
580 }
581 }
582
jnicholfd4289f2022-01-25 12:36:59 +0000583 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
584 rule.waitUntil { state.initialized.value }
jnichol5e1f7902021-08-04 18:38:26 +0100585 rule.runOnIdle {
586 val spacingPx = with(rule.density) {
587 spacing.roundToPx()
588 }
589 state.layoutInfo.assertVisibleItems(
590 count = 4,
591 spacing = spacingPx
592 )
593 }
594 }
595
jnicholda0477e2021-08-10 17:15:45 +0100596 @Composable
597 fun ObservingFun(
jnichold40ad762021-08-31 10:36:41 +0100598 state: ScalingLazyListState,
599 currentInfo: StableRef<ScalingLazyListLayoutInfo?>
jnicholda0477e2021-08-10 17:15:45 +0100600 ) {
601 currentInfo.value = state.layoutInfo
602 }
603
604 @Test
605 fun visibleItemsAreObservableWhenWeScroll() {
jnichold40ad762021-08-31 10:36:41 +0100606 lateinit var state: ScalingLazyListState
607 val currentInfo = StableRef<ScalingLazyListLayoutInfo?>(null)
jnicholda0477e2021-08-10 17:15:45 +0100608 rule.setContent {
609 ScalingLazyColumn(
jnichold40ad762021-08-31 10:36:41 +0100610 state = rememberScalingLazyListState().also { state = it },
jnichol50c68822022-01-17 15:08:52 +0000611 modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
612 autoCentering = false
jnicholda0477e2021-08-10 17:15:45 +0100613 ) {
614 items(6) {
615 Box(Modifier.requiredSize(itemSizeDp))
616 }
617 }
618 ObservingFun(state, currentInfo)
619 }
620
jnicholfd4289f2022-01-25 12:36:59 +0000621 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
622 rule.waitUntil { state.initialized.value }
jnicholda0477e2021-08-10 17:15:45 +0100623 rule.runOnIdle {
624 // empty it here and scrolling should invoke observingFun again
625 currentInfo.value = null
626 runBlocking {
627 state.scrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
628 }
629 }
630
631 rule.runOnIdle {
632 assertThat(currentInfo.value).isNotNull()
633 currentInfo.value!!.assertVisibleItems(count = 4, startIndex = 1)
634 }
635 }
636
637 @Test
638 fun visibleItemsAreObservableWhenWeDispatchRawDeltaScroll() {
jnichold40ad762021-08-31 10:36:41 +0100639 lateinit var state: ScalingLazyListState
640 val currentInfo = StableRef<ScalingLazyListLayoutInfo?>(null)
jnicholda0477e2021-08-10 17:15:45 +0100641 rule.setContent {
642 ScalingLazyColumn(
jnichold40ad762021-08-31 10:36:41 +0100643 state = rememberScalingLazyListState().also { state = it },
jnichol50c68822022-01-17 15:08:52 +0000644 modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
645 autoCentering = false
jnicholda0477e2021-08-10 17:15:45 +0100646 ) {
647 items(6) {
648 Box(Modifier.requiredSize(itemSizeDp))
649 }
650 }
651 ObservingFun(state, currentInfo)
652 }
653
jnicholfd4289f2022-01-25 12:36:59 +0000654 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
655 rule.waitUntil { state.initialized.value }
jnicholda0477e2021-08-10 17:15:45 +0100656 rule.runOnIdle {
657 // empty it here and scrolling should invoke observingFun again
658 currentInfo.value = null
659 runBlocking {
660 state.dispatchRawDelta(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
661 }
662 }
663
664 rule.runOnIdle {
665 assertThat(currentInfo.value).isNotNull()
666 currentInfo.value!!.assertVisibleItems(count = 4, startIndex = 1)
667 }
668 }
669
670 @Composable
671 fun ObservingIsScrollInProgressTrueFun(
jnichold40ad762021-08-31 10:36:41 +0100672 state: ScalingLazyListState,
jnicholda0477e2021-08-10 17:15:45 +0100673 currentInfo: StableRef<Boolean?>
674 ) {
675 // If isScrollInProgress is ever true record it - otherwise leave the value as null
676 if (state.isScrollInProgress) {
677 currentInfo.value = true
678 }
679 }
680
681 @Test
682 fun isScrollInProgressIsObservableWhenWeScroll() {
jnichold40ad762021-08-31 10:36:41 +0100683 lateinit var state: ScalingLazyListState
jnicholda0477e2021-08-10 17:15:45 +0100684 var scope: CoroutineScope? = null
685 val currentInfo = StableRef<Boolean?>(null)
686 rule.setContent {
687 scope = rememberCoroutineScope()
688 ScalingLazyColumn(
jnichold40ad762021-08-31 10:36:41 +0100689 state = rememberScalingLazyListState().also { state = it },
jnicholda0477e2021-08-10 17:15:45 +0100690 modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f)
691 ) {
692 items(6) {
693 Box(Modifier.requiredSize(itemSizeDp))
694 }
695 }
696 ObservingIsScrollInProgressTrueFun(state, currentInfo)
697 }
698
jnicholfd4289f2022-01-25 12:36:59 +0000699 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
700 rule.waitUntil { state.initialized.value }
jnicholda0477e2021-08-10 17:15:45 +0100701 scope!!.launch {
702 // empty it here and scrolling should invoke observingFun again
703 currentInfo.value = null
704 state.animateScrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
705 }
706
707 rule.runOnIdle {
708 assertThat(currentInfo.value).isNotNull()
709 assertThat(currentInfo.value).isTrue()
710 }
711 }
712
jnicholcdbe7e22021-12-01 19:04:23 +0000713 @Composable
714 fun ObservingCentralItemIndexFun(
715 state: ScalingLazyListState,
716 currentInfo: StableRef<Int?>
717 ) {
718 currentInfo.value = state.centerItemIndex
719 }
720
721 @Test
722 fun isCentralListItemIndexObservableWhenWeScroll() {
723 lateinit var state: ScalingLazyListState
724 var scope: CoroutineScope? = null
725 val currentInfo = StableRef<Int?>(null)
726 rule.setContent {
727 scope = rememberCoroutineScope()
728 ScalingLazyColumn(
729 state = rememberScalingLazyListState().also { state = it },
jnichol50c68822022-01-17 15:08:52 +0000730 modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
731 autoCentering = false
jnicholcdbe7e22021-12-01 19:04:23 +0000732 ) {
733 items(6) {
734 Box(Modifier.requiredSize(itemSizeDp))
735 }
736 }
737 ObservingCentralItemIndexFun(state, currentInfo)
738 }
739
jnicholfd4289f2022-01-25 12:36:59 +0000740 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
741 rule.waitUntil { state.initialized.value }
jnicholcdbe7e22021-12-01 19:04:23 +0000742 scope!!.launch {
743 // empty it here and scrolling should invoke observingFun again
744 currentInfo.value = null
745 state.animateScrollBy(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())
746 }
747
748 rule.runOnIdle {
749 assertThat(currentInfo.value).isNotNull()
750 assertThat(currentInfo.value).isEqualTo(2)
751 }
752 }
753
jnichol5e1f7902021-08-04 18:38:26 +0100754 @Test
755 fun visibleItemsAreObservableWhenResize() {
jnichold40ad762021-08-31 10:36:41 +0100756 lateinit var state: ScalingLazyListState
jnichol5e1f7902021-08-04 18:38:26 +0100757 var size by mutableStateOf(itemSizeDp * 2)
jnichold40ad762021-08-31 10:36:41 +0100758 var currentInfo: ScalingLazyListLayoutInfo? = null
jnichol5e1f7902021-08-04 18:38:26 +0100759 @Composable
760 fun observingFun() {
761 currentInfo = state.layoutInfo
762 }
763 rule.setContent {
764 ScalingLazyColumn(
jnichold40ad762021-08-31 10:36:41 +0100765 state = rememberScalingLazyListState().also { state = it }
jnichol5e1f7902021-08-04 18:38:26 +0100766 ) {
767 item {
768 Box(Modifier.requiredSize(size))
769 }
770 }
771 observingFun()
772 }
773
jnicholfd4289f2022-01-25 12:36:59 +0000774 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
775 rule.waitUntil { state.initialized.value }
jnichol5e1f7902021-08-04 18:38:26 +0100776 rule.runOnIdle {
777 assertThat(currentInfo).isNotNull()
778 currentInfo!!.assertVisibleItems(count = 1, unscaledSize = itemSizePx * 2)
779 currentInfo = null
780 size = itemSizeDp
781 }
782
783 rule.runOnIdle {
784 assertThat(currentInfo).isNotNull()
785 currentInfo!!.assertVisibleItems(count = 1, unscaledSize = itemSizePx)
786 }
787 }
788
789 @Test
790 fun viewportOffsetsAreCorrect() {
791 val sizePx = 45
792 val sizeDp = with(rule.density) { sizePx.toDp() }
jnichold40ad762021-08-31 10:36:41 +0100793 lateinit var state: ScalingLazyListState
jnichol5e1f7902021-08-04 18:38:26 +0100794 rule.setContent {
795 ScalingLazyColumn(
796 Modifier.requiredSize(sizeDp),
jnichold40ad762021-08-31 10:36:41 +0100797 state = rememberScalingLazyListState().also { state = it }
jnichol5e1f7902021-08-04 18:38:26 +0100798 ) {
799 items(4) {
800 Box(Modifier.requiredSize(sizeDp))
801 }
802 }
803 }
804
jnicholfd4289f2022-01-25 12:36:59 +0000805 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
806 rule.waitUntil { state.initialized.value }
jnichol5e1f7902021-08-04 18:38:26 +0100807 rule.runOnIdle {
808 assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
809 assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
810 }
811 }
812
813 @Test
814 fun viewportOffsetsAreCorrectWithContentPadding() {
815 val sizePx = 45
816 val startPaddingPx = 10
817 val endPaddingPx = 15
818 val sizeDp = with(rule.density) { sizePx.toDp() }
819 val topPaddingDp = with(rule.density) { startPaddingPx.toDp() }
820 val bottomPaddingDp = with(rule.density) { endPaddingPx.toDp() }
jnichold40ad762021-08-31 10:36:41 +0100821 lateinit var state: ScalingLazyListState
jnichol5e1f7902021-08-04 18:38:26 +0100822 rule.setContent {
823 ScalingLazyColumn(
824 Modifier.requiredSize(sizeDp),
825 contentPadding = PaddingValues(top = topPaddingDp, bottom = bottomPaddingDp),
jnichold40ad762021-08-31 10:36:41 +0100826 state = rememberScalingLazyListState().also { state = it }
jnichol5e1f7902021-08-04 18:38:26 +0100827 ) {
828 items(4) {
829 Box(Modifier.requiredSize(sizeDp))
830 }
831 }
832 }
833
jnicholfd4289f2022-01-25 12:36:59 +0000834 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
835 rule.waitUntil { state.initialized.value }
jnichol5e1f7902021-08-04 18:38:26 +0100836 rule.runOnIdle {
837 assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
838 assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
839 }
840 }
841
842 @Test
jnichol50c68822022-01-17 15:08:52 +0000843 fun viewportOffsetsAreCorrectWithAutoCentering() {
844 val sizePx = 45
845 val sizeDp = with(rule.density) { sizePx.toDp() }
846 lateinit var state: ScalingLazyListState
847 rule.setContent {
848 ScalingLazyColumn(
849 Modifier.requiredSize(sizeDp),
850 state = rememberScalingLazyListState().also { state = it },
851 autoCentering = true
852 ) {
853 items(4) {
854 Box(Modifier.requiredSize(sizeDp))
855 }
856 }
857 }
858
jnicholfd4289f2022-01-25 12:36:59 +0000859 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
860 rule.waitUntil { state.initialized.value }
jnichol50c68822022-01-17 15:08:52 +0000861 rule.runOnIdle {
862 assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
863 assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
864 }
865 }
866
867 @Test
jnichol5e1f7902021-08-04 18:38:26 +0100868 fun totalCountIsCorrect() {
869 var count by mutableStateOf(10)
jnichold40ad762021-08-31 10:36:41 +0100870 lateinit var state: ScalingLazyListState
jnichol5e1f7902021-08-04 18:38:26 +0100871 rule.setContent {
872 ScalingLazyColumn(
jnichold40ad762021-08-31 10:36:41 +0100873 state = rememberScalingLazyListState().also { state = it }
jnichol5e1f7902021-08-04 18:38:26 +0100874 ) {
875 items(count) {
876 Box(Modifier.requiredSize(10.dp))
877 }
878 }
879 }
880
jnicholfd4289f2022-01-25 12:36:59 +0000881 // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization
882 rule.waitUntil { state.initialized.value }
jnichol5e1f7902021-08-04 18:38:26 +0100883 rule.runOnIdle {
884 assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
885 count = 20
886 }
887
888 rule.runOnIdle {
889 assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20)
890 }
891 }
892
jnicholcdbe7e22021-12-01 19:04:23 +0000893 private fun ScalingLazyListLayoutInfo.assertVisibleItems(
jnichol5e1f7902021-08-04 18:38:26 +0100894 count: Int,
895 startIndex: Int = 0,
896 unscaledSize: Int = itemSizePx,
jnichol2ffdcb12022-01-12 10:15:47 +0000897 spacing: Int = defaultItemSpacingPx,
898 anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter
jnichol5e1f7902021-08-04 18:38:26 +0100899 ) {
900 assertThat(visibleItemsInfo.size).isEqualTo(count)
901 var currentIndex = startIndex
902 var previousEndOffset = -1
903 visibleItemsInfo.forEach {
904 assertThat(it.index).isEqualTo(currentIndex)
905 assertThat(it.size).isEqualTo((unscaledSize * it.scale).roundToInt())
906 currentIndex++
jnichol2ffdcb12022-01-12 10:15:47 +0000907 val startOffset = it.startOffset(anchorType).roundToInt()
jnichol5e1f7902021-08-04 18:38:26 +0100908 if (previousEndOffset != -1) {
jnichol2ffdcb12022-01-12 10:15:47 +0000909 assertThat(spacing).isEqualTo(startOffset - previousEndOffset)
jnichol5e1f7902021-08-04 18:38:26 +0100910 }
jnichol2ffdcb12022-01-12 10:15:47 +0000911 previousEndOffset = startOffset + it.size
jnichol5e1f7902021-08-04 18:38:26 +0100912 }
913 }
jnicholda0477e2021-08-10 17:15:45 +0100914}
915
916@Stable
jnichol05b82c6c2021-09-03 17:40:34 +0100917public class StableRef<T>(var value: T)