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 }
}

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

结语

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

相关推荐
alexhilton5 小时前
面向开发者的系统设计:像建筑师一样思考
android·kotlin·android jetpack
Lei活在当下1 天前
【业务场景架构实战】4. 支付状态分层流转的设计和实现
架构·android jetpack·响应式设计
天花板之恋2 天前
Compose之图片加载显示
android jetpack
消失的旧时光-19432 天前
Kotlinx.serialization 使用讲解
android·数据结构·android jetpack
Tans53 天前
Androidx Fragment 源码阅读笔记(下)
android jetpack·源码阅读
Lei活在当下4 天前
【业务场景架构实战】2. 对聚合支付 SDK 的封装
架构·android jetpack
Tans56 天前
Androidx Fragment 源码阅读笔记(上)
android jetpack·源码阅读
alexhilton7 天前
runBlocking实践:哪里该使用,哪里不该用
android·kotlin·android jetpack
Tans59 天前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读
ljt272496066111 天前
Compose笔记(四十九)--SwipeToDismiss
android·笔记·android jetpack