Android Compose 框架的列表与集合模块之滑动删除与拖拽深入分析(四十八) ...

打印 上一主题 下一主题

主题 1842|帖子 1842|积分 5536

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
Android Compose 框架的列表与集合模块之滑动删除与拖拽深入分析

一、引言

本人掘金号,接待点击关注:https://juejin.cn/user/4406498335701950

1.1 Android Compose 简介

在 Android 开辟范畴,界面的交互性和用户体验至关重要。传统的 Android 开辟方式在构建复杂界面和实现流畅交互时存在肯定的范围性。而 Android Compose 作为 Google 推出的声明式 UI 工具包,为开辟者带来了全新的开辟体验。它基于 Kotlin 语言,采用声明式编程范式,使得开辟者能够以简洁、高效的方式构建 UI,并且更轻易实现复杂的交互结果。
1.2 滑动删除与拖拽在列表中的重要性

在 Android 应用中,列表是一种常见的 UI 组件,用于展示大量的数据。滑动删除和拖拽功能可以极大地提升用户对列表数据的操作便捷性和交互体验。滑动删除允许用户通过简朴的滑动手势快速删除列表中的某一项数据,而拖拽功能则可以让用户重新分列列表项的次序。这些功能在许多应用场景中都非常实用,如待办事项列表、文件管理列表等。
1.3 本文的目标

本文将深入分析 Android Compose 框架的列表与集合模块中滑动删除与拖拽功能的实现原理和源码。通过详细的代码示例和源码分析,帮助开辟者明白怎样在 Android Compose 中实现这些功能,并且能够根据现实需求举行定制和扩展。
二、滑动删除功能实现

2.1 根本思绪

实现滑动删除功能的根本思绪是监听列表项的滑动手势,当滑动距离达到肯定阈值时,执行删除操作。在 Android Compose 中,可以通过 Modifier.pointerInput 来监听手势事件,结合 Animatable 实现滑动动画结果。
2.2 简朴示例代码

kotlin
  1. import androidx.compose.animation.core.Animatable
  2. import androidx.compose.animation.core.AnimationVector1D
  3. import androidx.compose.animation.core.tween
  4. import androidx.compose.foundation.gestures.detectHorizontalDragGestures
  5. import androidx.compose.foundation.layout.*
  6. import androidx.compose.material.*
  7. import androidx.compose.runtime.*
  8. import androidx.compose.ui.Modifier
  9. import androidx.compose.ui.graphics.Color
  10. import androidx.compose.ui.input.pointer.pointerInput
  11. import androidx.compose.ui.unit.Dp
  12. import androidx.compose.ui.unit.dp
  13. // 定义一个可组合函数,用于展示包含滑动删除功能的列表
  14. @Composable
  15. fun SwipeToDeleteList() {
  16.     // 使用 mutableStateOf 创建一个可变的列表,用于存储列表项数据
  17.     var items by remember { mutableStateOf((1..10).map { "Item $it" }.toList()) }
  18.     Column(
  19.         modifier = Modifier
  20.            .fillMaxSize()
  21.            .padding(16.dp)
  22.     ) {
  23.         // 遍历列表中的每一项
  24.         items.forEachIndexed { index, item ->
  25.             // 为每个列表项创建一个可动画化的偏移量对象,初始值为 0.dp
  26.             val offsetX = remember { Animatable(0f) }
  27.             // 创建一个 Card 组件作为列表项的容器
  28.             Card(
  29.                 modifier = Modifier
  30.                    .fillMaxWidth()
  31.                    .padding(vertical = 4.dp)
  32.                    .pointerInput(Unit) {
  33.                         // 监听水平滑动手势
  34.                         detectHorizontalDragGestures(
  35.                             onDrag = { change, dragAmount ->
  36.                                 // 更新偏移量,限制偏移量在 -200.dp 到 0.dp 之间
  37.                                 val newOffset = offsetX.value + dragAmount.x
  38.                                 offsetX.snapTo(newOffset.coerceIn(-200f, 0f))
  39.                                 // 标记手势事件已被消费
  40.                                 change.consume()
  41.                             },
  42.                             onDragEnd = {
  43.                                 // 当滑动结束时,判断偏移量是否超过 -100.dp
  44.                                 if (offsetX.value < -100f) {
  45.                                     // 如果超过 -100.dp,执行删除操作
  46.                                     items = items.toMutableList().apply { removeAt(index) }
  47.                                 } else {
  48.                                     // 否则,将偏移量动画回 0.dp
  49.                                     launch {
  50.                                         offsetX.animateTo(
  51.                                             targetValue = 0f,
  52.                                             animationSpec = tween(durationMillis = 200)
  53.                                         )
  54.                                     }
  55.                                 }
  56.                             }
  57.                         )
  58.                     }
  59.                    .offset {
  60.                         // 将偏移量应用到列表项上
  61.                         IntOffset(offsetX.value.toInt(), 0)
  62.                     }
  63.             ) {
  64.                 // 在 Card 中显示列表项的文本
  65.                 Text(
  66.                     text = item,
  67.                     modifier = Modifier.padding(16.dp)
  68.                 )
  69.             }
  70.         }
  71.     }
  72. }
