Android Jetpack compose 实现类似通讯录列表

前言

本篇文章是能够应用的最小API是24,如果要使用更小版本需要自行对某些逻辑进行修改。这篇文章是今天突然心血来潮写的。自己捣鼓了一些时间,完成的,可以说还是很粗糙的一个版本。具体实现了顶部的吸附式标签和右侧的字母点击索引。来先上成品再说其他。

实现步骤

  • 对于通讯录列表这个可以看成两个部分一部分是列表一部分是右侧的索引表。那么久可以确定要使用的控件了最外层Row,里面放一个LazyColumn和Column来实现。
kotlin 复制代码
Row{
LazyColumn{}
Column{}
}
  • 然后对列表进行设计,我们去看compose的开发文档中可以知道在列表中有一个粘性标题(注:改控件属于实验性的将来可能会被删除,因此使用的时候要介意),这边为了方便就先用了因此列表的部分的控件就可以写成
kotlin 复制代码
 val listState = rememberLazyListState()
LazyColumn(
            modifier = Modifier
                .fillMaxHeight()
                .weight(1f),
            state = listState,
        ) {
            data.forEach { initial, listData ->
                stickyHeader(contentType = initial) {
                //将头部UI回调出去让用户自定义
                    contentTitle(initial)
                }
                items(listData) {
                //将内容UI回调出去让用户自定义
                    contentBody(it)
                }
            }
        }

这边的 stickyHeader()加上contentType属性是为了滑动时能够判断索引的位置并且在字母项里面显示出来。

  • 然后就是字母列表的实现啦
kotlin 复制代码
 Column(
            modifier = Modifier
                .width(15.dp)
                .fillMaxHeight(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            for (list in charList) {
                Box(
                    modifier = Modifier
                        .size(15.dp)
                        .background(color)
                ) {
                    Text(
                        text = list.toString(),
                        color = Color.Black,
                        fontSize = 10.sp,
                        modifier = Modifier.align(
                            Alignment.Center
                        )
                    )
                }

            }
        }

到这步可以说通讯列表已经基本实现了,

  • 接下来就是将列表和字母索引进行连接啦,思路是列表现在展示的第一个名字的字母是啥,然后在将这个字母与索引列里面进行比对,相同的进行标注。 按照这个思路我们从listState.layoutInfo的源码可以知道在最后一次布局过程中会去计算的LazyListLayoutInfo的对象。例如,您可以使用它来计算当前可见的项目。 这样不就简单了嘛,写一个来观察当前第一个的项目的字母是啥(isSelectType ),然后在滑动的时候对其进行赋值。为什么要在其滑动的时候赋值而不是一进入就去赋值,这是因为刚创建的时候layoutInfo.visibleItemsInfo.first()这个是为空的,会报错的呢。代码如下
kotlin 复制代码
val layoutInfo by remember { derivedStateOf { listState.layoutInfo } }
var isSelectType by remember {
        mutableStateOf(35.toChar())
    }
if (listState.isScrollInProgress) {
        isSelectType = layoutInfo.visibleItemsInfo.first().contentType as Char
    }
  • 到这步后就要考虑点击索引的时候列表要滑动到相应的位置

    看上图,当我点击#的时候列表要移动到#的位置,又因为#位于第一个所以要滑动到0的位置,当我带你A的时候它要移动到A的位置,我们可以看出A此时的索引为4,那么A的索引计算就变成了#的大小3加1,然后在滑动到相应的位置,因此点击滑动的代码逻辑就可以这样写

kotlin 复制代码
if (char == 35.toChar()) {
                                coroutineScope.launch {
                                    listState.animateScrollToItem(0)
                                }
                            } else {
                                var index = 0
                                for (typeList in charList) {
                                    if (typeList == char) {
                                        coroutineScope.launch {
                                            listState.animateScrollToItem(index)
                                        }
                                        break
                                    } else {
                                        val size = data[typeList]?.size ?: 0
                                        index += size + 1
                                    }
                                }
                            }
  • 至此基本上的逻辑都完成了,现在将完整代码如下:
kotlin 复制代码
@RequiresApi(Build.VERSION_CODES.N)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun <T> AddressBookView(
    data: Map<Char, List<T>>,
    modifier: Modifier = Modifier,
    contentBody: @Composable (item: T) -> Unit,
    contentTitle: @Composable (item: Char) -> Unit,
) {
    val charList = getCharList()
    val listState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()
    val layoutInfo by remember { derivedStateOf { listState.layoutInfo } }
    var isSelectType by remember {
        mutableStateOf(35.toChar())
    }
    Row(modifier = modifier.fillMaxSize()) {
        LazyColumn(
            modifier = Modifier
                .fillMaxHeight()
                .weight(1f),
            state = listState,
        ) {
            data.forEach { initial, listData ->
                stickyHeader(contentType = initial) {
                    contentTitle(initial)
                }
                items(listData) {
                    contentBody(it)
                }
            }
        }
        Column(
            modifier = Modifier
                .width(15.dp)
                .fillMaxHeight(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            for (char in charList) {
                val color = if (isSelectType == char) Color.Green else Color.White
                Box(
                    modifier = Modifier
                        .size(15.dp)
                        .background(color)
                        .clickable {
                            if (char == 35.toChar()) {
                                coroutineScope.launch {
                                    listState.animateScrollToItem(0)
                                }
                            } else {
                                var index = 0
                                for (typeList in charList) {
                                    if (typeList == char) {
                                        coroutineScope.launch {
                                            listState.animateScrollToItem(index)
                                        }
                                        break
                                    } else {
                                        val size = data[typeList]?.size ?: 0
                                        index += size + 1
                                    }
                                }
                            }

                        }
                ) {
                    Text(
                        text = char.toString(),
                        color = Color.Black,
                        fontSize = 10.sp,
                        modifier = Modifier.align(
                            Alignment.Center
                        )
                    )
                }

            }
        }
    }

    if (listState.isScrollInProgress) {
        isSelectType = layoutInfo.visibleItemsInfo.first().contentType as Char
    }
}
//获取字母列表
private fun getCharList(): List<Char> {
    val charList = mutableListOf<Char>()
    val char = 35
    charList.add(char.toChar())
    (65..90).forEach { letter ->
        charList.add(letter.toChar())
    }
    return charList
}

具体应用

在应用的话直接上代码吧

kotlin 复制代码
@RequiresApi(Build.VERSION_CODES.N)
@Preview
@Composable
private fun ShowAddressBookView() {
    val data = getData()
    AddressBookView(
        data = data,
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.White)
            .padding(horizontal = 10.dp),
        contentBody = {

            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(30.dp)
            ) {
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .weight(1f)
                        .clickable {
                            Log.e("test", "ShowAddressBookView:${it.name} ")
                        },
                    horizontalArrangement = Arrangement.Center
                ) {
                    Text(
                        text = it.name, color = Color.Black, fontSize = 25.sp
                    )
                }
                Divider(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 17.5.dp),
                    color = Color.Black
                )
            }
        },
        contentTitle = {
            Text(
                text = it.toString(),
                color = Color.Black,
                fontSize = 15.sp,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(20.dp)
            )
        })
}
//自定义
data class AddressBookData(
    val nameChar: Char,
    val name: String,
)

