Android Compose——ScrollableTabRow和LazyColumn同步滑动

发布时间:2024年01月06日

效果

Demo简述:此Demo所实现的效果为当滑动List列表时,所对应的Tab相对应进行滑动切换,为了模拟复杂数据环境,通过不同类别的数据进行操作,通过sealed或者enum结合when 进行区分不同的类。具体效果如下视频所示。

数据

下列通过AnimalVegetableFruitVehicle四个不同的类用来模拟数据环境

data class Animal(val content:String)
data class Vegetable(val content:String)
data class Fruit(val content:String)
data class Vehicle(val content:String)


data class Type(
    val animals:List<Animal>,
    val vegetables:List<Vegetable>,
    val fruits:List<Fruit>,
    val vehicles:List<Vehicle>,
    val categories:List<String>
)

其中categories记录的是所有存在的类别列表标题,其余四个分别为对应的Tab的数据列表

val type = Type(
    animals = listOf(
       Animal("子鼠"),
        Animal("丑牛"),
        Animal("寅虎"),
        Animal("卯兔"),
        Animal("辰龙"),
        Animal("已蛇"),
        Animal("午马"),
        Animal("未羊"),
        Animal("申猴"),
        Animal("酉鸡"),
        Animal("戌狗"),
        Animal("亥猪")
    ),
    vegetables = listOf(
        Vegetable("白萝卜"),
        Vegetable("红萝卜"),
        Vegetable("黄萝卜"),
        Vegetable("绿萝卜"),
        Vegetable("紫萝卜"),
        Vegetable("橙萝卜"),
        Vegetable("黑萝卜"),
        Vegetable("粉红萝卜"),
        Vegetable("蓝萝卜"),
        Vegetable("青萝卜"),
        Vegetable("灰萝卜"),
        Vegetable("棕萝卜"),
        Vegetable("朱砂萝卜"),
        Vegetable("胭脂萝卜"),
    ),
    fruits = listOf(
        Fruit("苹果"),
        Fruit("香蕉"),
        Fruit("海棠"),
        Fruit("樱桃"),
        Fruit("枇杷"),
        Fruit("山楂"),
        Fruit("梨"),
        Fruit("李子"),
        Fruit("蓝莓"),
        Fruit("黑莓"),
        Fruit("西瓜"),
        Fruit("火龙果"),
        Fruit("榴莲"),
    ),
    vehicles = listOf(
        Vehicle("飞机"),
        Vehicle("火箭"),
        Vehicle("坦克"),
        Vehicle("共享单车"),
        Vehicle("汽车"),
        Vehicle("摩托车"),
        Vehicle("三轮车"),
        Vehicle("自行车"),
        Vehicle("电动车"),
        Vehicle("高铁"),
        Vehicle("马车"),
        Vehicle("驴车"),
        Vehicle("出租车"),
        Vehicle("地铁"),
    ),
    categories = listOf(
        "Animals",
        "Vegetables",
        "Fruits",
        "Vehicles",
    )
)

实现

lazyListTabSync是一个封装的组合函数,其中传入的参数是一个列表,上述我们建立了四个类别数据,则此参数传入应为mutableListOf(0,1,2,3),与下列所传入的参数效果一致

 val (selectedTabIndex, setSelectedTabIndex, listState) = lazyListTabSync(
                        type.categories.indices.toList()
                    )

除了上述用法之外,还可以像下面一样使用,但是所传入的tabsCount个数不能小于种类个数(mutableListOf(0,1,2,3)的个数)

val (selectedTabIndex, setSelectedTabIndex, listState) = tabSyncMediator(
        mutableListOf(0, 2, 4), 
        tabsCount = 3, 
        lazyListState = rememberLazyListState(), 
        smoothScroll = true, 
    )

Tab

Tab的实现主要在于当前Tab的位置和Tab的点击事件