复制代码
2.3 代码表明



  • 状态管理:使用 mutableStateOf 创建一个可变的列表 items,用于存储列表项的数据。当用户执行删除操作时,更新这个列表。
  • 手势监听:使用 Modifier.pointerInput 和 detectHorizontalDragGestures 监听列表项的程度滑动手势。在 onDrag 回调中,更新 offsetX 的值,并限定其范围在 -200.dp 到 0.dp 之间。在 onDragEnd 回调中,根据偏移量的值决定是执行删除操作还是将列表项动画回原来的位置。
  • 动画结果:使用 Animatable 实现列表项的滑动动画。在 onDragEnd 回调中,假如偏移量没有高出 -100.dp,使用 animateTo 方法将 offsetX 动画回 0.dp。
2.4 源码分析

Animatable 源码分析

Animatable 是 Android Compose 中用于实现动画的核心类之一。它的主要作用是管理动画的状态和执行动画。以下是 Animatable 的简化源码分析:
kotlin
  1. class Animatable<T, V : AnimationVector>(
  2.     initialValue: T,
  3.     val typeConverter: TwoWayConverter<T, V>
  4. ) {
  5.     // 当前动画的值
  6.     private var _value: T = initialValue
  7.     // 动画的状态
  8.     private var animationState: AnimationState<T, V> = AnimationState(initialValue, typeConverter)
  9.     // 获取当前动画的值
  10.     val value: T
  11.         get() = _value
  12.     // 立即将动画的值设置为指定的值
  13.     fun snapTo(targetValue: T) {
  14.         _value = targetValue
  15.         animationState = AnimationState(targetValue, typeConverter)
  16.     }
  17.     // 启动动画,将动画的值从当前值过渡到目标值
  18.     suspend fun animateTo(
  19.         targetValue: T,
  20.         animationSpec: AnimationSpec<T> = spring()
  21.     ) {
  22.         animationState.animateTo(
  23.             targetValue = targetValue,
  24.             animationSpec = animationSpec,
  25.             onUpdate = { value ->
  26.                 _value = value
  27.             }
  28.         )
  29.     }
  30. }
复制代码


  • Animatable 类接受一个初始值和一个类型转换器 typeConverter。
  • snapTo 方法用于立即将动画的值设置为指定的值。
  • animateTo 方法用于启动动画,将动画的值从当前值过渡到目标值。在动画过程中,会不断调用 onUpdate 回调更新当前值。
detectHorizontalDragGestures 源码分析

detectHorizontalDragGestures 是用于监听程度滑动手势的函数。以下是其简化源码分析:
kotlin
  1. suspend fun PointerInputScope.detectHorizontalDragGestures(
  2.     onDragStart: (Offset) -> Unit = {},
  3.     onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,
  4.     onDragEnd: () -> Unit = {},
  5.     onDragCancel: () -> Unit = {}
  6. ) {
  7.     awaitPointerEventScope {
  8.         while (true) {
  9.             // 等待第一个按下事件
  10.             val down = awaitFirstDown(requireUnconsumed = false)
  11.             onDragStart(down.position)
  12.             var overSlop = Offset.Zero
  13.             do {
  14.                 // 等待下一个指针事件
  15.                 val event = awaitPointerEvent()
  16.                 val dragChange = event.changes.find { it.id == down.id }!!
  17.                 if (dragChange.pressed) {
  18.                     // 计算拖动的偏移量
  19.                     val dragDelta = dragChange.positionChange()
  20.                     overSlop += dragDelta
  21.                     if (abs(overSlop.x) > ViewConfiguration.get(this@PointerInputScope).scaledTouchSlop) {
  22.                         // 当拖动距离超过阈值时,调用 onDrag 回调
  23.                         dragChange.consume()
  24.                         onDrag(dragChange, Offset(dragDelta.x, 0f))
  25.                     }
  26.                 }
  27.             } while (dragChange.pressed)
  28.             if (dragChange.isConsumed) {
  29.                 // 当拖动结束且事件已被消费时,调用 onDragEnd 回调
  30.                 onDragEnd()
  31.             } else {
  32.                 // 当拖动取消时,调用 onDragCancel 回调
  33.                 onDragCancel()
  34.             }
  35.         }
  36.     }
  37. }
