| /* |
| * Copyright 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.compose.foundation.lazy.staggeredgrid |
| |
| import androidx.compose.foundation.AutoTestFrameClock |
| import androidx.compose.foundation.ExperimentalFoundationApi |
| import androidx.compose.foundation.border |
| import androidx.compose.foundation.gestures.Orientation |
| import androidx.compose.foundation.gestures.scrollBy |
| import androidx.compose.foundation.layout.Box |
| import androidx.compose.foundation.layout.BoxWithConstraints |
| import androidx.compose.foundation.layout.Spacer |
| import androidx.compose.foundation.layout.size |
| import androidx.compose.foundation.lazy.list.assertIsPlaced |
| import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration |
| import androidx.compose.foundation.text.BasicText |
| import androidx.compose.runtime.DisposableEffect |
| import androidx.compose.runtime.LaunchedEffect |
| import androidx.compose.runtime.getValue |
| import androidx.compose.runtime.mutableStateListOf |
| import androidx.compose.runtime.mutableStateOf |
| import androidx.compose.runtime.setValue |
| import androidx.compose.ui.Modifier |
| import androidx.compose.ui.composed |
| import androidx.compose.ui.graphics.Color |
| import androidx.compose.ui.platform.testTag |
| import androidx.compose.ui.test.assertCountEquals |
| import androidx.compose.ui.test.assertIsDisplayed |
| import androidx.compose.ui.test.assertIsNotDisplayed |
| import androidx.compose.ui.test.assertPositionInRootIsEqualTo |
| import androidx.compose.ui.test.junit4.StateRestorationTester |
| import androidx.compose.ui.test.onChildren |
| import androidx.compose.ui.test.onNodeWithTag |
| import androidx.compose.ui.unit.Dp |
| import androidx.compose.ui.unit.DpOffset |
| import androidx.compose.ui.unit.DpSize |
| import androidx.compose.ui.unit.dp |
| import androidx.test.filters.MediumTest |
| import com.google.common.truth.Truth.assertThat |
| import kotlinx.coroutines.Dispatchers |
| import kotlinx.coroutines.runBlocking |
| import org.junit.After |
| import org.junit.Assert.assertTrue |
| import org.junit.Before |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.junit.runners.Parameterized |
| |
| @OptIn(ExperimentalFoundationApi::class) |
| @MediumTest |
| @RunWith(Parameterized::class) |
| class LazyStaggeredGridTest( |
| private val orientation: Orientation |
| ) : BaseLazyStaggeredGridWithOrientation(orientation) { |
| private val LazyStaggeredGridTag = "LazyStaggeredGridTag" |
| |
| internal lateinit var state: LazyStaggeredGridState |
| |
| companion object { |
| @JvmStatic |
| @Parameterized.Parameters(name = "{0}") |
| fun initParameters(): Array<Any> = arrayOf( |
| Orientation.Vertical, |
| Orientation.Horizontal, |
| ) |
| } |
| |
| private var itemSizeDp: Dp = Dp.Unspecified |
| private val itemSizePx: Int = 50 |
| |
| @Before |
| fun setUp() { |
| with(rule.density) { |
| itemSizeDp = itemSizePx.toDp() |
| } |
| } |
| |
| @After |
| fun tearDown() { |
| if (::state.isInitialized) { |
| var isSorted = true |
| var previousIndex = Int.MIN_VALUE |
| for (item in state.layoutInfo.visibleItemsInfo) { |
| if (previousIndex > item.index) { |
| isSorted = false |
| break |
| } |
| previousIndex = item.index |
| } |
| assertTrue( |
| "Visible items MUST BE sorted: ${state.layoutInfo.visibleItemsInfo}", |
| isSorted |
| ) |
| |
| assertThat(state.layoutInfo.orientation == orientation) |
| } |
| } |
| |
| @Test |
| fun showsZeroItems() { |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier.testTag(LazyStaggeredGridTag) |
| ) { } |
| } |
| |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .onChildren() |
| .assertCountEquals(0) |
| |
| assertThat(state.firstVisibleItemIndex).isEqualTo(0) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) |
| } |
| |
| @Test |
| fun showsOneItem() { |
| val itemTestTag = "itemTestTag" |
| |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| ) { |
| item { |
| Spacer( |
| Modifier |
| .size(itemSizeDp) |
| .testTag(itemTestTag) |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(itemTestTag) |
| .assertIsDisplayed() |
| |
| assertThat(state.firstVisibleItemIndex).isEqualTo(0) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) |
| } |
| |
| @Test |
| fun distributesSingleLine() { |
| rule.setContent { |
| LazyStaggeredGrid( |
| lanes = 3, |
| modifier = Modifier.crossAxisSize(itemSizeDp * 3), |
| ) { |
| items(3) { |
| Spacer( |
| Modifier |
| .size(itemSizeDp) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2) |
| } |
| |
| @Test |
| fun distributesTwoLines() { |
| rule.setContent { |
| LazyStaggeredGrid( |
| lanes = 3, |
| modifier = Modifier.crossAxisSize(itemSizeDp * 3), |
| ) { |
| items(6) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp * (it + 1) |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| // [item, 0, 0] |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| |
| // [item, item x 2, 0] |
| rule.onNodeWithTag("2") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2) |
| |
| // [item, item x 2, item x 3] |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| // [item x 4, item x 2, item x 3] |
| rule.onNodeWithTag("4") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp * 2) |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| |
| // [item x 4, item x 7, item x 3] |
| rule.onNodeWithTag("5") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp * 3) |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2) |
| |
| // [item x 4, item x 7, item x 9] |
| } |
| |
| @Test |
| fun moreItemsDisplayedOnScroll() { |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp), |
| ) { |
| items(6) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp * (it + 1) |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("3") |
| .assertDoesNotExist() |
| |
| state.scrollBy(itemSizeDp * 3) |
| |
| // [item, item x 2, item x 3] |
| rule.onNodeWithTag("3") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(-itemSizeDp * 2) |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| // [item x 4, item x 2, item x 3] |
| rule.onNodeWithTag("4") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(-itemSizeDp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| |
| // [item x 4, item x 7, item x 3] |
| rule.onNodeWithTag("5") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2) |
| |
| // [item x 4, item x 7, item x 9] |
| } |
| |
| @Test |
| fun itemSizeInLayoutInfo() { |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp), |
| ) { |
| items(6) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp * (it + 1) |
| ) |
| .testTag("$it") |
| .debugBorder() |
| ) |
| } |
| } |
| } |
| |
| state.scrollBy(itemSizeDp * 3) |
| |
| val items = state.layoutInfo.visibleItemsInfo |
| |
| assertThat(items.size).isEqualTo(3) |
| with(items[0]) { |
| assertThat(index).isEqualTo(3) |
| assertThat(size).isEqualTo(axisSize(itemSizePx, itemSizePx * 4)) |
| assertThat(offset).isEqualTo(axisOffset(0, -itemSizePx * 2)) |
| } |
| |
| with(items[1]) { |
| assertThat(index).isEqualTo(4) |
| assertThat(size).isEqualTo(axisSize(itemSizePx, itemSizePx * 5)) |
| assertThat(offset).isEqualTo(axisOffset(itemSizePx, -itemSizePx)) |
| } |
| |
| with(items[2]) { |
| assertThat(index).isEqualTo(5) |
| assertThat(size).isEqualTo(axisSize(itemSizePx, itemSizePx * 6)) |
| assertThat(offset).isEqualTo(axisOffset(itemSizePx * 2, 0)) |
| } |
| } |
| |
| @Test |
| fun itemCanEmitZeroNodes() { |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier |
| .axisSize(itemSizeDp * 3, itemSizeDp) |
| .testTag(LazyStaggeredGridTag), |
| ) { |
| items(6) { } |
| } |
| } |
| |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .assertIsDisplayed() |
| .onChildren() |
| .assertCountEquals(0) |
| } |
| |
| @Test |
| fun itemsAreHiddenOnScroll() { |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp), |
| ) { |
| items(6) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp * (it + 1) |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertIsDisplayed() |
| |
| state.scrollBy(itemSizeDp * 3) |
| |
| rule.onNodeWithTag("0") |
| .assertIsNotDisplayed() |
| |
| rule.onNodeWithTag("1") |
| .assertIsNotDisplayed() |
| |
| rule.onNodeWithTag("2") |
| .assertIsNotDisplayed() |
| } |
| |
| @Test |
| fun itemsArePresentedWhenScrollingBack() { |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier.axisSize(itemSizeDp * 3, itemSizeDp), |
| ) { |
| items(6) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp * (it + 1) |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertIsDisplayed() |
| |
| state.scrollBy(itemSizeDp * 3) |
| state.scrollBy(-itemSizeDp * 3) |
| |
| for (i in 0..2) { |
| rule.onNodeWithTag("$i") |
| .assertIsDisplayed() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| } |
| } |
| |
| @Test |
| fun itemsAreCorrectedWhenSizeIncreased() { |
| var expanded by mutableStateOf(false) |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp * 2 |
| ), |
| ) { |
| item { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = if (expanded) itemSizeDp * 2 else itemSizeDp |
| ) |
| .testTag("0") |
| ) |
| } |
| items(5) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp |
| ) |
| .testTag("${it + 1}") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertMainAxisSizeIsEqualTo(itemSizeDp) |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("1") |
| .assertMainAxisSizeIsEqualTo(itemSizeDp) |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("2") |
| .assertMainAxisSizeIsEqualTo(itemSizeDp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| |
| state.scrollBy(itemSizeDp * 3) |
| |
| expanded = true |
| |
| state.scrollBy(-itemSizeDp * 3) |
| |
| rule.onNodeWithTag("0") |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisSizeIsEqualTo(itemSizeDp * 2) |
| |
| rule.onNodeWithTag("2") |
| .assertMainAxisSizeIsEqualTo(itemSizeDp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| } |
| |
| @Test |
| fun itemsAreCorrectedWhenSizeDecreased() { |
| var expanded by mutableStateOf(true) |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp * 2 |
| ), |
| ) { |
| item { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = if (expanded) itemSizeDp * 2 else itemSizeDp |
| ) |
| .testTag("0") |
| ) |
| } |
| items(5) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp |
| ) |
| .testTag("${it + 1}") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertMainAxisSizeIsEqualTo(itemSizeDp * 2) |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("1") |
| .assertMainAxisSizeIsEqualTo(itemSizeDp) |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("2") |
| .assertMainAxisSizeIsEqualTo(itemSizeDp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| |
| state.scrollBy(itemSizeDp * 3) |
| |
| expanded = false |
| |
| state.scrollBy(-itemSizeDp * 3) |
| |
| rule.onNodeWithTag("0") |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisSizeIsEqualTo(itemSizeDp) |
| |
| rule.onNodeWithTag("2") |
| .assertMainAxisSizeIsEqualTo(itemSizeDp) |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| } |
| |
| @Test |
| fun itemsAreCorrectedWhenItemCountIsIncreasedFromZero() { |
| var itemCount by mutableStateOf(0) |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp * 2 |
| ), |
| ) { |
| items(itemCount) { |
| Spacer( |
| Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertDoesNotExist() |
| |
| itemCount = 4 |
| |
| rule.onNodeWithTag("0") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("1") |
| .assertIsDisplayed() |
| } |
| |
| @Test |
| fun itemsAreCorrectedWithWrongColumns() { |
| rule.setContent { |
| // intentionally wrong values, normally items should be [0, 1][2, 3][4, 5] |
| state = rememberLazyStaggeredGridState( |
| initialFirstVisibleItemIndex = 3, |
| initialFirstVisibleItemScrollOffset = itemSizePx / 2 |
| ) |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp |
| ), |
| ) { |
| items(6) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertDoesNotExist() |
| |
| rule.onNodeWithTag("3") |
| .assertMainAxisStartPositionInRootIsEqualTo(-itemSizeDp / 2) |
| |
| rule.onNodeWithTag("4") |
| .assertMainAxisSizeIsEqualTo(itemSizeDp) |
| .assertMainAxisStartPositionInRootIsEqualTo(-itemSizeDp / 2) |
| |
| state.scrollBy(-itemSizeDp * 3) |
| |
| rule.onNodeWithTag("0") |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("1") |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun itemsAreCorrectedWithAlignedOffsets() { |
| var expanded by mutableStateOf(false) |
| rule.setContent { |
| state = rememberLazyStaggeredGridState( |
| initialFirstVisibleItemIndex = 0, |
| ) |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp |
| ), |
| ) { |
| items(6) { |
| Spacer( |
| Modifier |
| .mainAxisSize( |
| if (it % 2 == 1 && expanded) itemSizeDp * 2 else itemSizeDp |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertIsDisplayed() |
| |
| state.scrollBy(itemSizeDp * 2) |
| |
| rule.runOnIdle { |
| expanded = true |
| } |
| |
| state.scrollBy(itemSizeDp * -2) |
| state.scrollBy(-itemSizeDp) |
| |
| rule.onNodeWithTag("0") |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("1") |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun itemsAreCorrectedWhenItemIncreased() { |
| var expanded by mutableStateOf(false) |
| rule.setContent { |
| state = rememberLazyStaggeredGridState( |
| initialFirstVisibleItemIndex = 0, |
| ) |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp |
| ), |
| ) { |
| items(6) { |
| Spacer( |
| Modifier |
| .mainAxisSize( |
| if (it == 3 && expanded) itemSizeDp * 2 else itemSizeDp |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertIsDisplayed() |
| |
| state.scrollBy(itemSizeDp * 2) |
| |
| rule.runOnIdle { |
| expanded = true |
| } |
| |
| state.scrollBy(itemSizeDp * -2) |
| state.scrollBy(-itemSizeDp) |
| |
| rule.onNodeWithTag("0") |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("1") |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp) |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun addItems() { |
| val state = LazyStaggeredGridState() |
| var itemsCount by mutableStateOf(1) |
| rule.setContent { |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp |
| ), |
| ) { |
| items(itemsCount) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("1") |
| .assertDoesNotExist() |
| |
| itemsCount = 10 |
| |
| rule.waitForIdle() |
| |
| state.scrollBy(itemSizeDp * 10) |
| |
| rule.onNodeWithTag("8") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("9") |
| .assertIsDisplayed() |
| |
| itemsCount = 20 |
| |
| rule.waitForIdle() |
| |
| state.scrollBy(itemSizeDp * 10) |
| |
| rule.onNodeWithTag("18") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("19") |
| .assertIsDisplayed() |
| } |
| |
| @Test |
| fun removeItems() { |
| var itemsCount by mutableStateOf(20) |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp |
| ), |
| ) { |
| items(itemsCount) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| state.scrollBy(itemSizeDp * 20) |
| |
| rule.onNodeWithTag("18") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("19") |
| .assertIsDisplayed() |
| |
| itemsCount = 10 |
| |
| rule.onNodeWithTag("8") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("9") |
| .assertIsDisplayed() |
| |
| itemsCount = 1 |
| |
| rule.onNodeWithTag("0") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("1") |
| // seems like reuse keeps the node around? |
| .assertIsNotDisplayed() |
| } |
| |
| @Test |
| fun resizingItems_maintainsScrollingRange() { |
| val state = LazyStaggeredGridState() |
| var itemSizes by mutableStateOf( |
| List(10) { |
| itemSizeDp * (it % 4 + 1) |
| } |
| ) |
| rule.setContent { |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier |
| .axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp * 5 |
| ) |
| .testTag(LazyStaggeredGridTag) |
| .border(1.dp, Color.Red), |
| ) { |
| items(itemSizes.size) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizes[it] |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .scrollMainAxisBy(itemSizeDp * 10) |
| |
| rule.onNodeWithTag("8") |
| .assertMainAxisSizeIsEqualTo(itemSizes[8]) |
| |
| rule.onNodeWithTag("9") |
| .assertMainAxisSizeIsEqualTo(itemSizes[9]) |
| |
| itemSizes = itemSizes.reversed() |
| |
| rule.onNodeWithTag("8") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag("9") |
| .assertIsDisplayed() |
| |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .scrollMainAxisBy(-itemSizeDp * 10) |
| |
| rule.onNodeWithTag("0") |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("1") |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun removingItems_maintainsCorrectOffsets() { |
| var itemCount by mutableStateOf(20) |
| rule.setContent { |
| state = rememberLazyStaggeredGridState( |
| initialFirstVisibleItemIndex = 10, |
| initialFirstVisibleItemScrollOffset = 0 |
| ) |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier |
| .axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp * 5 |
| ) |
| .testTag(LazyStaggeredGridTag) |
| .border(1.dp, Color.Red), |
| ) { |
| items(itemCount) { |
| Box( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp * (it % 3 + 1) |
| ) |
| .testTag("$it") |
| .border(1.dp, Color.Black) |
| ) { |
| BasicText("$it") |
| } |
| } |
| } |
| } |
| |
| itemCount = 3 |
| |
| rule.waitForIdle() |
| |
| rule.onNodeWithTag("0") |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("1") |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun staggeredGrid_supportsLargeIndices() { |
| rule.setContent { |
| state = rememberLazyStaggeredGridState( |
| initialFirstVisibleItemIndex = Int.MAX_VALUE / 2, |
| initialFirstVisibleItemScrollOffset = 0 |
| ) |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier |
| .axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp * 5 |
| ) |
| .testTag(LazyStaggeredGridTag) |
| .border(1.dp, Color.Red), |
| ) { |
| items(Int.MAX_VALUE) { |
| Spacer( |
| Modifier |
| .axisSize( |
| crossAxis = itemSizeDp, |
| mainAxis = itemSizeDp |
| ) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("${Int.MAX_VALUE / 2}") |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("${Int.MAX_VALUE / 2 + 1}") |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| // check that scrolling back and forth doesn't crash |
| val delta = itemSizeDp * 5 |
| state.scrollBy(-delta) |
| |
| state.scrollBy(delta * 2) |
| |
| state.scrollBy(-delta) |
| |
| rule.onNodeWithTag("${Int.MAX_VALUE / 2}") |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("${Int.MAX_VALUE / 2 + 1}") |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun scrollPositionIsRestored() { |
| val restorationTester = StateRestorationTester(rule) |
| var state: LazyStaggeredGridState? |
| |
| restorationTester.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state!!, |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp * 10) |
| .testTag(LazyStaggeredGridTag) |
| ) { |
| items(1000) { |
| Spacer( |
| Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .scrollMainAxisBy(itemSizeDp * 10f) |
| |
| rule.onNodeWithTag("30") |
| .assertIsDisplayed() |
| |
| state = null |
| restorationTester.emulateSavedInstanceStateRestore() |
| |
| rule.onNodeWithTag("30") |
| .assertIsDisplayed() |
| } |
| |
| @Test |
| fun restoredScrollPositionIsCorrectWhenItemsAreLoadedAsynchronously() { |
| val restorationTester = StateRestorationTester(rule) |
| |
| var itemsCount = 100 |
| val recomposeCounter = mutableStateOf(0) |
| |
| restorationTester.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp * 10) |
| .testTag(LazyStaggeredGridTag) |
| ) { |
| recomposeCounter.value // read state to force recomposition |
| |
| items(itemsCount) { |
| Spacer( |
| Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.runOnIdle { |
| runBlocking { |
| state.scrollToItem(9, 10) |
| } |
| itemsCount = 0 |
| } |
| |
| restorationTester.emulateSavedInstanceStateRestore() |
| |
| rule.runOnIdle { |
| itemsCount = 100 |
| recomposeCounter.value = 1 |
| } |
| |
| rule.runOnIdle { |
| assertThat(state.firstVisibleItemIndex).isEqualTo(9) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10) |
| } |
| } |
| |
| @Test |
| fun screenRotate_oneItem_withAdaptiveCells_fillsContentCorrectly() { |
| var rotated by mutableStateOf(false) |
| |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| |
| val crossAxis = if (!rotated) itemSizeDp * 6 else itemSizeDp * 9 |
| val mainAxis = if (!rotated) itemSizeDp * 9 else itemSizeDp * 6 |
| |
| LazyStaggeredGrid( |
| cells = StaggeredGridCells.Adaptive(itemSizeDp * 3), |
| state = state, |
| modifier = Modifier |
| .mainAxisSize(mainAxis) |
| .crossAxisSize(crossAxis) |
| .testTag(LazyStaggeredGridTag) |
| ) { |
| item { |
| Spacer( |
| Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag("0") |
| ) |
| } |
| } |
| } |
| |
| fun verifyState() { |
| assertThat(state.firstVisibleItemIndex).isEqualTo(0) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) |
| } |
| |
| rule.runOnIdle { |
| rotated = true |
| } |
| |
| rule.runOnIdle { |
| verifyState() |
| } |
| |
| rule.runOnIdle { |
| rotated = false |
| } |
| |
| rule.runOnIdle { |
| verifyState() |
| } |
| |
| rule.runOnIdle { |
| rotated = true |
| } |
| |
| rule.runOnIdle { |
| verifyState() |
| } |
| } |
| |
| @Test |
| fun screenRotate_twoItems_withAdaptiveCells_fillsContentCorrectly() { |
| var rotated by mutableStateOf(false) |
| |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| |
| val crossAxis = if (!rotated) itemSizeDp * 6 else itemSizeDp * 9 |
| val mainAxis = if (!rotated) itemSizeDp * 9 else itemSizeDp * 6 |
| |
| LazyStaggeredGrid( |
| cells = StaggeredGridCells.Adaptive(itemSizeDp * 3), |
| state = state, |
| modifier = Modifier |
| .mainAxisSize(mainAxis) |
| .crossAxisSize(crossAxis) |
| .testTag(LazyStaggeredGridTag) |
| ) { |
| items(2) { |
| Spacer( |
| Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| fun verifyState() { |
| rule.runOnIdle { |
| assertThat(state.firstVisibleItemIndex).isEqualTo(0) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) |
| } |
| rule.onNodeWithTag("0").assertIsDisplayed() |
| rule.onNodeWithTag("1").assertIsDisplayed() |
| } |
| |
| rule.runOnIdle { |
| rotated = true |
| } |
| |
| verifyState() |
| |
| rule.runOnIdle { |
| rotated = false |
| } |
| |
| verifyState() |
| |
| rule.runOnIdle { |
| rotated = true |
| } |
| |
| verifyState() |
| } |
| |
| @Test |
| fun scrollingALot_layoutIsNotRecomposed() { |
| var recomposed = 0 |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp * 10) |
| .composed { |
| recomposed++ |
| Modifier |
| } |
| ) { |
| items(1000) { |
| Spacer( |
| Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.waitForIdle() |
| assertThat(recomposed).isEqualTo(1) |
| |
| state.scrollBy(1000.dp) |
| |
| rule.waitForIdle() |
| assertThat(recomposed).isEqualTo(1) |
| } |
| |
| @Test |
| fun onlyOneInitialMeasurePass() { |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp * 10) |
| .composed { |
| Modifier |
| } |
| ) { |
| items(1000) { |
| Spacer( |
| Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag("$it") |
| ) |
| } |
| } |
| } |
| |
| rule.waitForIdle() |
| assertThat(state.measurePassCount).isEqualTo(1) |
| } |
| |
| @Test |
| fun fillingFullSize_nextItemIsNotComposed() { |
| val state = LazyStaggeredGridState() |
| state.prefetchingEnabled = false |
| val itemSizePx = 5f |
| val itemSize = with(rule.density) { itemSizePx.toDp() } |
| rule.setContentWithTestViewConfiguration { |
| LazyStaggeredGrid( |
| 1, |
| Modifier |
| .testTag(LazyStaggeredGridTag) |
| .mainAxisSize(itemSize), |
| state |
| ) { |
| items(3) { index -> |
| Box( |
| Modifier |
| .size(itemSize) |
| .testTag("$index")) |
| } |
| } |
| } |
| |
| repeat(3) { index -> |
| rule.onNodeWithTag("$index") |
| .assertIsDisplayed() |
| rule.onNodeWithTag("${index + 1}") |
| .assertDoesNotExist() |
| rule.runOnIdle { |
| runBlocking { |
| state.scrollBy(itemSizePx) |
| } |
| } |
| } |
| } |
| |
| @Test |
| fun fullSpan_fillsAllCrossAxisSpace() { |
| val state = LazyStaggeredGridState() |
| state.prefetchingEnabled = false |
| rule.setContentWithTestViewConfiguration { |
| LazyStaggeredGrid( |
| 3, |
| Modifier |
| .testTag(LazyStaggeredGridTag) |
| .crossAxisSize(itemSizeDp * 3) |
| .mainAxisSize(itemSizeDp * 10), |
| state |
| ) { |
| item(span = StaggeredGridItemSpan.FullLine) { |
| Box( |
| Modifier |
| .testTag("0") |
| .mainAxisSize(itemSizeDp)) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertMainAxisSizeIsEqualTo(itemSizeDp) |
| .assertCrossAxisSizeIsEqualTo(itemSizeDp * 3) |
| .assertPositionInRootIsEqualTo(0.dp, 0.dp) |
| } |
| |
| @Test |
| fun fullSpan_leavesEmptyGapsWithOtherItems() { |
| val state = LazyStaggeredGridState() |
| state.prefetchingEnabled = false |
| rule.setContentWithTestViewConfiguration { |
| LazyStaggeredGrid( |
| 3, |
| Modifier |
| .testTag(LazyStaggeredGridTag) |
| .crossAxisSize(itemSizeDp * 3) |
| .mainAxisSize(itemSizeDp * 10), |
| state |
| ) { |
| items(2) { |
| Box( |
| Modifier |
| .testTag("$it") |
| .mainAxisSize(itemSizeDp)) |
| } |
| |
| item(span = StaggeredGridItemSpan.FullLine) { |
| Box( |
| Modifier |
| .testTag("full") |
| .mainAxisSize(itemSizeDp)) |
| } |
| } |
| } |
| |
| // ┌─┬─┬─┐ |
| // │0│1│#│ |
| // ├─┴─┴─┤ |
| // │full │ |
| // └─────┘ |
| rule.onNodeWithTag("0") |
| .assertAxisBounds( |
| DpOffset(0.dp, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp) |
| ) |
| |
| rule.onNodeWithTag("1") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp) |
| ) |
| |
| rule.onNodeWithTag("full") |
| .assertAxisBounds( |
| DpOffset(0.dp, itemSizeDp), |
| DpSize(itemSizeDp * 3, itemSizeDp) |
| ) |
| } |
| |
| @Test |
| fun fullSpan_leavesGapsBetweenItems() { |
| val state = LazyStaggeredGridState() |
| state.prefetchingEnabled = false |
| rule.setContentWithTestViewConfiguration { |
| LazyStaggeredGrid( |
| 3, |
| Modifier |
| .testTag(LazyStaggeredGridTag) |
| .crossAxisSize(itemSizeDp * 3) |
| .mainAxisSize(itemSizeDp * 10), |
| state |
| ) { |
| items(3) { |
| Box( |
| Modifier |
| .testTag("$it") |
| .mainAxisSize(itemSizeDp + itemSizeDp * it / 2)) |
| } |
| |
| item(span = StaggeredGridItemSpan.FullLine) { |
| Box( |
| Modifier |
| .testTag("full") |
| .mainAxisSize(itemSizeDp)) |
| } |
| } |
| } |
| |
| // ┌───┬───┬───┐ |
| // │ 0 │ 1 │ 2 │ |
| // ├───┤ │ │ |
| // │ └───┤ │ |
| // ├───────┴───┤ |
| // │ full │ |
| // └───────────┘ |
| rule.onNodeWithTag("0") |
| .assertAxisBounds( |
| DpOffset(0.dp, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp) |
| ) |
| |
| rule.onNodeWithTag("1") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp * 1.5f) |
| ) |
| |
| rule.onNodeWithTag("2") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp * 2, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp * 2f) |
| ) |
| |
| rule.onNodeWithTag("full") |
| .assertAxisBounds( |
| DpOffset(0.dp, itemSizeDp * 2f), |
| DpSize(itemSizeDp * 3, itemSizeDp) |
| ) |
| } |
| |
| @Test |
| fun fullSpan_scrollsCorrectly() { |
| val state = LazyStaggeredGridState() |
| state.prefetchingEnabled = false |
| rule.setContentWithTestViewConfiguration { |
| LazyStaggeredGrid( |
| 3, |
| Modifier |
| .testTag(LazyStaggeredGridTag) |
| .crossAxisSize(itemSizeDp * 3) |
| .mainAxisSize(itemSizeDp * 2), |
| state |
| ) { |
| items(3) { |
| Box( |
| Modifier |
| .testTag("$it") |
| .mainAxisSize(itemSizeDp + itemSizeDp * it / 2) |
| ) |
| } |
| |
| item(span = StaggeredGridItemSpan.FullLine) { |
| Box( |
| Modifier |
| .testTag("full") |
| .mainAxisSize(itemSizeDp)) |
| } |
| |
| items(3) { |
| Box( |
| Modifier |
| .testTag("${it + 3}") |
| .mainAxisSize(itemSizeDp + itemSizeDp * it / 2) |
| ) |
| } |
| |
| item(span = StaggeredGridItemSpan.FullLine) { |
| Box( |
| Modifier |
| .testTag("full-2") |
| .mainAxisSize(itemSizeDp)) |
| } |
| } |
| } |
| |
| // ┌───┬───┬───┐ |
| // │ 0 │ 1 │ 2 │ |
| // ├───┤ │ │ |
| // │ └───┤ │ |
| // ├───────┴───┤ <-- scroll offset |
| // │ full │ |
| // ├───┬───┬───┤ |
| // │ 3 │ 4 │ 5 │ |
| // ├───┤ │ │ <-- end of screen |
| // │ └───┤ │ |
| // ├───────┴───┤ |
| // │ full-2 │ |
| // └───────────┘ |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .scrollMainAxisBy(itemSizeDp * 2f) |
| |
| rule.onNodeWithTag("full") |
| .assertAxisBounds( |
| DpOffset(0.dp, 0.dp), |
| DpSize(itemSizeDp * 3, itemSizeDp) |
| ) |
| |
| assertThat(state.firstVisibleItemIndex).isEqualTo(3) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) |
| } |
| |
| @Test |
| fun fullSpan_scrollsCorrectly_pastFullSpan() { |
| val state = LazyStaggeredGridState() |
| state.prefetchingEnabled = false |
| rule.setContentWithTestViewConfiguration { |
| LazyStaggeredGrid( |
| 3, |
| Modifier |
| .testTag(LazyStaggeredGridTag) |
| .crossAxisSize(itemSizeDp * 3) |
| .mainAxisSize(itemSizeDp * 2), |
| state |
| ) { |
| repeat(10) { repeatIndex -> |
| items(3) { |
| Box( |
| Modifier |
| .testTag("${repeatIndex * 3 + it}") |
| .mainAxisSize(itemSizeDp + itemSizeDp * it / 2) |
| ) |
| } |
| |
| item(span = StaggeredGridItemSpan.FullLine) { |
| Box( |
| Modifier |
| .testTag("full-$repeatIndex") |
| .mainAxisSize(itemSizeDp)) |
| } |
| } |
| } |
| } |
| |
| // ┌───┬───┬───┐ |
| // │ 0 │ 1 │ 2 │ |
| // ├───┤ │ │ |
| // │ └───┤ │ |
| // ├───────┴───┤ |
| // │ full-0 │ |
| // ├───┬───┬───┤ <-- scroll offset |
| // │ 3 │ 4 │ 5 │ |
| // ├───┤ │ │ |
| // │ └───┤ │ |
| // ├───────┴───┤ <-- end of screen |
| // │ full-1 │ |
| // └───────────┘ |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .scrollMainAxisBy(itemSizeDp * 3f) |
| |
| rule.onNodeWithTag("3") |
| .assertAxisBounds( |
| DpOffset(0.dp, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp) |
| ) |
| |
| rule.onNodeWithTag("4") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp * 1.5f) |
| ) |
| |
| rule.onNodeWithTag("5") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp * 2, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp * 2) |
| ) |
| |
| assertThat(state.firstVisibleItemIndex).isEqualTo(4) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) |
| } |
| |
| @Test |
| fun fullSpan_scrollsCorrectly_pastFullSpan_andBack() { |
| val state = LazyStaggeredGridState() |
| state.prefetchingEnabled = false |
| rule.setContentWithTestViewConfiguration { |
| LazyStaggeredGrid( |
| 3, |
| Modifier |
| .testTag(LazyStaggeredGridTag) |
| .crossAxisSize(itemSizeDp * 3) |
| .mainAxisSize(itemSizeDp * 2), |
| state |
| ) { |
| repeat(10) { repeatIndex -> |
| items(3) { |
| Box( |
| Modifier |
| .testTag("${repeatIndex * 3 + it}") |
| .mainAxisSize(itemSizeDp + itemSizeDp * it / 2) |
| ) |
| } |
| |
| item(span = StaggeredGridItemSpan.FullLine) { |
| Box( |
| Modifier |
| .testTag("full-$repeatIndex") |
| .mainAxisSize(itemSizeDp)) |
| } |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .scrollMainAxisBy(itemSizeDp * 3f) |
| |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .scrollMainAxisBy(-itemSizeDp * 3f) |
| |
| // ┌───┬───┬───┐ <-- scroll offset |
| // │ 0 │ 1 │ 2 │ |
| // ├───┤ │ │ |
| // │ └───┤ │ |
| // ├───────┴───┤ <-- end of screen |
| // │ full-0 │ |
| // ├───┬───┬───┤ |
| // │ 3 │ 4 │ 5 │ |
| // ├───┤ │ │ |
| // │ └───┤ │ |
| // ├───────┴───┤ |
| // │ full-1 │ |
| // └───────────┘ |
| |
| rule.onNodeWithTag("0") |
| .assertAxisBounds( |
| DpOffset(0.dp, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp) |
| ) |
| |
| rule.onNodeWithTag("1") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp * 1.5f) |
| ) |
| |
| rule.onNodeWithTag("2") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp * 2, 0.dp), |
| DpSize(itemSizeDp, itemSizeDp * 2) |
| ) |
| |
| assertThat(state.firstVisibleItemIndex).isEqualTo(0) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) |
| } |
| |
| @Test |
| fun fullSpan_scrollsCorrectly_multipleFullSpans() { |
| val state = LazyStaggeredGridState() |
| state.prefetchingEnabled = false |
| rule.setContentWithTestViewConfiguration { |
| LazyStaggeredGrid( |
| 3, |
| Modifier |
| .testTag(LazyStaggeredGridTag) |
| .crossAxisSize(itemSizeDp * 3) |
| .mainAxisSize(itemSizeDp * 2), |
| state |
| ) { |
| items(10, span = { StaggeredGridItemSpan.FullLine }) { |
| Box( |
| Modifier |
| .testTag("$it") |
| .mainAxisSize(itemSizeDp) |
| ) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .scrollMainAxisBy(itemSizeDp * 3f) |
| |
| rule.onNodeWithTag("3") |
| .assertAxisBounds( |
| DpOffset(0.dp, 0.dp), |
| DpSize(itemSizeDp * 3, itemSizeDp) |
| ) |
| |
| rule.onNodeWithTag("4") |
| .assertAxisBounds( |
| DpOffset(0.dp, itemSizeDp), |
| DpSize(itemSizeDp * 3, itemSizeDp) |
| ) |
| |
| assertThat(state.firstVisibleItemIndex).isEqualTo(3) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) |
| |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .scrollMainAxisBy(itemSizeDp * 10f) |
| |
| rule.onNodeWithTag("8") |
| .assertAxisBounds( |
| DpOffset(0.dp, 0.dp), |
| DpSize(itemSizeDp * 3, itemSizeDp) |
| ) |
| |
| rule.onNodeWithTag("9") |
| .assertAxisBounds( |
| DpOffset(0.dp, itemSizeDp), |
| DpSize(itemSizeDp * 3, itemSizeDp) |
| ) |
| |
| assertThat(state.firstVisibleItemIndex).isEqualTo(8) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) |
| } |
| |
| @Test |
| fun initialIndex_largerThanItemCount_ordersItemsCorrectly_withFullSpan() { |
| rule.setContent { |
| state = rememberLazyStaggeredGridState(20) |
| Box(Modifier.mainAxisSize(itemSizeDp * 4)) { |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier |
| .crossAxisSize(itemSizeDp * 3) |
| .testTag(LazyStaggeredGridTag), |
| ) { |
| item(span = StaggeredGridItemSpan.FullLine) { |
| Spacer( |
| Modifier |
| .testTag("full") |
| .mainAxisSize(itemSizeDp * 2) |
| ) |
| } |
| items(6) { |
| val size = when (it) { |
| 0, 3 -> itemSizeDp * 2 |
| 1, 4 -> itemSizeDp * 1.5f |
| 2, 5 -> itemSizeDp |
| else -> error("unexpected item $it") |
| } |
| Spacer( |
| Modifier |
| .testTag("$it") |
| .mainAxisSize(size) |
| ) |
| } |
| } |
| } |
| } |
| |
| // ┌───────────┐ |
| // │ │ |
| // │ full │ <-- scroll offset |
| // │ │ |
| // ├───┬───┬───┤ |
| // │ 0 │ 1 │ 2 │ |
| // │ │ ├───┤ |
| // │ │───┤ 3 │ |
| // ├───┤ 4 │ │ |
| // │ 5 │ │ │ |
| // └───┴───┴───┘ <-- end of grid |
| |
| rule.onNodeWithTag("full") |
| .assertAxisBounds( |
| DpOffset(0.dp, -itemSizeDp), DpSize(itemSizeDp * 3, itemSizeDp * 2) |
| ) |
| |
| rule.onNodeWithTag("0") |
| .assertAxisBounds( |
| DpOffset(0.dp, itemSizeDp), DpSize(itemSizeDp, itemSizeDp * 2f) |
| ) |
| |
| rule.onNodeWithTag("1") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp, itemSizeDp), DpSize(itemSizeDp, itemSizeDp * 1.5f) |
| ) |
| |
| rule.onNodeWithTag("2") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp * 2f, itemSizeDp), DpSize(itemSizeDp, itemSizeDp) |
| ) |
| |
| rule.onNodeWithTag("3") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp * 2f, itemSizeDp * 2f), DpSize(itemSizeDp, itemSizeDp * 2) |
| ) |
| |
| rule.onNodeWithTag("4") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp, itemSizeDp * 2.5f), DpSize(itemSizeDp, itemSizeDp * 1.5f) |
| ) |
| |
| rule.onNodeWithTag("5") |
| .assertAxisBounds( |
| DpOffset(0.dp, itemSizeDp * 3), DpSize(itemSizeDp, itemSizeDp) |
| ) |
| } |
| |
| @Test |
| fun initialIndex_largerThanItemCount_ordersItemsCorrectly() { |
| rule.setContent { |
| state = rememberLazyStaggeredGridState(20) |
| Box(Modifier.mainAxisSize(itemSizeDp * 4)) { |
| LazyStaggeredGrid( |
| lanes = 3, |
| state = state, |
| modifier = Modifier |
| .crossAxisSize(itemSizeDp * 3) |
| .testTag(LazyStaggeredGridTag), |
| ) { |
| items(6) { |
| val size = when (it) { |
| 0, 3 -> itemSizeDp * 2 |
| 1, 4 -> itemSizeDp * 1.5f |
| 2, 5 -> itemSizeDp |
| else -> error("unexpected item $it") |
| } |
| Spacer( |
| Modifier |
| .testTag("$it") |
| .mainAxisSize(size) |
| ) |
| } |
| } |
| } |
| } |
| |
| // ┌───┬───┬───┐ |
| // │ 0 │ 1 │ 2 │ |
| // │ │ ├───┤ |
| // │ │───┤ 3 │ |
| // ├───┤ 4 │ │ |
| // │ 5 │ │ │ |
| // └───┴───┴───┘ |
| |
| rule.onNodeWithTag(LazyStaggeredGridTag) |
| .assertMainAxisSizeIsEqualTo(itemSizeDp * 3) |
| |
| rule.onNodeWithTag("0") |
| .assertAxisBounds( |
| DpOffset(0.dp, 0.dp), DpSize(itemSizeDp, itemSizeDp * 2f) |
| ) |
| |
| rule.onNodeWithTag("1") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp, 0.dp), DpSize(itemSizeDp, itemSizeDp * 1.5f) |
| ) |
| |
| rule.onNodeWithTag("2") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp * 2f, 0.dp), DpSize(itemSizeDp, itemSizeDp) |
| ) |
| |
| rule.onNodeWithTag("3") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp * 2f, itemSizeDp), DpSize(itemSizeDp, itemSizeDp * 2) |
| ) |
| |
| rule.onNodeWithTag("4") |
| .assertAxisBounds( |
| DpOffset(itemSizeDp, itemSizeDp * 1.5f), DpSize(itemSizeDp, itemSizeDp * 1.5f) |
| ) |
| |
| rule.onNodeWithTag("5") |
| .assertAxisBounds( |
| DpOffset(0.dp, itemSizeDp * 2), DpSize(itemSizeDp, itemSizeDp) |
| ) |
| } |
| |
| @Test |
| fun changeItemsAndScrollImmediately() { |
| val keys = mutableStateListOf<Int>().also { list -> |
| repeat(10) { |
| list.add(it) |
| } |
| } |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 2, |
| Modifier.mainAxisSize(itemSizeDp), |
| state |
| ) { |
| items(keys, key = { it }) { |
| Box(Modifier.size(itemSizeDp * 2)) |
| } |
| } |
| } |
| |
| rule.waitForIdle() |
| state.scrollTo(8) |
| |
| rule.runOnIdle { |
| assertThat(state.firstVisibleItemIndex).isEqualTo(8) |
| |
| keys.add(0, -1) |
| keys.add(0, -2) |
| |
| runBlocking(AutoTestFrameClock()) { |
| state.scrollBy(10f) |
| state.scrollBy(-10f) |
| } |
| |
| assertThat(state.firstVisibleItemIndex).isEqualTo(10) |
| } |
| } |
| |
| @Test |
| fun fixedSizeCell_forcesFixedSize() { |
| val state = LazyStaggeredGridState() |
| rule.setContent { |
| LazyStaggeredGrid( |
| cells = StaggeredGridCells.FixedSize(itemSizeDp * 2), |
| modifier = Modifier.axisSize(crossAxis = itemSizeDp * 5, mainAxis = itemSizeDp * 5), |
| state = state |
| ) { |
| items(10) { index -> |
| Box( |
| Modifier |
| .size(itemSizeDp) |
| .testTag(index.toString())) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertCrossAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertCrossAxisSizeIsEqualTo(itemSizeDp * 2) |
| rule.onNodeWithTag("1") |
| .assertCrossAxisStartPositionInRootIsEqualTo(itemSizeDp * 2f) |
| .assertCrossAxisSizeIsEqualTo(itemSizeDp * 2) |
| } |
| |
| @Test |
| fun manyPlaceablesInItem_itemSizeIsMaxOfPlaceables() { |
| val state = LazyStaggeredGridState() |
| rule.setContent { |
| LazyStaggeredGrid( |
| lanes = 2, |
| modifier = Modifier.axisSize(crossAxis = itemSizeDp * 2, mainAxis = itemSizeDp * 5), |
| state = state |
| ) { |
| item(span = StaggeredGridItemSpan.FullLine) { |
| Box(Modifier.size(itemSizeDp * 2)) |
| Box(Modifier.size(itemSizeDp)) |
| } |
| |
| items(10) { index -> |
| Box( |
| Modifier |
| .size(itemSizeDp) |
| .testTag(index.toString())) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertAxisBounds(DpOffset(0.dp, itemSizeDp * 2), DpSize(itemSizeDp, itemSizeDp)) |
| rule.onNodeWithTag("1") |
| .assertAxisBounds(DpOffset(itemSizeDp, itemSizeDp * 2), DpSize(itemSizeDp, itemSizeDp)) |
| } |
| |
| @Test |
| fun scrollDuringMeasure() { |
| rule.setContent { |
| BoxWithConstraints { |
| val state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 1, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp * 5 |
| ), |
| ) { |
| items(20) { |
| Spacer( |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag(it.toString()) |
| ) |
| } |
| } |
| LaunchedEffect(state) { |
| state.scrollToItem(10) |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("10") |
| .assertStartPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun scrollInLaunchedEffect() { |
| rule.setContent { |
| val state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 1, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp * 5 |
| ), |
| ) { |
| items(20) { |
| Spacer( |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag(it.toString()) |
| ) |
| } |
| } |
| LaunchedEffect(state) { |
| state.scrollToItem(10) |
| } |
| } |
| |
| rule.onNodeWithTag("10") |
| .assertStartPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun scrollToPreviouslyFullSpanItem() { |
| var firstItemVisible by mutableStateOf(false) |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier.axisSize( |
| crossAxis = itemSizeDp * 2, |
| mainAxis = itemSizeDp * 2 |
| ), |
| ) { |
| if (firstItemVisible) { |
| item { |
| Spacer( |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag("first") |
| ) |
| } |
| } |
| |
| items( |
| count = 20, |
| span = { |
| if (it == 10) |
| StaggeredGridItemSpan.FullLine |
| else |
| StaggeredGridItemSpan.SingleLane |
| } |
| ) { |
| Spacer( |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp) |
| .testTag(it.toString()) |
| ) |
| } |
| } |
| } |
| |
| rule.runOnIdle { |
| runBlocking(AutoTestFrameClock()) { |
| state.scrollToItem(10) |
| } |
| |
| firstItemVisible = true |
| |
| runBlocking(AutoTestFrameClock()) { |
| state.scrollToItem(17) |
| |
| state.scrollToItem(10) |
| } |
| } |
| |
| rule.onNodeWithTag("9") |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| } |
| |
| @Test |
| fun itemsRemovedAfterLargeThenSmallScrollForward() { |
| lateinit var state: LazyStaggeredGridState |
| val composedItems = mutableSetOf<Int>() |
| rule.setContent { |
| state = rememberLazyStaggeredGridState() |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp * 1.5f) |
| .crossAxisSize(itemSizeDp * 2) |
| ) { |
| items(100) { |
| Spacer(Modifier.mainAxisSize(itemSizeDp)) |
| DisposableEffect(it) { |
| composedItems += it |
| onDispose { composedItems -= it } |
| } |
| } |
| } |
| } |
| |
| rule.runOnIdle { |
| runBlocking { |
| state.prefetchingEnabled = false |
| state.scrollBy(itemSizePx * 3f) |
| assertThat(state.firstVisibleItemIndex).isEqualTo(6) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0) |
| state.scrollBy(10f) |
| assertThat(state.firstVisibleItemIndex).isEqualTo(6) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10) |
| } |
| } |
| rule.runOnIdle { |
| assertThat(composedItems).isEqualTo(setOf(6, 7, 8, 9)) |
| } |
| } |
| |
| @Test |
| fun itemsRemovedAfterLargeThenSmallScrollBackward() { |
| lateinit var state: LazyStaggeredGridState |
| val composedItems = mutableSetOf<Int>() |
| rule.setContent { |
| state = rememberLazyStaggeredGridState(initialFirstVisibleItemIndex = 6) |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp * 1.5f) |
| .crossAxisSize(itemSizeDp * 2) |
| ) { |
| items(100) { |
| Spacer(Modifier.mainAxisSize(itemSizeDp)) |
| DisposableEffect(it) { |
| composedItems += it |
| onDispose { composedItems -= it } |
| } |
| } |
| } |
| } |
| |
| rule.runOnIdle { |
| runBlocking { |
| state.prefetchingEnabled = false |
| state.scrollBy(-itemSizePx * 2.5f) |
| assertThat(state.firstVisibleItemIndex).isEqualTo(0) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(itemSizePx / 2) |
| state.scrollBy(-5f) |
| assertThat(state.firstVisibleItemIndex).isEqualTo(0) |
| assertThat(state.firstVisibleItemScrollOffset).isEqualTo(itemSizePx / 2 - 5) |
| } |
| } |
| rule.runOnIdle { |
| assertThat(composedItems).isEqualTo(setOf(0, 1, 2, 3)) |
| } |
| } |
| |
| @Test |
| fun zeroSizeItemIsPlacedWhenItIsAtTheTop() { |
| lateinit var state: LazyStaggeredGridState |
| |
| rule.setContent { |
| state = rememberLazyStaggeredGridState(initialFirstVisibleItemIndex = 0) |
| LazyStaggeredGrid( |
| lanes = 2, |
| state = state, |
| modifier = Modifier |
| .mainAxisSize(itemSizeDp * 2) |
| .crossAxisSize(itemSizeDp * 2) |
| ) { |
| repeat(10) { index -> |
| items(2) { |
| Spacer(Modifier.testTag("${index * 10 + it}")) |
| } |
| items(8) { |
| Spacer(Modifier.mainAxisSize(itemSizeDp)) |
| } |
| } |
| } |
| } |
| |
| rule.onNodeWithTag("0") |
| .assertIsPlaced() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisSizeIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("1") |
| .assertIsPlaced() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisSizeIsEqualTo(0.dp) |
| |
| runBlocking(Dispatchers.Main + AutoTestFrameClock()) { |
| state.scrollToItem(10, 0) |
| } |
| |
| rule.onNodeWithTag("10") |
| .assertIsPlaced() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisSizeIsEqualTo(0.dp) |
| |
| rule.onNodeWithTag("11") |
| .assertIsPlaced() |
| .assertMainAxisStartPositionInRootIsEqualTo(0.dp) |
| .assertMainAxisSizeIsEqualTo(0.dp) |
| } |
| } |