@Composable
fun MyTabBar(
    type: Type,
    selectedTabIndex: Int,
    onTabClicked: (index: Int, type: String) -> Unit
) {
    ScrollableTabRow(
        selectedTabIndex = selectedTabIndex,
        edgePadding = 0.dp
    ) {
        type.categories.forEachIndexed { index, category ->
            Tab(
                selected = index == selectedTabIndex,
                onClick = { onTabClicked(index, category) },
                text = { Text(category) }
            )
        }
    }
}

List列表

LazyListState是用来同步滑动状态,下列通过enum对四个类别名称进行封装,然后通过when进行区分,最后在分别实现不同类别的数据效果

@Composable
fun MyLazyList(
    type: Type,
    listState: LazyListState = rememberLazyListState(),
) {
    LazyColumn(
        state = listState,
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        itemsIndexed(type.categories) { tabIndex, tab ->
            Column(modifier = Modifier.fillMaxWidth())
            {
                Text(text = tab, fontSize = 18.sp)
                Spacer(modifier = Modifier.height(10.dp))
                when (tab) {
                    Category.Vegetables.title -> {
                        type.vegetables.forEachIndexed { index, vegetable ->
                            Row(
                                horizontalArrangement = Arrangement.Center,
                                verticalAlignment = Alignment.CenterVertically,
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .height(40.dp)
                                    .background(Color.Gray)
                            ) {
                                Text(
                                    vegetable.content,
                                    textAlign = TextAlign.Center,
                                    color = Color.White
                                )
                            }
                            if (index < type.vegetables.size - 1) Spacer(
                                modifier = Modifier.height(
                                    10.dp
                                )
                            )
                        }
                    }

                    Category.Vehicles.title -> {
                        type.vehicles.forEachIndexed { index, vehicle ->
                            Row(
                                horizontalArrangement = Arrangement.Center,
                                verticalAlignment = Alignment.CenterVertically,
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .height(40.dp)
                                    .background(Color.Gray)
                            ) {
                                Text(
                                    vehicle.content,
                                    textAlign = TextAlign.Center,
                                    color = Color.White
                                )
                            }
                            if (index < type.vehicles.size - 1) Spacer(modifier = Modifier.height(10.dp))
                        }
                    }

                    Category.Animals.title -> {
                        type.animals.forEachIndexed { index, animal ->
                            Row(
                                horizontalArrangement = Arrangement.Center,
                                verticalAlignment = Alignment.CenterVertically,
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .height(40.dp)
                                    .background(Color.Gray)
                            ) {
                                Text(
                                    animal.content,
                                    textAlign = TextAlign.Center,
                                    color = Color.White
                                )
                            }
                            if (index < type.animals.size - 1) Spacer(modifier = Modifier.height(10.dp))
                        }
                    }

                    Category.Fruits.title -> {
                        type.fruits.forEachIndexed { index, fruit ->
                            Row(
                                horizontalArrangement = Arrangement.Center,
                                verticalAlignment = Alignment.CenterVertically,
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .height(40.dp)
                                    .background(Color.Gray)
                            ) {
                                Text(
                                    fruit.content,
                                    textAlign = TextAlign.Center,
                                    color = Color.White
                                )
                            }
                            if (index < type.fruits.size - 1) Spacer(modifier = Modifier.height(10.dp))
                        }
                    }
                }
                if (tabIndex < type.categories.size - 1) Spacer(modifier = Modifier.height(20.dp))
            }
        }
    }
}

如何同步实现?

我们需要一种方法来反映一个状态与另一个状态的状态,这意味着无论当前所选索引的值是多少,它都应该反映列表状态中的正确位置,反之亦然,无论列表状态的当前位置是什么,它都反映正确的索引。

监听列表滑动变化

通过LazyListState来了解列表的滑动状态,每次滑动都会都会重新计算列表滑动位置和Tab对应的滑动关系。通过查找第一个完全或部分可见的列表子项以及最后一个完全可见的列表子项来最终确定将所要选择的索引。如果发生变化,则返回变化结果,然后随即变化Tab状态