复制代码


  • detectHorizontalDragGestures 函数在一个协程中不断监听指针事件。
  • 当检测到按下事件时,调用 onDragStart 回调。
  • 在拖动过程中,盘算拖动的偏移量,当偏移量高出阈值时,调用 onDrag 回调。
  • 当拖动竣事且事件已被消耗时,调用 onDragEnd 回调;当拖动取消时,调用 onDragCancel 回调。
2.5 优化滑动删除功能

增加删除提示

可以在列表项滑动时显示删除提示,加强用户体验。以下是优化后的代码:
kotlin
  1. import androidx.compose.animation.core.Animatable
  2. import androidx.compose.animation.core.AnimationVector1D
  3. import androidx.compose.animation.core.tween
  4. import androidx.compose.foundation.background
  5. import androidx.compose.foundation.gestures.detectHorizontalDragGestures
  6. import androidx.compose.foundation.layout.*
  7. import androidx.compose.material.*
  8. import androidx.compose.runtime.*
  9. import androidx.compose.ui.Alignment
  10. import androidx.compose.ui.Modifier
  11. import androidx.compose.ui.graphics.Color
  12. import androidx.compose.ui.input.pointer.pointerInput
  13. import androidx.compose.ui.unit.Dp
  14. import androidx.compose.ui.unit.dp
  15. @Composable
  16. fun SwipeToDeleteListWithHint() {
  17.     var items by remember { mutableStateOf((1..10).map { "Item $it" }.toList()) }
  18.     Column(
  19.         modifier = Modifier
  20.            .fillMaxSize()
  21.            .padding(16.dp)
  22.     ) {
  23.         items.forEachIndexed { index, item ->
  24.             val offsetX = remember { Animatable(0f) }
  25.             Box(
  26.                 modifier = Modifier
  27.                    .fillMaxWidth()
  28.                    .padding(vertical = 4.dp)
  29.             ) {
  30.                 // 显示删除提示的背景
  31.                 Box(
  32.                     modifier = Modifier
  33.                        .fillMaxSize()
  34.                        .background(Color.Red)
  35.                        .padding(16.dp)
  36.                        .align(Alignment.CenterEnd)
  37.                 ) {
  38.                     Text(
  39.                         text = "Delete",
  40.                         color = Color.White
  41.                     )
  42.                 }
  43.                 Card(
  44.                     modifier = Modifier
  45.                        .fillMaxWidth()
  46.                        .pointerInput(Unit) {
  47.                             detectHorizontalDragGestures(
  48.                                 onDrag = { change, dragAmount ->
  49.                                     val newOffset = offsetX.value + dragAmount.x
  50.                                     offsetX.snapTo(newOffset.coerceIn(-200f, 0f))
  51.                                     change.consume()
  52.                                 },
  53.                                 onDragEnd = {
  54.                                     if (offsetX.value < -100f) {
  55.                                         items = items.toMutableList().apply { removeAt(index) }
  56.                                     } else {
  57.                                         launch {
  58.                                             offsetX.animateTo(
  59.                                                 targetValue = 0f,
  60.                                                 animationSpec = tween(durationMillis = 200)
  61.                                             )
  62.                                         }
  63.                                     }
  64.                                 }
  65.                             )
  66.                         }
  67.                        .offset {
  68.                             IntOffset(offsetX.value.toInt(), 0)
  69.                         }
  70.                 ) {
  71.                     Text(
  72.                         text = item,
  73.                         modifier = Modifier.padding(16.dp)
  74.                     )
  75.                 }
  76.             }
  77.         }
  78.     }
  79. }
复制代码
代码表明



  • 在 Box 组件中添加一个红色背景的 Box 作为删除提示,当列表项滑动时,删除提示会逐渐显示出来。
  • 其他部门的代码与之前的示例类似,只是在结构上举行了调整。
三、拖拽功能实现

3.1 根本思绪

实现拖拽功能的根本思绪是监听列表项的长按手势,当检测到长按时,开始拖拽操作。在拖拽过程中,更新列表项的位置,并在拖拽竣事时,更新列表项的次序。在 Android Compose 中,可以通过 Modifier.pointerInput 监听手势事件,结合 Layout 组件实现列表项的位置更新。
3.2 简朴示例代码

