准备数据
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
}
代码位置点我
到此这篇关于Android Compose实现联系人列表流程的文章就介绍到这了,更多相关Android Compose内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!