jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | package androidx.wear.compose.material |
| 18 | |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 19 | import androidx.compose.foundation.gestures.animateScrollBy |
| 20 | import androidx.compose.foundation.gestures.scrollBy |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 21 | import androidx.compose.foundation.layout.Arrangement |
| 22 | import androidx.compose.foundation.layout.Box |
| 23 | import androidx.compose.foundation.layout.PaddingValues |
| 24 | import androidx.compose.foundation.layout.requiredSize |
| 25 | import androidx.compose.runtime.Composable |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 26 | import androidx.compose.runtime.Stable |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 27 | import androidx.compose.runtime.getValue |
| 28 | import androidx.compose.runtime.mutableStateOf |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 29 | import androidx.compose.runtime.rememberCoroutineScope |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 30 | import androidx.compose.runtime.setValue |
| 31 | import androidx.compose.ui.Modifier |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 32 | import androidx.compose.ui.platform.testTag |
| 33 | import androidx.compose.ui.test.assertIsDisplayed |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 34 | import androidx.compose.ui.test.junit4.createComposeRule |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 35 | import androidx.compose.ui.test.onNodeWithTag |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 36 | import androidx.compose.ui.unit.Dp |
| 37 | import androidx.compose.ui.unit.dp |
| 38 | import androidx.test.ext.junit.runners.AndroidJUnit4 |
| 39 | import androidx.test.filters.MediumTest |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 40 | import com.google.common.truth.Truth.assertThat |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 41 | import kotlinx.coroutines.CoroutineScope |
| 42 | import kotlinx.coroutines.launch |
| 43 | import kotlinx.coroutines.runBlocking |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 44 | import org.junit.Before |
| 45 | import org.junit.Rule |
| 46 | import org.junit.Test |
| 47 | import org.junit.runner.RunWith |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 48 | import kotlin.math.roundToInt |
| 49 | |
| 50 | @MediumTest |
| 51 | @RunWith(AndroidJUnit4::class) |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 52 | public class ScalingLazyListLayoutInfoTest { |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 53 | @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() { |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 71 | lateinit var state: ScalingLazyListState |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 72 | rule.setContent { |
| 73 | ScalingLazyColumn( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 74 | state = rememberScalingLazyListState().also { state = it }, |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 75 | modifier = Modifier.requiredSize( |
| 76 | itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f |
| 77 | ), |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 78 | autoCentering = false |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 79 | ) { |
| 80 | items(5) { |
| 81 | Box(Modifier.requiredSize(itemSizeDp)) |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 86 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 87 | rule.waitUntil { state.initialized.value } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 88 | rule.runOnIdle { |
| 89 | state.layoutInfo.assertVisibleItems(count = 4) |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | @Test |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 94 | 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 | ), |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 102 | reverseLayout = true, |
| 103 | autoCentering = false |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 104 | ) { |
| 105 | items(5) { |
| 106 | Box(Modifier.requiredSize(itemSizeDp)) |
| 107 | } |
| 108 | } |
| 109 | } |
| 110 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 111 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 112 | rule.waitUntil { state.initialized.value } |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 113 | rule.runOnIdle { |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 114 | assertThat(state.centerItemIndex).isEqualTo(1) |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 115 | state.layoutInfo.assertVisibleItems(count = 4) |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | @Test |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 120 | 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 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 137 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 138 | rule.waitUntil { state.initialized.value } |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 139 | 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 |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 147 | fun visibleItemsAreCorrectAfterScrolling() { |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 148 | lateinit var state: ScalingLazyListState |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 149 | rule.setContent { |
| 150 | ScalingLazyColumn( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 151 | state = rememberScalingLazyListState().also { state = it }, |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 152 | modifier = Modifier.requiredSize( |
| 153 | itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f |
| 154 | ), |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 155 | autoCentering = false |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 156 | ) { |
| 157 | items(5) { |
| 158 | Box(Modifier.requiredSize(itemSizeDp)) |
| 159 | } |
| 160 | } |
| 161 | } |
| 162 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 163 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 164 | rule.waitUntil { state.initialized.value } |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 165 | 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 |
jnichol | efc7284 | 2021-10-05 18:51:17 +0100 | [diff] [blame] | 174 | 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 | ), |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 182 | autoCentering = false |
jnichol | efc7284 | 2021-10-05 18:51:17 +0100 | [diff] [blame] | 183 | ) { |
| 184 | items(5) { |
| 185 | Box(Modifier.requiredSize(itemSizeDp * 5)) |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 190 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 191 | rule.waitUntil { state.initialized.value } |
jnichol | efc7284 | 2021-10-05 18:51:17 +0100 | [diff] [blame] | 192 | 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 |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 206 | val centerItemIndex = 2 |
jnichol | efc7284 | 2021-10-05 18:51:17 +0100 | [diff] [blame] | 207 | rule.setContent { |
| 208 | ScalingLazyColumn( |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 209 | state = rememberScalingLazyListState(centerItemIndex).also { state = it }, |
jnichol | efc7284 | 2021-10-05 18:51:17 +0100 | [diff] [blame] | 210 | modifier = Modifier.requiredSize( |
| 211 | itemSizeDp * 3 |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 212 | ), |
jnichol | efc7284 | 2021-10-05 18:51:17 +0100 | [diff] [blame] | 213 | ) { |
| 214 | items(5) { |
| 215 | Box(Modifier.requiredSize(itemSizeDp)) |
| 216 | } |
| 217 | } |
| 218 | } |
| 219 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 220 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 221 | rule.waitUntil { state.initialized.value } |
jnichol | efc7284 | 2021-10-05 18:51:17 +0100 | [diff] [blame] | 222 | rule.runOnIdle { |
| 223 | // Get the middle item on the screen |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 224 | val centerScreenItem = |
| 225 | state.layoutInfo.visibleItemsInfo.find { it.index == centerItemIndex } |
| 226 | // and confirm its offset is 0 |
| 227 | assertThat(centerScreenItem!!.offset).isEqualTo(0) |
jnichol | efc7284 | 2021-10-05 18:51:17 +0100 | [diff] [blame] | 228 | // And that it is not scaled |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 229 | assertThat(centerScreenItem.scale).isEqualTo(1.0f) |
jnichol | efc7284 | 2021-10-05 18:51:17 +0100 | [diff] [blame] | 230 | } |
| 231 | } |
| 232 | |
| 233 | @Test |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 234 | 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, |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 243 | autoCentering = false |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 244 | ) { |
| 245 | items(5) { |
| 246 | Box(Modifier.requiredSize(itemSizeDp)) |
| 247 | } |
| 248 | } |
| 249 | } |
| 250 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 251 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 252 | rule.waitUntil { state.initialized.value } |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 253 | 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 |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 262 | fun visibleItemsAreCorrectCenterPivotNoOffset() { |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 263 | lateinit var state: ScalingLazyListState |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 264 | rule.setContent { |
| 265 | ScalingLazyColumn( |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 266 | state = rememberScalingLazyListState(2).also { state = it }, |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 267 | modifier = Modifier.requiredSize( |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 268 | itemSizeDp * 2f + defaultItemSpacingDp * 1f |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 269 | ), |
| 270 | scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f) |
| 271 | ) { |
| 272 | items(5) { |
| 273 | Box(Modifier.requiredSize(itemSizeDp)) |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 278 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 279 | rule.waitUntil { state.initialized.value } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 280 | rule.runOnIdle { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 281 | 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 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 304 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 305 | rule.waitUntil { state.initialized.value } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 306 | 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 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 331 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 332 | rule.waitUntil { state.initialized.value } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 333 | 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 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 358 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 359 | rule.waitUntil { state.initialized.value } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 360 | rule.runOnIdle { |
| 361 | state.layoutInfo.assertVisibleItems(count = 3, startIndex = 1) |
| 362 | assertThat(state.centerItemIndex).isEqualTo(2) |
| 363 | assertThat(state.centerItemScrollOffset).isEqualTo(-5) |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 364 | } |
| 365 | } |
| 366 | |
| 367 | @Test |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 368 | fun visibleItemsAreCorrectNoScalingForReverseLayout() { |
| 369 | lateinit var state: ScalingLazyListState |
| 370 | rule.setContent { |
| 371 | ScalingLazyColumn( |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 372 | state = rememberScalingLazyListState(8).also { state = it }, |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 373 | modifier = Modifier.requiredSize( |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 374 | itemSizeDp * 4f + defaultItemSpacingDp * 3f |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 375 | ), |
| 376 | scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f), |
| 377 | reverseLayout = true |
| 378 | ) { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 379 | items(15) { |
| 380 | Box(Modifier.requiredSize(itemSizeDp).testTag("Item:$it")) |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 381 | } |
| 382 | } |
| 383 | } |
| 384 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 385 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 386 | rule.waitUntil { state.initialized.value } |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 387 | rule.waitForIdle() |
| 388 | |
| 389 | // Assert that items are being shown at the end of the parent as this is reverseLayout |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 390 | rule.onNodeWithTag(testTag = "Item:8").assertIsDisplayed() |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 391 | |
| 392 | rule.runOnIdle { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 393 | state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6) |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 394 | } |
| 395 | } |
| 396 | |
| 397 | @Test |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 398 | fun visibleItemsAreCorrectAfterScrollNoScaling() { |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 399 | lateinit var state: ScalingLazyListState |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 400 | rule.setContent { |
| 401 | ScalingLazyColumn( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 402 | state = rememberScalingLazyListState().also { state = it }, |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 403 | modifier = Modifier.requiredSize( |
| 404 | itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f |
| 405 | ), |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 406 | scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f), |
| 407 | contentPadding = PaddingValues(vertical = 100.dp), |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 408 | ) { |
| 409 | items(5) { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 410 | Box( |
| 411 | Modifier |
| 412 | .requiredSize(itemSizeDp) |
| 413 | .testTag("Item:$it")) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 414 | } |
| 415 | } |
| 416 | } |
| 417 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 418 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 419 | rule.waitUntil { state.initialized.value } |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 420 | rule.waitForIdle() |
| 421 | |
| 422 | rule.onNodeWithTag(testTag = "Item:0").assertIsDisplayed() |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 423 | |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 424 | val scrollAmount = (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()).roundToInt() |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 425 | rule.runOnIdle { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 426 | assertThat(state.centerItemIndex).isEqualTo(0) |
| 427 | assertThat(state.centerItemScrollOffset).isEqualTo(0) |
| 428 | |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 429 | runBlocking { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 430 | state.scrollBy(scrollAmount.toFloat()) |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 431 | } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 432 | state.layoutInfo.assertVisibleItems(count = 4) |
| 433 | assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(-scrollAmount) |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 434 | } |
| 435 | |
| 436 | rule.runOnIdle { |
| 437 | runBlocking { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 438 | state.scrollBy(-scrollAmount.toFloat()) |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 439 | } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 440 | state.layoutInfo.assertVisibleItems(count = 3) |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 441 | 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( |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 450 | state = rememberScalingLazyListState(8).also { state = it }, |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 451 | modifier = Modifier.requiredSize( |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 452 | itemSizeDp * 4f + defaultItemSpacingDp * 3f |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 453 | ), |
| 454 | scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f), |
| 455 | reverseLayout = true |
| 456 | ) { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 457 | items(15) { |
| 458 | Box(Modifier.requiredSize(itemSizeDp).testTag("Item:$it")) |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 459 | } |
| 460 | } |
| 461 | } |
| 462 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 463 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 464 | rule.waitUntil { state.initialized.value } |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 465 | rule.waitForIdle() |
| 466 | |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 467 | rule.onNodeWithTag(testTag = "Item:8").assertIsDisplayed() |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 468 | |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 469 | val scrollAmount = (itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()).roundToInt() |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 470 | rule.runOnIdle { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 471 | state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6) |
| 472 | assertThat(state.centerItemIndex).isEqualTo(8) |
| 473 | assertThat(state.centerItemScrollOffset).isEqualTo(0) |
| 474 | |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 475 | runBlocking { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 476 | state.scrollBy(scrollAmount.toFloat()) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 477 | } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 478 | state.layoutInfo.assertVisibleItems(count = 5, startIndex = 7) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 479 | } |
| 480 | |
| 481 | rule.runOnIdle { |
| 482 | runBlocking { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 483 | state.scrollBy(-scrollAmount.toFloat()) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 484 | } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 485 | state.layoutInfo.assertVisibleItems(count = 5, startIndex = 6) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 486 | } |
| 487 | } |
| 488 | |
| 489 | @Test |
| 490 | fun visibleItemsAreCorrectAfterDispatchRawDeltaScrollNoScaling() { |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 491 | lateinit var state: ScalingLazyListState |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 492 | rule.setContent { |
| 493 | ScalingLazyColumn( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 494 | state = rememberScalingLazyListState().also { state = it }, |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 495 | modifier = Modifier.requiredSize( |
| 496 | itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f |
| 497 | ), |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 498 | scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f), |
| 499 | contentPadding = PaddingValues(vertical = 100.dp) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 500 | ) { |
| 501 | items(5) { |
| 502 | Box(Modifier.requiredSize(itemSizeDp)) |
| 503 | } |
| 504 | } |
| 505 | } |
| 506 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 507 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 508 | rule.waitUntil { state.initialized.value } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 509 | val scrollAmount = itemSizePx.toFloat() + defaultItemSpacingPx.toFloat() |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 510 | rule.runOnIdle { |
| 511 | runBlocking { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 512 | state.dispatchRawDelta(scrollAmount) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 513 | } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 514 | state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0) |
| 515 | assertThat(state.layoutInfo.visibleItemsInfo.first().offset) |
| 516 | .isEqualTo(-scrollAmount.roundToInt()) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 517 | } |
| 518 | |
| 519 | rule.runOnIdle { |
| 520 | runBlocking { |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 521 | state.dispatchRawDelta(-scrollAmount) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 522 | } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 523 | state.layoutInfo.assertVisibleItems(count = 3, startIndex = 0) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 524 | assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(0) |
| 525 | } |
| 526 | } |
| 527 | |
| 528 | @Test |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 529 | 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), |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 538 | reverseLayout = true, |
| 539 | autoCentering = false |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 540 | ) { |
| 541 | items(5) { |
| 542 | Box(Modifier.requiredSize(itemSizeDp)) |
| 543 | } |
| 544 | } |
| 545 | } |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 546 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 547 | rule.waitUntil { state.initialized.value } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 548 | val firstItemOffset = state.layoutInfo.visibleItemsInfo.first().offset |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 549 | rule.runOnIdle { |
| 550 | runBlocking { |
| 551 | state.dispatchRawDelta(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat()) |
| 552 | } |
| 553 | state.layoutInfo.assertVisibleItems(count = 4, startIndex = 1) |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 554 | assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(firstItemOffset) |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 555 | } |
| 556 | |
| 557 | rule.runOnIdle { |
| 558 | runBlocking { |
| 559 | state.dispatchRawDelta(-(itemSizePx.toFloat() + defaultItemSpacingPx.toFloat())) |
| 560 | } |
| 561 | state.layoutInfo.assertVisibleItems(count = 4, startIndex = 0) |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 562 | assertThat(state.layoutInfo.visibleItemsInfo.first().offset).isEqualTo(firstItemOffset) |
jnichol | 79900f5 | 2021-09-01 09:57:34 +0100 | [diff] [blame] | 563 | } |
| 564 | } |
| 565 | |
| 566 | @Test |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 567 | fun visibleItemsAreCorrectWithCustomSpacing() { |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 568 | lateinit var state: ScalingLazyListState |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 569 | val spacing: Dp = 10.dp |
| 570 | rule.setContent { |
| 571 | ScalingLazyColumn( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 572 | state = rememberScalingLazyListState().also { state = it }, |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 573 | modifier = Modifier.requiredSize(itemSizeDp * 3.5f + spacing * 2.5f), |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 574 | verticalArrangement = Arrangement.spacedBy(spacing), |
| 575 | autoCentering = false |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 576 | ) { |
| 577 | items(5) { |
| 578 | Box(Modifier.requiredSize(itemSizeDp)) |
| 579 | } |
| 580 | } |
| 581 | } |
| 582 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 583 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 584 | rule.waitUntil { state.initialized.value } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 585 | 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 | |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 596 | @Composable |
| 597 | fun ObservingFun( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 598 | state: ScalingLazyListState, |
| 599 | currentInfo: StableRef<ScalingLazyListLayoutInfo?> |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 600 | ) { |
| 601 | currentInfo.value = state.layoutInfo |
| 602 | } |
| 603 | |
| 604 | @Test |
| 605 | fun visibleItemsAreObservableWhenWeScroll() { |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 606 | lateinit var state: ScalingLazyListState |
| 607 | val currentInfo = StableRef<ScalingLazyListLayoutInfo?>(null) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 608 | rule.setContent { |
| 609 | ScalingLazyColumn( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 610 | state = rememberScalingLazyListState().also { state = it }, |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 611 | modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f), |
| 612 | autoCentering = false |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 613 | ) { |
| 614 | items(6) { |
| 615 | Box(Modifier.requiredSize(itemSizeDp)) |
| 616 | } |
| 617 | } |
| 618 | ObservingFun(state, currentInfo) |
| 619 | } |
| 620 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 621 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 622 | rule.waitUntil { state.initialized.value } |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 623 | 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() { |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 639 | lateinit var state: ScalingLazyListState |
| 640 | val currentInfo = StableRef<ScalingLazyListLayoutInfo?>(null) |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 641 | rule.setContent { |
| 642 | ScalingLazyColumn( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 643 | state = rememberScalingLazyListState().also { state = it }, |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 644 | modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f), |
| 645 | autoCentering = false |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 646 | ) { |
| 647 | items(6) { |
| 648 | Box(Modifier.requiredSize(itemSizeDp)) |
| 649 | } |
| 650 | } |
| 651 | ObservingFun(state, currentInfo) |
| 652 | } |
| 653 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 654 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 655 | rule.waitUntil { state.initialized.value } |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 656 | 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( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 672 | state: ScalingLazyListState, |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 673 | 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() { |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 683 | lateinit var state: ScalingLazyListState |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 684 | var scope: CoroutineScope? = null |
| 685 | val currentInfo = StableRef<Boolean?>(null) |
| 686 | rule.setContent { |
| 687 | scope = rememberCoroutineScope() |
| 688 | ScalingLazyColumn( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 689 | state = rememberScalingLazyListState().also { state = it }, |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 690 | 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 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 699 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 700 | rule.waitUntil { state.initialized.value } |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 701 | 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 | |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 713 | @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 }, |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 730 | modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f), |
| 731 | autoCentering = false |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 732 | ) { |
| 733 | items(6) { |
| 734 | Box(Modifier.requiredSize(itemSizeDp)) |
| 735 | } |
| 736 | } |
| 737 | ObservingCentralItemIndexFun(state, currentInfo) |
| 738 | } |
| 739 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 740 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 741 | rule.waitUntil { state.initialized.value } |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 742 | 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 | |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 754 | @Test |
| 755 | fun visibleItemsAreObservableWhenResize() { |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 756 | lateinit var state: ScalingLazyListState |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 757 | var size by mutableStateOf(itemSizeDp * 2) |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 758 | var currentInfo: ScalingLazyListLayoutInfo? = null |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 759 | @Composable |
| 760 | fun observingFun() { |
| 761 | currentInfo = state.layoutInfo |
| 762 | } |
| 763 | rule.setContent { |
| 764 | ScalingLazyColumn( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 765 | state = rememberScalingLazyListState().also { state = it } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 766 | ) { |
| 767 | item { |
| 768 | Box(Modifier.requiredSize(size)) |
| 769 | } |
| 770 | } |
| 771 | observingFun() |
| 772 | } |
| 773 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 774 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 775 | rule.waitUntil { state.initialized.value } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 776 | 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() } |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 793 | lateinit var state: ScalingLazyListState |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 794 | rule.setContent { |
| 795 | ScalingLazyColumn( |
| 796 | Modifier.requiredSize(sizeDp), |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 797 | state = rememberScalingLazyListState().also { state = it } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 798 | ) { |
| 799 | items(4) { |
| 800 | Box(Modifier.requiredSize(sizeDp)) |
| 801 | } |
| 802 | } |
| 803 | } |
| 804 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 805 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 806 | rule.waitUntil { state.initialized.value } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 807 | 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() } |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 821 | lateinit var state: ScalingLazyListState |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 822 | rule.setContent { |
| 823 | ScalingLazyColumn( |
| 824 | Modifier.requiredSize(sizeDp), |
| 825 | contentPadding = PaddingValues(top = topPaddingDp, bottom = bottomPaddingDp), |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 826 | state = rememberScalingLazyListState().also { state = it } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 827 | ) { |
| 828 | items(4) { |
| 829 | Box(Modifier.requiredSize(sizeDp)) |
| 830 | } |
| 831 | } |
| 832 | } |
| 833 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 834 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 835 | rule.waitUntil { state.initialized.value } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 836 | rule.runOnIdle { |
| 837 | assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx) |
| 838 | assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx) |
| 839 | } |
| 840 | } |
| 841 | |
| 842 | @Test |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 843 | 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 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 859 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 860 | rule.waitUntil { state.initialized.value } |
jnichol | 50c6882 | 2022-01-17 15:08:52 +0000 | [diff] [blame] | 861 | rule.runOnIdle { |
| 862 | assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0) |
| 863 | assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx) |
| 864 | } |
| 865 | } |
| 866 | |
| 867 | @Test |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 868 | fun totalCountIsCorrect() { |
| 869 | var count by mutableStateOf(10) |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 870 | lateinit var state: ScalingLazyListState |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 871 | rule.setContent { |
| 872 | ScalingLazyColumn( |
jnichol | d40ad76 | 2021-08-31 10:36:41 +0100 | [diff] [blame] | 873 | state = rememberScalingLazyListState().also { state = it } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 874 | ) { |
| 875 | items(count) { |
| 876 | Box(Modifier.requiredSize(10.dp)) |
| 877 | } |
| 878 | } |
| 879 | } |
| 880 | |
jnichol | fd4289f | 2022-01-25 12:36:59 +0000 | [diff] [blame] | 881 | // TODO(b/210654937): Remove the waitUntil once we no longer need 2 stage initialization |
| 882 | rule.waitUntil { state.initialized.value } |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 883 | 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 | |
jnichol | cdbe7e2 | 2021-12-01 19:04:23 +0000 | [diff] [blame] | 893 | private fun ScalingLazyListLayoutInfo.assertVisibleItems( |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 894 | count: Int, |
| 895 | startIndex: Int = 0, |
| 896 | unscaledSize: Int = itemSizePx, |
jnichol | 2ffdcb1 | 2022-01-12 10:15:47 +0000 | [diff] [blame] | 897 | spacing: Int = defaultItemSpacingPx, |
| 898 | anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 899 | ) { |
| 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++ |
jnichol | 2ffdcb1 | 2022-01-12 10:15:47 +0000 | [diff] [blame] | 907 | val startOffset = it.startOffset(anchorType).roundToInt() |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 908 | if (previousEndOffset != -1) { |
jnichol | 2ffdcb1 | 2022-01-12 10:15:47 +0000 | [diff] [blame] | 909 | assertThat(spacing).isEqualTo(startOffset - previousEndOffset) |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 910 | } |
jnichol | 2ffdcb1 | 2022-01-12 10:15:47 +0000 | [diff] [blame] | 911 | previousEndOffset = startOffset + it.size |
jnichol | 5e1f790 | 2021-08-04 18:38:26 +0100 | [diff] [blame] | 912 | } |
| 913 | } |
jnichol | da0477e | 2021-08-10 17:15:45 +0100 | [diff] [blame] | 914 | } |
| 915 | |
| 916 | @Stable |
jnichol | 05b82c6c | 2021-09-03 17:40:34 +0100 | [diff] [blame] | 917 | public class StableRef<T>(var value: T) |