kotlin
  1. import androidx.compose.foundation.gestures.detectDragGestures
  2. import androidx.compose.foundation.gestures.detectLongPressGestures
  3. import androidx.compose.foundation.layout.*
  4. import androidx.compose.material.*
  5. import androidx.compose.runtime.*
  6. import androidx.compose.ui.Alignment
  7. import androidx.compose.ui.Modifier
  8. import androidx.compose.ui.graphics.graphicsLayer
  9. import androidx.compose.ui.input.pointer.pointerInput
  10. import androidx.compose.ui.unit.IntOffset
  11. import androidx.compose.ui.unit.dp
  12. import kotlin.math.roundToInt
  13. // 定义一个可组合函数,用于展示包含拖拽功能的列表
  14. @Composable
  15. fun DragAndDropList() {
  16.     // 使用 mutableStateOf 创建一个可变的列表,用于存储列表项数据
  17.     var items by remember { mutableStateOf((1..10).map { "Item $it" }.toList()) }
  18.     // 用于记录当前正在拖拽的列表项的索引,初始值为 -1 表示没有拖拽操作
  19.     var draggedIndex by remember { mutableStateOf(-1) }
  20.     // 用于记录拖拽过程中的偏移量,初始值为 IntOffset(0, 0)
  21.     var offset by remember { mutableStateOf(IntOffset(0, 0)) }
  22.     Column(
  23.         modifier = Modifier
  24.            .fillMaxSize()
  25.            .padding(16.dp)
  26.     ) {
  27.         // 遍历列表中的每一项
  28.         items.forEachIndexed { index, item ->
  29.             // 创建一个 Card 组件作为列表项的容器
  30.             Card(
  31.                 modifier = Modifier
  32.                    .fillMaxWidth()
  33.                    .padding(vertical = 4.dp)
  34.                    .pointerInput(Unit) {
  35.                         // 监听长按手势
  36.                         detectLongPressGestures(
  37.                             onLongPress = {
  38.                                 // 当检测到长按时,记录当前拖拽的列表项索引
  39.                                 draggedIndex = index
  40.                             },
  41.                             onDrag = { change, dragAmount ->
  42.                                 // 当正在拖拽时,更新偏移量
  43.                                 if (draggedIndex != -1) {
  44.                                     offset = IntOffset(
  45.                                         (offset.x + dragAmount.x).roundToInt(),
  46.                                         (offset.y + dragAmount.y).roundToInt()
  47.                                     )
  48.                                     // 标记手势事件已被消费
  49.                                     change.consume()
  50.                                 }
  51.                             },
  52.                             onDragEnd = {
  53.                                 if (draggedIndex != -1) {
  54.                                     // 当拖拽结束时,计算新的索引位置
  55.                                     val newIndex = (offset.y / 56.dp.toPx()).roundToInt() + draggedIndex
  56.                                     // 确保新的索引位置在有效范围内
  57.                                     val validNewIndex = newIndex.coerceIn(0, items.size - 1)
  58.                                     // 更新列表项的顺序
  59.                                     items = items.toMutableList().apply {
  60.                                         val draggedItem = removeAt(draggedIndex)
  61.                                         add(validNewIndex, draggedItem)
  62.                                     }
  63.                                     // 重置拖拽索引和偏移量
  64.                                     draggedIndex = -1
  65.                                     offset = IntOffset(0, 0)
  66.                                 }
  67.                             }
  68.                         )
  69.                     }
  70.                    .graphicsLayer {
  71.                         // 如果当前列表项正在被拖拽,应用偏移量
  72.                         if (index == draggedIndex) {
  73.                             translationX = offset.x.toFloat()
  74.                             translationY = offset.y.toFloat()
  75.                         }
  76.                     }
  77.             ) {
  78.                 // 在 Card 中显示列表项的文本
  79.                 Text(
  80.                     text = item,
  81.                     modifier = Modifier.padding(16.dp)
  82.                 )
  83.             }
  84.         }
  85.     }
  86. }
复制代码
3.3 代码表明



  • 状态管理:使用 mutableStateOf 创建三个可变状态:items 用于存储列表项的数据,draggedIndex 用于记录当前正在拖拽的列表项的索引,offset 用于记录拖拽过程中的偏移量。
  • 手势监听:使用 Modifier.pointerInput 和 detectLongPressGestures 监听列表项的长按和拖拽手势。在 onLongPress 回调中,记录当前拖拽的列表项索引;在 onDrag 回调中,更新偏移量;在 onDragEnd 回调中,盘算新的索引位置,并更新列表项的次序。
  • 位置更新:使用 graphicsLayer 组件应用偏移量,实现列表项的位置更新。
