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

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

结语

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

相关推荐
是六一啊i12 小时前
Compose 在Row、Column上使用focusRestorer修饰符失效原因
android jetpack
用户060905255222 天前
Compose 主题 MaterialTheme
android jetpack
用户060905255222 天前
Compose 简介和基础使用
android jetpack
用户060905255222 天前
Compose 重组优化
android jetpack
行墨2 天前
Jetpack Compose 深入浅出(一)——预览 @Preview
android jetpack
alexhilton3 天前
突破速度障碍:非阻塞启动画面如何将Android 应用启动时间缩短90%
android·kotlin·android jetpack
Pika4 天前
深入浅出 Compose 测量机制
android·android jetpack·composer
fundroid6 天前
掌握 Compose 性能优化三步法
android·android jetpack
ljt272496066110 天前
Compose笔记(五十一)--rememberTextMeasurer
android·笔记·android jetpack