blob: a938ed86fc05d5f2c02e9421e284702b6cae6dce [file] [log] [blame]
/*
* Copyright 2021 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.grid
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyGridLayoutInfo
import androidx.compose.foundation.lazy.LazyGridState
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@MediumTest
@OptIn(ExperimentalFoundationApi::class)
// @RunWith(Parameterized::class)
class LazyGridLayoutInfoTest(
// private val reverseLayout: Boolean = false
) {
// companion object {
// @JvmStatic
// @Parameterized.Parameters(name = "reverseLayout={0}")
// fun initParameters(): Array<Any> = arrayOf(false, true)
// }
@get:Rule
val rule = createComposeRule()
private var itemSizePx: Int = 50
private var itemSizeDp: Dp = Dp.Infinity
private var gridWidthPx: Int = itemSizePx * 2
private var gridWidthDp: Dp = Dp.Infinity
@Before
fun before() {
with(rule.density) {
itemSizeDp = itemSizePx.toDp()
gridWidthDp = gridWidthPx.toDp()
}
}
@Test
fun visibleItemsAreCorrect() {
lateinit var state: LazyGridState
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
// reverseLayout = reverseLayout,
modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
cells = GridCells.Fixed(2)
) {
items((0..11).toList()) {
Box(Modifier.size(itemSizeDp))
}
}
}
rule.runOnIdle {
state.layoutInfo.assertVisibleItems(count = 8, itemsPerRow = 2)
}
}
@Test
fun visibleItemsAreCorrectAfterScroll() {
lateinit var state: LazyGridState
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
// reverseLayout = reverseLayout,
modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
cells = GridCells.Fixed(2)
) {
items((0..11).toList()) {
Box(Modifier.size(itemSizeDp))
}
}
}
rule.runOnIdle {
runBlocking {
state.scrollToItem(2, 10)
}
state.layoutInfo
.assertVisibleItems(count = 8, startIndex = 2, startOffset = -10, itemsPerRow = 2)
}
}
@Test
fun visibleItemsAreCorrectWithSpacing() {
lateinit var state: LazyGridState
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
// reverseLayout = reverseLayout,
verticalArrangement = Arrangement.spacedBy(itemSizeDp),
modifier = Modifier.width(itemSizeDp).height(itemSizeDp * 3.5f),
cells = GridCells.Fixed(1)
) {
items((0..11).toList()) {
Box(Modifier.size(itemSizeDp))
}
}
}
rule.runOnIdle {
state.layoutInfo.assertVisibleItems(count = 2, spacing = itemSizePx, itemsPerRow = 1)
}
}
@Composable
fun ObservingFun(state: LazyGridState, currentInfo: StableRef<LazyGridLayoutInfo?>) {
currentInfo.value = state.layoutInfo
}
@Test
fun visibleItemsAreObservableWhenWeScroll() {
lateinit var state: LazyGridState
val currentInfo = StableRef<LazyGridLayoutInfo?>(null)
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
// reverseLayout = reverseLayout,
modifier = Modifier.size(itemSizeDp * 2f, itemSizeDp * 3.5f),
cells = GridCells.Fixed(2)
) {
items((0..11).toList()) {
Box(Modifier.size(itemSizeDp))
}
}
ObservingFun(state, currentInfo)
}
rule.runOnIdle {
// empty it here and scrolling should invoke observingFun again
currentInfo.value = null
runBlocking {
state.scrollToItem(2, 0)
}
}
rule.runOnIdle {
assertThat(currentInfo.value).isNotNull()
currentInfo.value!!
.assertVisibleItems(count = 8, startIndex = 2, itemsPerRow = 2)
}
}
@Test
fun visibleItemsAreObservableWhenResize() {
lateinit var state: LazyGridState
var size by mutableStateOf(itemSizeDp * 2)
var currentInfo: LazyGridLayoutInfo? = null
@Composable
fun observingFun() {
currentInfo = state.layoutInfo
}
rule.setContent {
LazyVerticalGrid(
modifier = Modifier.width(itemSizeDp),
// reverseLayout = reverseLayout,
state = rememberLazyGridState().also { state = it },
cells = GridCells.Fixed(1)
) {
item {
Box(Modifier.size(size))
}
}
observingFun()
}
rule.runOnIdle {
assertThat(currentInfo).isNotNull()
currentInfo!!.assertVisibleItems(
count = 1,
expectedSize = IntSize(itemSizePx, itemSizePx * 2),
itemsPerRow = 1
)
currentInfo = null
size = itemSizeDp
}
rule.runOnIdle {
assertThat(currentInfo).isNotNull()
currentInfo!!.assertVisibleItems(
count = 1,
expectedSize = IntSize(itemSizePx, itemSizePx),
itemsPerRow = 1
)
}
}
@Test
fun totalCountIsCorrect() {
var count by mutableStateOf(10)
lateinit var state: LazyGridState
rule.setContent {
LazyVerticalGrid(
// reverseLayout = reverseLayout,
state = rememberLazyGridState().also { state = it },
cells = GridCells.Fixed(2)
) {
items((0 until count).toList()) {
Box(Modifier.size(10.dp))
}
}
}
rule.runOnIdle {
assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
count = 20
}
rule.runOnIdle {
assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20)
}
}
@Test
fun viewportOffsetsAndSizeAreCorrect() {
val sizePx = 45
val sizeDp = with(rule.density) { sizePx.toDp() }
lateinit var state: LazyGridState
rule.setContent {
LazyVerticalGrid(
modifier = Modifier.height(sizeDp).width(sizeDp * 2),
// reverseLayout = reverseLayout,
state = rememberLazyGridState().also { state = it },
cells = GridCells.Fixed(2)
) {
items((0..7).toList()) {
Box(Modifier.size(sizeDp))
}
}
}
rule.runOnIdle {
assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(0)
assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx)
assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
}
}
@Test
fun viewportOffsetsAndSizeAreCorrectWithContentPadding() {
val sizePx = 45
val startPaddingPx = 10
val endPaddingPx = 15
val sizeDp = with(rule.density) { sizePx.toDp() }
val topPaddingDp = with(rule.density) {
if (!false/*reverseLayout*/) startPaddingPx.toDp() else endPaddingPx.toDp()
}
val bottomPaddingDp = with(rule.density) {
if (!false/*reverseLayout*/) endPaddingPx.toDp() else startPaddingPx.toDp()
}
lateinit var state: LazyGridState
rule.setContent {
LazyVerticalGrid(
modifier = Modifier.height(sizeDp).width(sizeDp * 2),
contentPadding = PaddingValues(
top = topPaddingDp,
bottom = bottomPaddingDp,
start = 2.dp,
end = 2.dp
),
// reverseLayout = reverseLayout,
state = rememberLazyGridState().also { state = it },
cells = GridCells.Fixed(2)
) {
items((0..7).toList()) {
Box(Modifier.size(sizeDp))
}
}
}
rule.runOnIdle {
assertThat(state.layoutInfo.viewportStartOffset).isEqualTo(-startPaddingPx)
assertThat(state.layoutInfo.viewportEndOffset).isEqualTo(sizePx - startPaddingPx)
// TODO(b/211753558) currently failing because we need to port aosp/1903956 to grids
// assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
}
}
@Test
fun emptyItemsInVisibleItemsInfo() {
lateinit var state: LazyGridState
rule.setContent {
LazyVerticalGrid(
cells = GridCells.Fixed(2),
state = rememberLazyGridState().also { state = it }
) {
item { Box(Modifier) }
item { }
}
}
rule.runOnIdle {
assertThat(state.layoutInfo.visibleItemsInfo.size).isEqualTo(2)
assertThat(state.layoutInfo.visibleItemsInfo.first().index).isEqualTo(0)
assertThat(state.layoutInfo.visibleItemsInfo.last().index).isEqualTo(1)
}
}
@Test
fun reverseLayoutIsCorrect() {
lateinit var state: LazyGridState
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
// reverseLayout = reverseLayout,
modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
cells = GridCells.Fixed(2)
) {
items((0..11).toList()) {
Box(Modifier.size(itemSizeDp))
}
}
}
rule.runOnIdle {
assertThat(state.layoutInfo.reverseLayout).isEqualTo(false/*reverseLayout*/)
}
}
@Test
fun orientationIsCorrect() {
lateinit var state: LazyGridState
rule.setContent {
LazyVerticalGrid(
state = rememberLazyGridState().also { state = it },
// reverseLayout = reverseLayout,
modifier = Modifier.width(gridWidthDp).height(itemSizeDp * 3.5f),
cells = GridCells.Fixed(2)
) {
items((0..11).toList()) {
Box(Modifier.size(itemSizeDp))
}
}
}
rule.runOnIdle {
assertThat(state.layoutInfo.orientation).isEqualTo(Orientation.Vertical)
}
}
fun LazyGridLayoutInfo.assertVisibleItems(
count: Int,
itemsPerRow: Int,
startIndex: Int = 0,
startOffset: Int = 0,
expectedSize: IntSize = IntSize(itemSizePx, itemSizePx),
spacing: Int = 0
) {
assertThat(visibleItemsInfo.size).isEqualTo(count)
if (count == 0) return
assertThat(startIndex % itemsPerRow).isEqualTo(0)
assertThat(visibleItemsInfo.size % itemsPerRow).isEqualTo(0)
var currentIndex = startIndex
var currentOffset = startOffset
var currentRow = startIndex / itemsPerRow
var currentColumn = 0
visibleItemsInfo.forEach {
assertThat(it.index).isEqualTo(currentIndex)
assertWithMessage("Offset of item $currentIndex").that(it.offset.y)
.isEqualTo(currentOffset)
assertThat(it.size).isEqualTo(expectedSize)
assertThat(it.row).isEqualTo(currentRow)
assertThat(it.column).isEqualTo(currentColumn)
currentIndex++
currentColumn++
if (currentColumn == itemsPerRow) {
currentColumn = 0
++currentRow
currentOffset += it.size.height + spacing
}
}
}
}
@Stable
class StableRef<T>(var value: T)