3.4 源码分析

detectLongPressGestures 源码分析

detectLongPressGestures 是用于监听长按和拖拽手势的函数。以下是其简化源码分析:
kotlin
  1. suspend fun PointerInputScope.detectLongPressGestures(
  2.     onLongPress: (Offset) -> Unit = {},
  3.     onDragStart: (Offset) -> Unit = {},
  4.     onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,
  5.     onDragEnd: () -> Unit = {},
  6.     onDragCancel: () -> Unit = {}
  7. ) {
  8.     awaitPointerEventScope {
  9.         while (true) {
  10.             // 等待第一个按下事件
  11.             val down = awaitFirstDown(requireUnconsumed = false)
  12.             var longPressDetected = false
  13.             // 启动一个协程,在长按时间后检测是否触发长按事件
  14.             val longPressJob = launch {
  15.                 delay(ViewConfiguration.get(this@PointerInputScope).longPressTimeoutMillis)
  16.                 if (down.pressed) {
  17.                     longPressDetected = true
  18.                     onLongPress(down.position)
  19.                 }
  20.             }
  21.             var overSlop = Offset.Zero
  22.             do {
  23.                 // 等待下一个指针事件
  24.                 val event = awaitPointerEvent()
  25.                 val dragChange = event.changes.find { it.id == down.id }!!
  26.                 if (dragChange.pressed) {
  27.                     // 计算拖动的偏移量
  28.                     val dragDelta = dragChange.positionChange()
  29.                     overSlop += dragDelta
  30.                     if (abs(overSlop.getDistance()) > ViewConfiguration.get(this@PointerInputScope).scaledTouchSlop) {
  31.                         if (longPressDetected) {
  32.                             // 当长按事件已触发且拖动距离超过阈值时,调用 onDragStart 回调
  33.                             longPressJob.cancel()
  34.                             onDragStart(down.position)
  35.                             while (dragChange.pressed) {
  36.                                 // 持续监听拖动事件,调用 onDrag 回调
  37.                                 val nextEvent = awaitPointerEvent()
  38.                                 val nextDragChange = nextEvent.changes.find { it.id == down.id }!!
  39.                                 if (nextDragChange.pressed) {
  40.                                     val nextDragDelta = nextDragChange.positionChange()
  41.                                     onDrag(nextDragChange, nextDragDelta)
  42.                                     nextDragChange.consume()
  43.                                 }
  44.                             }
  45.                             // 当拖动结束时,调用 onDragEnd 回调
  46.                             onDragEnd()
  47.                         } else {
  48.                             // 当长按事件未触发且拖动距离超过阈值时,取消长按协程
  49.                             longPressJob.cancel()
  50.                         }
  51.                         break
  52.                     }
  53.                 }
  54.             } while (dragChange.pressed)
  55.             if (!longPressDetected) {
  56.                 // 当长按事件未触发时,取消长按协程
  57.                 longPressJob.cancel()
  58.             }
  59.         }
  60.     }
  61. }
复制代码


  • detectLongPressGestures 函数在一个协程中不断监听指针事件。
  • 当检测到按下事件时,启动一个协程,在长按时间后检测是否触发长按事件。
  • 在拖动过程中,盘算拖动的偏移量,当偏移量高出阈值时,根据长按事件是否触发,执行相应的操作。
  • 当拖动竣事时,调用 onDragEnd 回调。
graphicsLayer 源码分析

graphicsLayer 是用于应用图形变动的修饰符。以下是其简化源码分析:
kotlin
  1. fun Modifier.graphicsLayer(
  2.     alpha: Float = 1f,
  3.     scaleX: Float = 1f,
  4.     scaleY: Float = 1f,
  5.     translationX: Float = 0f,
  6.     translationY: Float = 0f,
  7.     rotationX: Float = 0f,
  8.     rotationY: Float = 0f,
  9.     rotationZ: Float = 0f,
  10.     shadowElevation: Float = 0f,
  11.     shape: Shape = RectangleShape,
  12.     clip: Boolean = false,
  13.     transformOrigin: TransformOrigin = TransformOrigin.Center
  14. ): Modifier = composed {
  15.     val layer = remember { GraphicsLayerScope() }
  16.     layer.alpha = alpha
  17.     layer.scaleX = scaleX
  18.     layer.scaleY = scaleY
  19.     layer.translationX = translationX
  20.     layer.translationY = translationY
  21.     layer.rotationX = rotationX
  22.     layer.rotationY = rotationY
  23.     layer.rotationZ = rotationZ
  24.     layer.shadowElevation = shadowElevation
  25.     layer.shape = shape
  26.     layer.clip = clip
  27.     layer.transformOrigin = transformOrigin
  28.     this.then(
  29.         LayoutModifier { measurable, constraints ->
  30.             val placeable = measurable.measure(constraints)
  31.             layout(placeable.width, placeable.height) {
  32.                 placeable.placeRelative(
  33.                     x = layer.translationX.roundToInt(),
  34.                     y = layer.translationY.roundToInt()
  35.                 )
  36.             }
  37.         }
  38.     )
  39. }