@Composable
fun lazyListTabSync(
    syncedIndices: List<Int>,
    lazyListState: LazyListState = rememberLazyListState(),
    tabsCount: Int? = null,
    smoothScroll: Boolean = true
): TabSyncState {
    require(syncedIndices.isNotEmpty()) {
        "You can't use the mediator without providing at least one index in the syncedIndices array"
    }

    if (tabsCount != null) {
        require(tabsCount <= syncedIndices.size) {
            "The tabs count is out of the bounds of the syncedIndices list provided. " +
                    "Either add an index to syncedIndices that corresponds to an item to your lazy list, " +
                    "or remove your excessive tab"
        }
    }

    var selectedTabIndex by remember { mutableStateOf(0) }

    LaunchedEffect(lazyListState) {
        snapshotFlow { lazyListState.layoutInfo }.collect {
            var itemPosition = lazyListState.findFirstFullyVisibleItemIndex()

            if (itemPosition == -1) {
                itemPosition = lazyListState.firstVisibleItemIndex
            }

            if (itemPosition == -1) {
                return@collect
            }

            if (lazyListState.findLastFullyVisibleItemIndex() == syncedIndices.last()) {
                itemPosition = syncedIndices.last()
            }

            if (syncedIndices.contains(itemPosition) && itemPosition != syncedIndices[selectedTabIndex]) {
                selectedTabIndex = syncedIndices.indexOf(itemPosition)
            }
        }
    }

    return TabSyncState(
        selectedTabIndex,
        lazyListState,
        rememberCoroutineScope(),
        syncedIndices,
        smoothScroll
    )
}

计算列表子项索引位置

由上述可知,我们一共建立了四个数据类别,则共有四个子项,每一个子项又是一个列表,此处我们计算的是单个数据类别位置,通过计算其可见子项的偏移量判断是否在对应范围内,从而返回对应的Tab子项下标

fun LazyListState.findFirstFullyVisibleItemIndex(): Int = findFullyVisibleItemIndex(reversed = false)

fun LazyListState.findLastFullyVisibleItemIndex(): Int = findFullyVisibleItemIndex(reversed = true)

fun LazyListState.findFullyVisibleItemIndex(reversed: Boolean): Int {
    layoutInfo.visibleItemsInfo.run { if (reversed) reversed() else this }.forEach { itemInfo ->
        val itemStartOffset = itemInfo.offset
        val itemEndOffset = itemInfo.offset + itemInfo.size
        val viewportStartOffset = layoutInfo.viewportStartOffset
        val viewportEndOffset = layoutInfo.viewportEndOffset
        if (itemStartOffset >= viewportStartOffset && itemEndOffset <= viewportEndOffset) {
            return itemInfo.index //返回当前滑动列表的子项所属Tab的下标
        }
    }
    return -1
}

Tab滑动

下面定义了三个解构声明语句,其中其中的 component1()component2() component3() 函数是在 Kotlin 中广泛使用的约定原则。下列实现的功能为通过启动一个协程作用域让Tab对应的列表滚动到相应的位置

@Stable
class TabSyncState(
    var selectedTabIndex: Int,
    var lazyListState: LazyListState,
    private var coroutineScope: CoroutineScope,
    private var syncedIndices: List<Int>,
    private var smoothScroll: Boolean,
) {
    operator fun component1(): Int = selectedTabIndex
    operator fun component2(): (Int) -> Unit = {
        require(it <= syncedIndices.size - 1) {
            "The selected tab's index is out of the bounds of the syncedIndices list provided. " +
                    "Either add an index to syncedIndices that corresponds to an item to your lazy list, " +
                    "or remove your excessive tab"
        }

        selectedTabIndex = it

        coroutineScope.launch {
            if (smoothScroll) {
                lazyListState.animateScrollToItem(syncedIndices[selectedTabIndex])
            } else {
                lazyListState.scrollToItem(syncedIndices[selectedTabIndex])
            }
        }
    }

    operator fun component3(): LazyListState = lazyListState
}

此Demo改编至一个国外博主,其原作者Github链接地址如下所示
原作者Github链接

文章来源:https://blog.csdn.net/News53231323/article/details/135428657
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。