//获取数据
private fun getData(): Map<Char, List<AddressBookData>> {
    val data = mutableListOf<AddressBookData>()
    for (i in 0..100) {
        val a = (65..91).random()
        val nameChar = if (a == 91) {
            val char = 35
            char.toChar()
        } else {
            a.toChar()
        }
        data.add(AddressBookData(nameChar = nameChar, name = "name:$i"))
    }
    return data.sortedBy { it.nameChar }.groupBy { it.nameChar }
}

数据的类型可以自己去自定义,要展示的样式也可以自定义,不过字母索引的选中状态需要自己去修改,只要传入的类型没错的话那么就没啥大问题。

结语

好了今天就写到这里啦,有问题的话欢迎大家写出来,这个还存在点击字母索引的时候滑动动画不够自然的问题,当然如果还有其他问题欢迎指出来,感谢各位看官的观看。

相关推荐
一航jason8 天前
Android Jetpack Compose 现有Java老项目集成使用compose开发
android·java·android jetpack
帅次9 天前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
IAM四十二11 天前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
Wgllss12 天前
那些大厂架构师是怎样封装网络请求的?
android·架构·android jetpack
x0241 个月前
Android Room(SQLite) too many SQL variables异常
sqlite·安卓·android jetpack·1024程序员节
alexhilton1 个月前
深入理解观察者模式
android·kotlin·android jetpack
Wgllss1 个月前
花式高阶:插件化之Dex文件的高阶用法,极少人知道的秘密
android·性能优化·android jetpack
上官阳阳1 个月前
使用Compose创造有趣的动画:使用Compose共享元素
android·android jetpack
沐言人生1 个月前
Android10 Framework—Init进程-15.属性变化控制Service
android·android studio·android jetpack
IAM四十二1 个月前
Android Jetpack Core
android·android studio·android jetpack