复制代码


  • graphicsLayer 修饰符接受多个参数,用于设置图形变动的属性,如透明度、缩放、平移、旋转等。
  • 在 LayoutModifier 中,根据设置的属性对组件举行结构和变动。
3.5 优化拖拽功能

增加拖拽动画

可以在拖拽过程中增加动画结果,提升用户体验。以下是优化后的代码:
kotlin
  1. import androidx.compose.animation.core.Animatable
  2. import androidx.compose.animation.core.AnimationVector2D
  3. import androidx.compose.animation.core.tween
  4. import androidx.compose.foundation.gestures.detectDragGestures
  5. import androidx.compose.foundation.gestures.detectLongPressGestures
  6. import androidx.compose.foundation.layout.*
  7. import androidx.compose.material.*
  8. import androidx.compose.runtime.*
  9. import androidx.compose.ui.Alignment
  10. import androidx.compose.ui.Modifier
  11. import androidx.compose.ui.graphics.graphicsLayer
  12. import androidx.compose.ui.input.pointer.pointerInput
  13. import androidx.compose.ui.unit.IntOffset
  14. import androidx.compose.ui.unit.dp
  15. import kotlin.math.roundToInt
  16. @Composable
  17. fun DragAndDropListWithAnimation() {
  18.     var items by remember { mutableStateOf((1..10).map { "Item $it" }.toList()) }
  19.     var draggedIndex by remember { mutableStateOf(-1) }
  20.     val offsetX = remember { Animatable(0f) }
  21.     val offsetY = remember { Animatable(0f) }
  22.     Column(
  23.         modifier = Modifier
  24.            .fillMaxSize()
  25.            .padding(16.dp)
  26.     ) {
  27.         items.forEachIndexed { index, item ->
  28.             Card(
  29.                 modifier = Modifier
  30.                    .fillMaxWidth()
  31.                    .padding(vertical = 4.dp)
  32.                    .pointerInput(Unit) {
  33.                         detectLongPressGestures(
  34.                             onLongPress = {
  35.                                 draggedIndex = index
  36.                             },
  37.                             onDrag = { change, dragAmount ->
  38.                                 if (draggedIndex != -1) {
  39.                                     launch {
  40.                                         offsetX.snapTo(offsetX.value + dragAmount.x)
  41.                                         offsetY.snapTo(offsetY.value + dragAmount.y)
  42.                                     }
  43.                                     change.consume()
  44.                                 }
  45.                             },
  46.                             onDragEnd = {
  47.                                 if (draggedIndex != -1) {
  48.                                     val newIndex = (offsetY.value / 56.dp.toPx()).roundToInt() + draggedIndex
  49.                                     val validNewIndex = newIndex.coerceIn(0, items.size - 1)
  50.                                     items = items.toMutableList().apply {
  51.                                         val draggedItem = removeAt(draggedIndex)
  52.                                         add(validNewIndex, draggedItem)
  53.                                     }
  54.                                     launch {
  55.                                         offsetX.animateTo(
  56.                                             targetValue = 0f,
  57.                                             animationSpec = tween(durationMillis = 200)
  58.                                         )
  59.                                         offsetY.animateTo(
  60.                                             targetValue = 0f,
  61.                                             animationSpec = tween(durationMillis = 200)
  62.                                         )
  63.                                     }
  64.                                     draggedIndex = -1
  65.                                 }
  66.                             }
  67.                         )
  68.                     }
  69.                    .graphicsLayer {
  70.                         if (index == draggedIndex) {
  71.                             translationX = offsetX.value
  72.                             translationY = offsetY.value
  73.                         }
  74.                     }
  75.             ) {
  76.                 Text(
  77.                     text = item,
  78.                     modifier = Modifier.padding(16.dp)
  79.                 )
  80.             }
  81.         }
  82.     }
  83. }
