Android Compose——ScrollableTabRow和LazyColumn同步滑动

简介: Android Compose——ScrollableTabRow和LazyColumn同步滑动

效果

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

数据

下列通过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链接

相关文章
|
1月前
|
存储 缓存 Android开发
安卓Jetpack Compose+Kotlin, 使用ExoPlayer播放多个【远程url】音频,搭配Okhttp库进行下载和缓存,播放完随机播放下一首
这是一个Kotlin项目,使用Jetpack Compose和ExoPlayer框架开发Android应用,功能是播放远程URL音频列表。应用会检查本地缓存,如果文件存在且大小与远程文件一致则使用缓存,否则下载文件并播放。播放完成后或遇到异常,会随机播放下一首音频,并在播放前随机设置播放速度(0.9到1.2倍速)。代码包括ViewModel,负责音频管理和播放逻辑,以及UI层,包含播放和停止按钮。
132 0
|
5天前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
1月前
|
安全 JavaScript 前端开发
kotlin开发安卓app,JetPack Compose框架,给webview新增一个按钮,点击刷新网页
在Kotlin中开发Android应用,使用Jetpack Compose框架时,可以通过添加一个按钮到TopAppBar来实现WebView页面的刷新功能。按钮位于右上角,点击后调用`webViewState?.reload()`来刷新网页内容。以下是代码摘要:
|
1月前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
1月前
|
JavaScript 前端开发 Android开发
kotlin安卓在Jetpack Compose 框架下使用webview , 网页中的JavaScript代码如何与native交互
在Jetpack Compose中使用Kotlin创建Webview组件,设置JavaScript交互:`@Composable`函数`ComposableWebView`加载网页并启用JavaScript。通过`addJavascriptInterface`添加`WebAppInterface`类,允许JavaScript调用Android方法如播放音频。当页面加载完成时,执行`onWebViewReady`回调。
|
1月前
|
监控 Android开发 数据安全/隐私保护
安卓kotlin JetPack Compose 实现摄像头监控画面变化并录制视频
在这个示例中,开发者正在使用Kotlin和Jetpack Compose构建一个Android应用程序,该程序 能够通过手机后置主摄像头录制视频、检测画面差异、实时预览并将视频上传至FTP服务器的Android应用
|
15天前
|
Android开发
Android仿高德首页三段式滑动
Android仿高德首页三段式滑动
29 0
|
20天前
|
Android开发
Jetpack Compose: Hello Android
Jetpack Compose: Hello Android
12 0
|
27天前
|
编解码 Android开发
Android 解决TextView多行滑动与NestedScrollView嵌套滑动冲突的问题
Android 解决TextView多行滑动与NestedScrollView嵌套滑动冲突的问题
18 0
|
1月前
|
XML Android开发 UED