Android Compose实现联系人列表流程
作者:会编程的猩猩 发布时间:2023-09-25 00:50:04
标签:Android,Compose,联系人
准备数据
data class ContactEntity(
val letter: Char,
val name: String,
val color: Color
)
/**
* 获取联系人数据
*/
fun getContactData(): MutableList<ContactEntity> {
val contactList = mutableListOf<ContactEntity>()
(65..90).forEach { letter ->
// val random = (5..20).random()
val random = 5
repeat(random) { index ->
contactList.add(
ContactEntity(
letter = letter.toChar(), name = "联系人 $index",
color = Color(
red = (0..255).random(),
blue = (0..255).random(),
green = (0..255).random()
)
)
)
}
}
return contactList
}
/**
* 获取首字母列表
*/
fun getCharList(): MutableList<Char> {
val charList = mutableListOf<Char>()
(65..90).forEach { letter ->
charList.add(letter.toChar())
}
return charList
}
思路
整体是由Box布局包裹, 左侧是LazyColumn 右侧放置自定义布局
左侧LazyColumn用state来观察滑动的一些参数,来控制右侧字母的位置
右侧使用canvas绘制字母 在触控的时候使用scrollToItem准确的定位到左侧应该滑动的位置,感觉有些不准确,不知道是计算的问题还是bug,在谷歌上看到类似的issue提交。
中间显示滑动到的字母,也可以使用贝塞尔曲线来绘制类似于水滴的样式,懒得去搞了。
利用derivedStateOf来跟踪变化的remember数据,这样可以减少性能的损耗,但是感觉有个地方没处理好,就是触摸的时候需要改变,滑动的时候也需要改变,而derivedStateOf是val类型的,不能直接赋值,所以又设置了一个remember变量。有可以优化的地方请指出。
数据和样式都可以自定义,非常方便
代码实现
@OptIn(ExperimentalTextApi::class)
@Composable
fun ContactPage(navCtrl: NavHostController, title: String) {
//联系人数据
val contactList = getContactData()
//字母数据
val charList = getCharList()
val offsetY = with(LocalDensity.current) {
20.dp.toPx()
}
val offsetX = with(LocalDensity.current) {
25.dp.toPx()
}
val coroutineScope = rememberCoroutineScope()
val textMeasure = rememberTextMeasurer()
val state = rememberLazyListState()
//触摸的位置
var touchOffset by remember() {
mutableStateOf(Offset.Zero)
}
//触摸到的上一次的字母下标
var touchIndexLast by remember {
mutableStateOf(-1)
}
//触摸的字母的下标
val touchIndex = remember(touchOffset.y) {
derivedStateOf {
if (touchOffset.y > 0f) {
//通过偏移的倍数计算滑动到哪个字母的位置了
val y = (touchOffset.y / offsetY).roundToInt()
if (y in 0..25) {
touchIndexLast = y
y
} else {
touchIndexLast
}
} else touchIndexLast
}
}
//触摸到的字符的index
val touchLetterIndex = remember {
derivedStateOf {
if (state.isScrollInProgress && state.layoutInfo.visibleItemsInfo.isNotEmpty()) {
val key = state.layoutInfo.visibleItemsInfo[0].key
if (key is Int && key < contactList.size) {
val letter = contactList[key].letter
val findIndex = charList.indexOfFirst {
it == letter
}
findIndex
} else {
touchIndex.value
}
} else {
touchIndex.value
}
}
}
//上一次选择的letter
var lastLetter by remember {
mutableStateOf("")
}
//滑动到的letter
val scrollLetter = remember {
derivedStateOf {
if (touchIndex.value > 0) {
val letter = (65 + touchIndex.value).toChar()
coroutineScope.launch {
//找到相应的item
val index = getIndex(contactList, letter)
state.scrollToItem(index, scrollOffset = 20)
}
val str = letter.toString()
lastLetter = str
str
} else {
lastLetter
}
}
}
CommonToolbar(navCtrl, title) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxWidth(), state = state, content = {
contactList.forEachIndexed { index, contactEntity ->
item(key = index) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(50.dp)
.clip(CircleShape)
.background(
color = contactEntity.color,
), contentAlignment = Alignment.Center
) {
Text(
text = "${contactEntity.letter}",
fontSize = 16.sp,
color = Color.White,
fontWeight = FontWeight.SemiBold,
)
}
Text(
text = contactEntity.name,
modifier = Modifier
.padding(20.dp)
.weight(1f)
.padding(horizontal = 10.dp, vertical = 16.dp)
)
}
Divider()
}
}
}
item {
Text(
text = "共${contactList.size}联系人",
fontSize = 12.sp,
color = Color(0xFF333333),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
textAlign = TextAlign.Center
)
}
})
//绘制右侧的26个字母
Canvas(modifier = Modifier
.padding(top = 30.dp)
.width(50.dp)
.fillMaxHeight()
.align(Alignment.TopEnd)
.pointerInput(Unit) {
coroutineScope {
while (true) {
// down事件
val downPointerInputChange = awaitPointerEventScope {
awaitFirstDown()
}
// 如果位置不在手指按下的位置,先动画的形式过度到手指按下的位置
if (touchOffset.x != downPointerInputChange.position.x && touchOffset.y != downPointerInputChange.position.y) {
launch {
touchOffset = downPointerInputChange.position
}
}
// touch Move事件
// 滑动的时候,box随着手指的移动去移动
awaitPointerEventScope {
drag(downPointerInputChange.id, onDrag = {
touchOffset = it.position
})
}
// 在手指弹起的时候,才通过动画的形式,回到原点的位置
val dragUpOrCancelPointerInputChange = awaitPointerEventScope {
awaitDragOrCancellation(downPointerInputChange.id)
}
// 等于空,说明已经抬起
if (dragUpOrCancelPointerInputChange == null) {
launch {
touchOffset = Offset.Zero
}
}
}
}
}, onDraw = {
charList.forEachIndexed { index, char ->
drawText(
size = Size(width = offsetY, offsetY),
textMeasurer = textMeasure, text = "$char",
style = TextStyle(
fontWeight = if (touchLetterIndex.value == index) FontWeight.SemiBold else FontWeight.Medium,
color = if (touchLetterIndex.value == index) Color.Blue else Color(
0xFF333333
),
textAlign = TextAlign.Center,
fontSize = if (touchLetterIndex.value == index) 16.sp else 14.sp
),
topLeft = Offset(offsetX, offsetY * index),
)
}
})
//中间显示的大写字母
val textMeasurer1 = rememberTextMeasurer()
if (touchOffset.x != 0f && touchOffset.y != 0f && scrollLetter.value.isNotEmpty()) {
val annotatedString = AnnotatedString(
scrollLetter.value, spanStyle = SpanStyle(
fontWeight = FontWeight.Medium,
color = Color(0xFF333333),
fontSize = 20.sp,
)
)
val textLayoutResult = textMeasurer1.measure(text = annotatedString)
val textSize = textLayoutResult.size
Canvas(modifier = Modifier
.align(Alignment.Center)
.requiredSize(width = 50.dp, height = 50.dp), onDraw = {
//底部颜色
drawRoundRect(
color = Color(0xA403A9F4), cornerRadius = CornerRadius(10f, 10f)
)
//绘制字母
drawText(
textMeasurer = textMeasurer1,
text = annotatedString,
topLeft = Offset(
(size.width - textSize.width) / 2f,
(size.height - textSize.height) / 2f
)
)
})
}
}
}
}
private fun getIndex(list: MutableList<ContactEntity>, letter: Char): Int {
val findIndex = list.indexOfFirst { contactEntity ->
contactEntity.letter == letter
}
return findIndex
}
代码位置点我
来源:https://blog.csdn.net/u010436867/article/details/129021056
0
投稿
猜你喜欢
- 单元测试是程序员对代码的自测,一般公司都会严格要求单元测试,这是对自己代码的负责,也是对代码的敬畏。一般单元测试都是测试Service层,下
- 本文实例讲述了java使用dom4j生成与解析xml文档的方法。分享给大家供大家参考,具体如下:xml是一种新的数据格式,主要用于数据交换。
- 1. matlab的lp2lp函数的作用去归一化 H(s) 的分母2. matlab的lp2lp函数的使用方法[z, p, k]=butta
- 一、layui.use1、LayUI的官方使用文档:https://www.layui.com/doc/2、layui的内置模块不是默认就加
- 前言: Java 8已经公布有一段时间了,种种迹象表明Java 8是一个有重大改变的发行版。在Java Code Geeks上已经有很多介绍
- 效果如下:BitmapShader 的简单介绍关于 Shader是什么,Shader的种类有哪几种以及如何使用不属于本文范畴,对这方面不是很
- 在工作中,我们经常使用线程池,但是你真的了解线程池的原理吗?同时,线程池工作原理和底层实现原理也是面试经常问的考题,所以,今天我们一起聊聊线
- 一、背景假如:黑客黑进了数据库,或者离职人员导出了数据,那么就可能导致这些敏感数据的泄漏。因此我们就需要找到一种方法来解决这个问题。二、解决
- JDK12的五大重要新特性Java12在March 19, 2019发布了。在2017年发布Java 9之后,Java平台发布节奏已从每3年
- 一、之前旧的写法class Singleton{ private Singleton() {} &nb
- 自动注入和@Autowire@Autowire不属于自动注入!注入方式(重要)在Spring官网上(文档),定义了在Spring中的注入方式
- substring(参数)是java截取字符串的一个方法。它有两种传参的方式:第一种:public String substring(int
- ProxyFactory是创建代理类的工厂接口,其中的setProperties方法用来对工厂进行属性设置,但是mybatis内置的两个实现
- 前言Spring Boot项目一般都是内嵌tomcat或者jetty服务器运行,很少用war包部署到外部的服务容器,即使放到linux中,一
- java控制台输出图书馆管理系统(只用java代码不用数据库和GUI,java入门的新手秒懂)在个项目中,我只用数组保存数据,和只用for循
- 什么是EurekaNetflix Eureka 是一款由 Netflix 开源的基于 REST 服务的注册中心,用于提供服务发现功能。Spr
- 需求是需要在TextView前端加入一个标签展示。最终效果图如下:根据效果图,很容易就能想到使用SpannableStringBuilder
- Android 无障碍的全局悬浮窗可以在屏幕上添加 UI 供用户进行快捷操作,可以展示在所有应用程序之上长期展示。另一方面,在一些自动化场景
- 本文实例讲述了Java编程调用微信接口实现图文信息等推送功能。分享给大家供大家参考,具体如下:Java调用微信接口工具类,包含素材上传、获取
- 一般情况下,我们大部分人都是在XML文件直接写布局,可是有些时候需要用代码动态添加布局,比如我昨天做一个viewpager的页数