复制代码
代码表明



  • 使用 Animatable 实现拖拽过程中的动画结果。在 onDrag 回调中,使用 snapTo 方法立即更新偏移量;在 onDragEnd 回调中,使用 animateTo 方法将偏移量动画回 0。
  • 其他部门的代码与之前的示例类似,只是在偏移量的处理上增加了动画结果。
四、滑动删除与拖拽的结合使用

4.1 实现思绪

在现实应用中,大概需要同时实现滑动删除和拖拽功能。实现思绪是在监听手势事件时,根据不同的手势操作执行相应的功能。例如,当检测到程度滑动且滑动距离高出肯定阈值时,执行滑动删除操作;当检测到长按并拖动时,执行拖拽操作。
4.2 示例代码

kotlin
  1. import androidx.compose.animation.core.Animatable
  2. import androidx.compose.animation.core.AnimationVector1D
  3. import androidx.compose.animation.core.AnimationVector2D
  4. import androidx.compose.animation.core.tween
  5. import androidx.compose.foundation.gestures.detectDragGestures
  6. import androidx.compose.foundation.gestures.detectHorizontalDragGestures
  7. import androidx.compose.foundation.gestures.detectLongPressGestures
  8. import androidx.compose.foundation.layout.*
  9. import androidx.compose.material.*
  10. import androidx.compose.runtime.*
  11. import androidx.compose.ui.Alignment
  12. import androidx.compose.ui.Modifier
  13. import androidx.compose.ui.graphics.Color
  14. import androidx.compose.ui.graphics.graphicsLayer
  15. import androidx.compose.ui.input.pointer.pointerInput
  16. import androidx.compose.ui.unit.IntOffset
  17. import androidx.compose.ui.unit.dp
  18. import kotlin.math.abs
  19. import kotlin.math.roundToInt
  20. @Composable
  21. fun SwipeAndDragList() {
  22.     var items by remember { mutableStateOf((1..10).map { "Item $it" }.toList()) }
  23.     var draggedIndex by remember { mutableStateOf(-1) }
  24.     val offsetX = remember { Animatable(0f) }
  25.     val offsetY = remember { Animatable(0f) }
  26.     Column(
  27.         modifier = Modifier
  28.            .fillMaxSize()
  29.            .padding(16.dp)
  30.     ) {
  31.         items.forEachIndexed { index, item ->
  32.             Box(
  33.                 modifier = Modifier
  34.                    .fillMaxWidth()
  35.                    .padding(vertical = 4.dp)
  36.             ) {
  37.                 Box(
  38.                     modifier = Modifier
  39.                        .fillMaxSize()
  40.                        .background(Color.Red)
  41.                        .padding(16.dp)
  42.                        .align(Alignment.CenterEnd)
  43.                 ) {
  44.                     Text(
  45.                         text = "Delete",
  46.                         color = Color.White
  47.                     )
  48.                 }
  49.                 Card(
  50.                     modifier = Modifier
  51.                        .fillMaxWidth()
  52.                        .pointerInput(Unit) {
  53.                             detectLongPressGestures(
  54.                                 onLongPress = {
  55.                                     draggedIndex = index
  56.                                 },
  57.                                 onDrag = { change, dragAmount ->
  58.                                     if (draggedIndex != -1) {
  59.                                         launch {
  60.                                             offsetX.snapTo(offsetX.value + dragAmount.x)
  61.                                             offsetY.snapTo(offsetY.value + dragAmount.y)
  62.                                         }
  63.                                         change.consume()
  64.                                     }
  65.                                 },
  66.                                 onDragEnd = {
  67.                                     if (draggedIndex != -1) {
  68.                                         if (abs(offsetX.value) > 100f) {
  69.                                             items = items.toMutableList().apply { removeAt(draggedIndex) }
  70.                                         } else {
  71.                                             val newIndex = (offsetY.value / 56.dp.toPx()).roundToInt() + draggedIndex
  72.                                             val validNewIndex = newIndex.coerceIn(0, items.size - 1)
  73.                                             items = items.toMutableList().apply {
  74.                                                 val draggedItem = removeAt(draggedIndex)
  75.                                                 add(validNewIndex, draggedItem)
  76.                                             }
  77.                                         }
  78.                                         launch {
  79.                                             offsetX.animateTo(
  80.                                                 targetValue = 0f,
  81.                                                 animationSpec = tween(durationMillis = 200)
  82.                                             )
  83.                                             offsetY.animateTo(
  84.                                                 targetValue = 0f,
  85.                                                 animationSpec = tween(durationMillis = 200)
  86.                                             )
  87.                                         }
  88.                                         draggedIndex = -1
  89.                                     }
  90.                                 }
  91.                             )
  92.                             detectHorizontalDragGestures(
  93.                                 onDrag = { change, dragAmount ->
  94.                                     if (draggedIndex == -1) {
  95.                                         val newOffset = offsetX.value + dragAmount.x
  96.                                         offsetX.snapTo(newOffset.coerceIn(-200f, 0f))
  97.                                         change.consume()
  98.                                     }
  99.                                 },
  100.                                 onDragEnd = {
  101.                                     if (draggedIndex == -1) {
  102.                                         if (offsetX.value < -100f) {
  103.                                             items = items.toMutableList().apply { removeAt(index) }
  104.                                         } else {
  105.                                             launch {
  106.                                                 offsetX.animateTo(
  107.                                                     targetValue = 0f,
  108.                                                     animationSpec = tween(durationMillis = 200)
  109.                                                 )
  110.                                             }
  111.                                         }
  112.                                     }
  113.                                 }
  114.                             )
  115.                         }
  116.                        .graphicsLayer {
  117.                             if (index == draggedIndex) {
  118.                                 translationX = offsetX.value
  119.                                 translationY = offsetY.value
  120.                             } else {
  121.                                 translationX = offsetX.value
  122.                             }
  123.                         }
  124.                 ) {
  125.                     Text(
  126.                         text = item,
  127.                         modifier = Modifier.padding(16.dp)
  128.                     )
  129.                 }
  130.             }
  131.         }
  132.     }
  133. }
复制代码
4.3 代码表明



  • 状态管理:使用 mutableStateOf 创建四个可变状态:items 用于存储列表项的数据,draggedIndex 用于记录当前正在拖拽的列表项的索引,offsetX 和 offsetY 用于记录拖拽过程中的偏移量。
  • 手势监听:使用 detectLongPressGestures 监听长按和拖拽手势,使用 detectHorizontalDragGestures 监听程度滑动手势。在不同的手势回调中,根据当前的状态执行相应的操作。
  • 功能实现:当检测到长按并拖动时,执行拖拽操作;当检测到程度滑动且滑动距离高出 -100f 时,执行滑动删除操作。
4.4 源码分析

在结合使用滑动删除和拖拽功能时,主要是对之前的手势监听和状态管理代码举行整合。在 detectLongPressGestures 和 detectHorizontalDragGestures 的回调中,根据 draggedIndex 的值判断当前是处于拖拽状态还是滑动删除状态,从而执行相应的操作。
五、性能优化

5.1 减少不必要的重绘

在实现滑动删除和拖拽功能时,要只管减少不必要的重绘。例如,在 Animatable 的 animateTo 方法中,可以使用 tween 动画规格并设置符合的连续时间,避免动画过于频仍导致的性能问题。同时,在手势监听回调中,使用 change.consume() 标记手势事件已被消耗,避免不必要的事件流传。
5.2 公道使用 remember

在使用 Animatable 和其他可变状态时,使用 remember 函数举行影象,避免在每次重组时重新创建对象。例如,在前面的示例中,使用 remember { Animatable(0f) } 创建 Animatable 对象,确保在组件的生命周期内只创建一次。
5.3 避免嵌套过多的组件

在结构设计时,要避免嵌套过多的组件,减少结构的复杂度。过多的组件嵌套会增加结构的测量和绘制时间,影响性能。例如,在实现滑动删除提示时,可以使用简朴的 Box 组件,而不是复杂的结构嵌套。
六、总结与展望

6.1 总结

本文深入分析了 Android Compose 框架的列表与集合模块中滑动删除与拖拽功能的实现原理和源码。通过详细的代码示例和源码分析,我们了解了怎样使用 Modifier.pointerInput 监听手势事件,使用 Animatable 实现动画结果,以及怎样结合 Layout 组件实现列表项的位置更新。同时,我们还学习了怎样优化滑动删除和拖拽功能,包括增加提示、动画结果和性能优化等方面。
6.2 展望

随着 Android Compose 的不断发展,滑动删除和拖拽功能大概会有更多的优化和扩展。例如,大概会提供更方便的 API 来实现这些功能,减少开辟者的代码量。同时,大概会支持更多的手势操作和动画结果,提升用户体验。别的,性能优化方面也大概会有进一步的改进,确保在不同装备上都能有流畅的交互结果。未来,开辟者可以更加轻松地在 Android Compose 中实现复杂的列表交互功能。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

篮之新喜

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表