Android Compose 框架的列表与集合模块之滑动删除与拖拽深入分析(四十八)
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
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp// 定义一个可组合函数,用于展示包含滑动删除功能的列表
@Composable
fun SwipeToDeleteList() {// 使用 mutableStateOf 创建一个可变的列表,用于存储列表项数据var items by remember { mutableStateOf((1..10).map { "Item $it" }.toList()) }Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {// 遍历列表中的每一项items.forEachIndexed { index, item ->// 为每个列表项创建一个可动画化的偏移量对象,初始值为 0.dpval offsetX = remember { Animatable(0f) }// 创建一个 Card 组件作为列表项的容器Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).pointerInput(Unit) {// 监听水平滑动手势detectHorizontalDragGestures(onDrag = { change, dragAmount ->// 更新偏移量,限制偏移量在 -200.dp 到 0.dp 之间val newOffset = offsetX.value + dragAmount.xoffsetX.snapTo(newOffset.coerceIn(-200f, 0f))// 标记手势事件已被消费change.consume()},onDragEnd = {// 当滑动结束时,判断偏移量是否超过 -100.dpif (offsetX.value < -100f) {// 如果超过 -100.dp,执行删除操作items = items.toMutableList().apply { removeAt(index) }} else {// 否则,将偏移量动画回 0.dplaunch {offsetX.animateTo(targetValue = 0f,animationSpec = tween(durationMillis = 200))}}})}.offset {// 将偏移量应用到列表项上IntOffset(offsetX.value.toInt(), 0)}) {// 在 Card 中显示列表项的文本Text(text = item,modifier = Modifier.padding(16.dp))}}}
}
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
class Animatable<T, V : AnimationVector>(initialValue: T,val typeConverter: TwoWayConverter<T, V>
) {// 当前动画的值private var _value: T = initialValue// 动画的状态private var animationState: AnimationState<T, V> = AnimationState(initialValue, typeConverter)// 获取当前动画的值val value: Tget() = _value// 立即将动画的值设置为指定的值fun snapTo(targetValue: T) {_value = targetValueanimationState = AnimationState(targetValue, typeConverter)}// 启动动画,将动画的值从当前值过渡到目标值suspend fun animateTo(targetValue: T,animationSpec: AnimationSpec<T> = spring()) {animationState.animateTo(targetValue = targetValue,animationSpec = animationSpec,onUpdate = { value ->_value = value})}
}
Animatable类接受一个初始值和一个类型转换器typeConverter。snapTo方法用于立即将动画的值设置为指定的值。animateTo方法用于启动动画,将动画的值从当前值过渡到目标值。在动画过程中,会不断调用onUpdate回调更新当前值。
detectHorizontalDragGestures 源码分析
detectHorizontalDragGestures 是用于监听水平滑动手势的函数。以下是其简化源码分析:
kotlin
suspend fun PointerInputScope.detectHorizontalDragGestures(onDragStart: (Offset) -> Unit = {},onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,onDragEnd: () -> Unit = {},onDragCancel: () -> Unit = {}
) {awaitPointerEventScope {while (true) {// 等待第一个按下事件val down = awaitFirstDown(requireUnconsumed = false)onDragStart(down.position)var overSlop = Offset.Zerodo {// 等待下一个指针事件val event = awaitPointerEvent()val dragChange = event.changes.find { it.id == down.id }!!if (dragChange.pressed) {// 计算拖动的偏移量val dragDelta = dragChange.positionChange()overSlop += dragDeltaif (abs(overSlop.x) > ViewConfiguration.get(this@PointerInputScope).scaledTouchSlop) {// 当拖动距离超过阈值时,调用 onDrag 回调dragChange.consume()onDrag(dragChange, Offset(dragDelta.x, 0f))}}} while (dragChange.pressed)if (dragChange.isConsumed) {// 当拖动结束且事件已被消费时,调用 onDragEnd 回调onDragEnd()} else {// 当拖动取消时,调用 onDragCancel 回调onDragCancel()}}}
}
detectHorizontalDragGestures函数在一个协程中不断监听指针事件。- 当检测到按下事件时,调用
onDragStart回调。 - 在拖动过程中,计算拖动的偏移量,当偏移量超过阈值时,调用
onDrag回调。 - 当拖动结束且事件已被消费时,调用
onDragEnd回调;当拖动取消时,调用onDragCancel回调。
2.5 优化滑动删除功能
增加删除提示
可以在列表项滑动时显示删除提示,增强用户体验。以下是优化后的代码:
kotlin
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp@Composable
fun SwipeToDeleteListWithHint() {var items by remember { mutableStateOf((1..10).map { "Item $it" }.toList()) }Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {items.forEachIndexed { index, item ->val offsetX = remember { Animatable(0f) }Box(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {// 显示删除提示的背景Box(modifier = Modifier.fillMaxSize().background(Color.Red).padding(16.dp).align(Alignment.CenterEnd)) {Text(text = "Delete",color = Color.White)}Card(modifier = Modifier.fillMaxWidth().pointerInput(Unit) {detectHorizontalDragGestures(onDrag = { change, dragAmount ->val newOffset = offsetX.value + dragAmount.xoffsetX.snapTo(newOffset.coerceIn(-200f, 0f))change.consume()},onDragEnd = {if (offsetX.value < -100f) {items = items.toMutableList().apply { removeAt(index) }} else {launch {offsetX.animateTo(targetValue = 0f,animationSpec = tween(durationMillis = 200))}}})}.offset {IntOffset(offsetX.value.toInt(), 0)}) {Text(text = item,modifier = Modifier.padding(16.dp))}}}}
}
代码解释
- 在
Box组件中添加一个红色背景的Box作为删除提示,当列表项滑动时,删除提示会逐渐显示出来。 - 其他部分的代码与之前的示例类似,只是在布局上进行了调整。
三、拖拽功能实现
3.1 基本思路
实现拖拽功能的基本思路是监听列表项的长按手势,当检测到长按时,开始拖拽操作。在拖拽过程中,更新列表项的位置,并在拖拽结束时,更新列表项的顺序。在 Android Compose 中,可以通过 Modifier.pointerInput 监听手势事件,结合 Layout 组件实现列表项的位置更新。
3.2 简单示例代码
kotlin
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectLongPressGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt// 定义一个可组合函数,用于展示包含拖拽功能的列表
@Composable
fun DragAndDropList() {// 使用 mutableStateOf 创建一个可变的列表,用于存储列表项数据var items by remember { mutableStateOf((1..10).map { "Item $it" }.toList()) }// 用于记录当前正在拖拽的列表项的索引,初始值为 -1 表示没有拖拽操作var draggedIndex by remember { mutableStateOf(-1) }// 用于记录拖拽过程中的偏移量,初始值为 IntOffset(0, 0)var offset by remember { mutableStateOf(IntOffset(0, 0)) }Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {// 遍历列表中的每一项items.forEachIndexed { index, item ->// 创建一个 Card 组件作为列表项的容器Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).pointerInput(Unit) {// 监听长按手势detectLongPressGestures(onLongPress = {// 当检测到长按时,记录当前拖拽的列表项索引draggedIndex = index},onDrag = { change, dragAmount ->// 当正在拖拽时,更新偏移量if (draggedIndex != -1) {offset = IntOffset((offset.x + dragAmount.x).roundToInt(),(offset.y + dragAmount.y).roundToInt())// 标记手势事件已被消费change.consume()}},onDragEnd = {if (draggedIndex != -1) {// 当拖拽结束时,计算新的索引位置val newIndex = (offset.y / 56.dp.toPx()).roundToInt() + draggedIndex// 确保新的索引位置在有效范围内val validNewIndex = newIndex.coerceIn(0, items.size - 1)// 更新列表项的顺序items = items.toMutableList().apply {val draggedItem = removeAt(draggedIndex)add(validNewIndex, draggedItem)}// 重置拖拽索引和偏移量draggedIndex = -1offset = IntOffset(0, 0)}})}.graphicsLayer {// 如果当前列表项正在被拖拽,应用偏移量if (index == draggedIndex) {translationX = offset.x.toFloat()translationY = offset.y.toFloat()}}) {// 在 Card 中显示列表项的文本Text(text = item,modifier = Modifier.padding(16.dp))}}}
}
3.3 代码解释
- 状态管理:使用
mutableStateOf创建三个可变状态:items用于存储列表项的数据,draggedIndex用于记录当前正在拖拽的列表项的索引,offset用于记录拖拽过程中的偏移量。 - 手势监听:使用
Modifier.pointerInput和detectLongPressGestures监听列表项的长按和拖拽手势。在onLongPress回调中,记录当前拖拽的列表项索引;在onDrag回调中,更新偏移量;在onDragEnd回调中,计算新的索引位置,并更新列表项的顺序。 - 位置更新:使用
graphicsLayer组件应用偏移量,实现列表项的位置更新。
3.4 源码分析
detectLongPressGestures 源码分析
detectLongPressGestures 是用于监听长按和拖拽手势的函数。以下是其简化源码分析:
kotlin
suspend fun PointerInputScope.detectLongPressGestures(onLongPress: (Offset) -> Unit = {},onDragStart: (Offset) -> Unit = {},onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,onDragEnd: () -> Unit = {},onDragCancel: () -> Unit = {}
) {awaitPointerEventScope {while (true) {// 等待第一个按下事件val down = awaitFirstDown(requireUnconsumed = false)var longPressDetected = false// 启动一个协程,在长按时间后检测是否触发长按事件val longPressJob = launch {delay(ViewConfiguration.get(this@PointerInputScope).longPressTimeoutMillis)if (down.pressed) {longPressDetected = trueonLongPress(down.position)}}var overSlop = Offset.Zerodo {// 等待下一个指针事件val event = awaitPointerEvent()val dragChange = event.changes.find { it.id == down.id }!!if (dragChange.pressed) {// 计算拖动的偏移量val dragDelta = dragChange.positionChange()overSlop += dragDeltaif (abs(overSlop.getDistance()) > ViewConfiguration.get(this@PointerInputScope).scaledTouchSlop) {if (longPressDetected) {// 当长按事件已触发且拖动距离超过阈值时,调用 onDragStart 回调longPressJob.cancel()onDragStart(down.position)while (dragChange.pressed) {// 持续监听拖动事件,调用 onDrag 回调val nextEvent = awaitPointerEvent()val nextDragChange = nextEvent.changes.find { it.id == down.id }!!if (nextDragChange.pressed) {val nextDragDelta = nextDragChange.positionChange()onDrag(nextDragChange, nextDragDelta)nextDragChange.consume()}}// 当拖动结束时,调用 onDragEnd 回调onDragEnd()} else {// 当长按事件未触发且拖动距离超过阈值时,取消长按协程longPressJob.cancel()}break}}} while (dragChange.pressed)if (!longPressDetected) {// 当长按事件未触发时,取消长按协程longPressJob.cancel()}}}
}
detectLongPressGestures函数在一个协程中不断监听指针事件。- 当检测到按下事件时,启动一个协程,在长按时间后检测是否触发长按事件。
- 在拖动过程中,计算拖动的偏移量,当偏移量超过阈值时,根据长按事件是否触发,执行相应的操作。
- 当拖动结束时,调用
onDragEnd回调。
graphicsLayer 源码分析
graphicsLayer 是用于应用图形变换的修饰符。以下是其简化源码分析:
kotlin
fun Modifier.graphicsLayer(alpha: Float = 1f,scaleX: Float = 1f,scaleY: Float = 1f,translationX: Float = 0f,translationY: Float = 0f,rotationX: Float = 0f,rotationY: Float = 0f,rotationZ: Float = 0f,shadowElevation: Float = 0f,shape: Shape = RectangleShape,clip: Boolean = false,transformOrigin: TransformOrigin = TransformOrigin.Center
): Modifier = composed {val layer = remember { GraphicsLayerScope() }layer.alpha = alphalayer.scaleX = scaleXlayer.scaleY = scaleYlayer.translationX = translationXlayer.translationY = translationYlayer.rotationX = rotationXlayer.rotationY = rotationYlayer.rotationZ = rotationZlayer.shadowElevation = shadowElevationlayer.shape = shapelayer.clip = cliplayer.transformOrigin = transformOriginthis.then(LayoutModifier { measurable, constraints ->val placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {placeable.placeRelative(x = layer.translationX.roundToInt(),y = layer.translationY.roundToInt())}})
}
graphicsLayer修饰符接受多个参数,用于设置图形变换的属性,如透明度、缩放、平移、旋转等。- 在
LayoutModifier中,根据设置的属性对组件进行布局和变换。
3.5 优化拖拽功能
增加拖拽动画
可以在拖拽过程中增加动画效果,提升用户体验。以下是优化后的代码:
kotlin
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectLongPressGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt@Composable
fun DragAndDropListWithAnimation() {var items by remember { mutableStateOf((1..10).map { "Item $it" }.toList()) }var draggedIndex by remember { mutableStateOf(-1) }val offsetX = remember { Animatable(0f) }val offsetY = remember { Animatable(0f) }Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {items.forEachIndexed { index, item ->Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).pointerInput(Unit) {detectLongPressGestures(onLongPress = {draggedIndex = index},onDrag = { change, dragAmount ->if (draggedIndex != -1) {launch {offsetX.snapTo(offsetX.value + dragAmount.x)offsetY.snapTo(offsetY.value + dragAmount.y)}change.consume()}},onDragEnd = {if (draggedIndex != -1) {val newIndex = (offsetY.value / 56.dp.toPx()).roundToInt() + draggedIndexval validNewIndex = newIndex.coerceIn(0, items.size - 1)items = items.toMutableList().apply {val draggedItem = removeAt(draggedIndex)add(validNewIndex, draggedItem)}launch {offsetX.animateTo(targetValue = 0f,animationSpec = tween(durationMillis = 200))offsetY.animateTo(targetValue = 0f,animationSpec = tween(durationMillis = 200))}draggedIndex = -1}})}.graphicsLayer {if (index == draggedIndex) {translationX = offsetX.valuetranslationY = offsetY.value}}) {Text(text = item,modifier = Modifier.padding(16.dp))}}}
}
代码解释
- 使用
Animatable实现拖拽过程中的动画效果。在onDrag回调中,使用snapTo方法立即更新偏移量;在onDragEnd回调中,使用animateTo方法将偏移量动画回 0。 - 其他部分的代码与之前的示例类似,只是在偏移量的处理上增加了动画效果。
四、滑动删除与拖拽的结合使用
4.1 实现思路
在实际应用中,可能需要同时实现滑动删除和拖拽功能。实现思路是在监听手势事件时,根据不同的手势操作执行相应的功能。例如,当检测到水平滑动且滑动距离超过一定阈值时,执行滑动删除操作;当检测到长按并拖动时,执行拖拽操作。
4.2 示例代码
kotlin
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectLongPressGestures
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.roundToInt@Composable
fun SwipeAndDragList() {var items by remember { mutableStateOf((1..10).map { "Item $it" }.toList()) }var draggedIndex by remember { mutableStateOf(-1) }val offsetX = remember { Animatable(0f) }val offsetY = remember { Animatable(0f) }Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {items.forEachIndexed { index, item ->Box(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {Box(modifier = Modifier.fillMaxSize().background(Color.Red).padding(16.dp).align(Alignment.CenterEnd)) {Text(text = "Delete",color = Color.White)}Card(modifier = Modifier.fillMaxWidth().pointerInput(Unit) {detectLongPressGestures(onLongPress = {draggedIndex = index},onDrag = { change, dragAmount ->if (draggedIndex != -1) {launch {offsetX.snapTo(offsetX.value + dragAmount.x)offsetY.snapTo(offsetY.value + dragAmount.y)}change.consume()}},onDragEnd = {if (draggedIndex != -1) {if (abs(offsetX.value) > 100f) {items = items.toMutableList().apply { removeAt(draggedIndex) }} else {val newIndex = (offsetY.value / 56.dp.toPx()).roundToInt() + draggedIndexval validNewIndex = newIndex.coerceIn(0, items.size - 1)items = items.toMutableList().apply {val draggedItem = removeAt(draggedIndex)add(validNewIndex, draggedItem)}}launch {offsetX.animateTo(targetValue = 0f,animationSpec = tween(durationMillis = 200))offsetY.animateTo(targetValue = 0f,animationSpec = tween(durationMillis = 200))}draggedIndex = -1}})detectHorizontalDragGestures(onDrag = { change, dragAmount ->if (draggedIndex == -1) {val newOffset = offsetX.value + dragAmount.xoffsetX.snapTo(newOffset.coerceIn(-200f, 0f))change.consume()}},onDragEnd = {if (draggedIndex == -1) {if (offsetX.value < -100f) {items = items.toMutableList().apply { removeAt(index) }} else {launch {offsetX.animateTo(targetValue = 0f,animationSpec = tween(durationMillis = 200))}}}})}.graphicsLayer {if (index == draggedIndex) {translationX = offsetX.valuetranslationY = offsetY.value} else {translationX = offsetX.value}}) {Text(text = item,modifier = Modifier.padding(16.dp))}}}}
}
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 中实现复杂的列表交互功能。
相关文章:
Android Compose 框架的列表与集合模块之滑动删除与拖拽深入分析(四十八)
Android Compose 框架的列表与集合模块之滑动删除与拖拽深入分析 一、引言 本人掘金号,欢迎点击关注:https://juejin.cn/user/4406498335701950 1.1 Android Compose 简介 在 Android 开发领域,界面的交互性和用户体验至关重要。传统的 A…...
一、LLM 大语言模型初窥:起源、概念与核心原理
一、初识大模型 1.1 人工智能演进与大模型兴起:从A11.0到A12.0的变迁 AI 1.0时代(2012-2022年) 感知智能的突破:以卷积神经网络(CNN)为核心,AI在图像识别、语音处理等感知任务中超越人类水平。例如&#…...
PyTorch核心函数详解:gather与where的实战指南
PyTorch中的torch.gather和torch.where是处理张量数据的关键工具,前者实现基于索引的灵活数据提取,后者完成条件筛选与动态生成。本文通过典型应用场景和代码演示,深入解析两者的工作原理及使用技巧,帮助开发者提升数据处理的灵活…...
《Operating System Concepts》阅读笔记:p636-p666
《Operating System Concepts》学习第 58 天,p636-p666 总结,总计 31 页。 一、技术总结 1.system and network threats (1)attack network traffic (2)denial of service (3)port scanning 2.symmetric/asymmetric encryption algorithm (1)symm…...
Go:接口
接口既约定 Go 语言中接口是抽象类型 ,与具体类型不同 ,不暴露数据布局、内部结构及基本操作 ,仅提供一些方法 ,拿到接口类型的值 ,只能知道它能做什么 ,即提供了哪些方法 。 func Fprintf(w io.Writer, …...
ESP32+Arduino入门(三):连接WIFI获取当前时间
ESP32内置了WIFI模块连接WIFI非常简单方便。 代码如下: #include <WiFi.h>const char* ssid "WIFI名称"; const char* password "WIFI密码";void setup() {Serial.begin(115200);WiFi.begin(ssid,password);while(WiFi.status() ! WL…...
FastAPI用户认证系统开发指南:从零构建安全API
前言 在现代Web应用开发中,用户认证系统是必不可少的功能。本文将带你使用FastAPI框架构建一个完整的用户认证系统,包含注册、登录、信息更新和删除等功能。我们将采用JWT(JSON Web Token)进行身份验证,并使用SQLite作…...
CSS高度坍塌?如何解决?
一、什么是高度坍塌? 高度坍塌(Collapsing Margins)是指当父元素没有设置边框(border)、内边距(padding)、内容(content)或清除浮动时,其子元素的 margin 会…...
【数据结构】之散列
一、定义与基本术语 (一)、定义 散列(Hash)是一种将键(key)通过散列函数映射到一个固定大小的数组中的技术,因为键值对的映射关系,散列表可以实现快速的插入、删除和查找操作。在这…...
空地机器人在复杂动态环境下,如何高效自主导航?
随着空陆两栖机器人(AGR)在应急救援和城市巡检等领域的应用范围不断扩大,其在复杂动态环境中实现自主导航的挑战也日益凸显。对此香港大学王俊铭基于阿木实验室P600无人机平台自主搭建了一整套空地两栖机器人,使用Prometheus开源框架完成算法的仿真验证与…...
python小记(十二):Python 中 Lambda函数详解
Python 中 Lambda函数详解 Lambda函数详解:从入门到实战一、什么是Lambda函数?二、Lambda的核心语法与特点1. 基础语法2. 与普通函数对比 三、Lambda的六大应用场景(附代码示例)1. 基本数学运算2. 列表排序与自定义规则3. 数据映射…...
第二十一讲 XGBoost 回归建模 + SHAP 可解释性分析(利用R语言内置数据集)
下面我将使用 R 语言内置的 mtcars 数据集,模拟一个完整的 XGBoost 回归建模 SHAP 可解释性分析 实战流程。我们将以预测汽车的油耗(mpg)为目标变量,构建 XGBoost 模型,并用 SHAP 来解释模型输出。 🚗 示例…...
数据分析实战案例:使用 Pandas 和 Matplotlib 进行居民用水
原创 IT小本本 IT小本本 2025年04月15日 18:31 北京 本文将使用 Matplotlib 及 Seaborn 进行数据可视化。探索如何清理数据、计算月度用水量并生成有价值的统计图表,以便更好地理解居民的用水情况。 数据处理与清理 读取 Excel 文件 首先,我们使用 pan…...
Asp.NET Core WebApi 创建带鉴权机制的Api
构建一个包含 JWT(JSON Web Token)鉴权的 Web API 是一种常见的做法,用于保护 API 端点并验证用户身份。以下是一个基于 ASP.NET Core 的完整示例,展示如何实现 JWT 鉴权。 1. 创建 ASP.NET Core Web API 项目 使用 .NET CLI 或 …...
hash.
Redis 自身就是键值对结构 Redis 自身的键值对结构就是通过 哈希 的方式来组织的 哈希类型中的映射关系通常称为 field-value,用于区分 Redis 整体的键值对(key-value), 注意这里的 value 是指 field 对应的值,不是键…...
记录鸿蒙应用上架应用未配置图标的前景图和后景图标准要求尺寸1024px*1024px和标准要求尺寸1024px*1024px
审核报错【①应用未配置图标的前景图和后景图,标准要求尺寸1024px*1024px且需下载HUAWEI DevEco Studio 5.0.5.315或以上版本进行图标再处理、②应用在展开状态下存在页面左边距过大的问题, 应用在展开状态下存在页面右边距过大的问题, 当前页面左边距: 504 px, 当前页面右边距…...
golang-常见的语法错误
https://juejin.cn/post/6923477800041054221 看这篇文章 Golang 基础面试高频题详细解析【第一版】来啦~ 大叔说码 for-range的坑 func main() { slice : []int{0, 1, 2, 3} m : make(map[int]*int) for key, val : range slice {m[key] &val }for k, v : …...
Google最新《Prompt Engineering》白皮书全解析
近期有幸拿到了Google最新发布的《Prompt Engineering》白皮书,这是一份由Lee Boonstra主笔,Michael Sherman、Yuan Cao、Erick Armbrust、Antonio Gulli等多位专家共同贡献的权威性指南,发布于2025年2月。今天我想和大家分享这份68页的宝贵资…...
如何快速部署基于Docker 的 OBDIAG 开发环境
很多开发者对 OceanBase的 SIG社区小组很有兴趣,但如何将OceanBase的各类工具部署在开发环境,对于不少开发者而言都是比较蛮烦的事情。例如,像OBDIAG,其在WINDOWS系统上配置较繁琐,需要单独搭建C开发环境。此外&#x…...
[LeetCode 1306] 跳跃游戏3(Ⅲ)
题面: LeetCode 1306 思路: 只要能跳到其中一个0即可,和跳跃游戏1/2完全不同了,记忆化暴搜即可。 时间复杂度: O ( n ) O(n) O(n) 空间复杂度: O ( n ) O(n) O(n) 代码: dfs vector<…...
spring-ai-alibaba使用Agent实现智能机票助手
示例目标是使用 Spring AI Alibaba 框架开发一个智能机票助手,它可以帮助消费者完成机票预定、问题解答、机票改签、取消等动作,具体要求为: 基于 AI 大模型与用户对话,理解用户自然语言表达的需求支持多轮连续对话,能…...
STM32平衡车开发实战教程:从零基础到项目精通
STM32平衡车开发实战教程:从零基础到项目精通 一、项目概述与基本原理 1.1 平衡车工作原理 平衡车是一种基于倒立摆原理的两轮自平衡小车,其核心控制原理类似于人类保持平衡的过程。当人站立不稳时,会通过腿部肌肉的快速调整来维持平衡。平…...
使用DeepSeek AI高效降低论文重复率
一、论文查重原理与DeepSeek降重机制 1.1 主流查重系统工作原理 文本比对算法:连续字符匹配(通常13-15字符)语义识别技术:检测同义替换和结构调整参考文献识别:区分合理引用与不当抄袭跨语言检测:中英文互译内容识别1.2 DeepSeek降重核心技术 深度语义理解:分析句子核心…...
linux多线(进)程编程——(7)消息队列
前言 现在修真界大家的沟通手段已经越来越丰富了,有了匿名管道,命名管道,共享内存等多种方式。但是随着深入使用人们逐渐发现了这些传音术的局限性。 匿名管道:只能在有血缘关系的修真者(进程)间使用&…...
WinForm真入门(14)——ListView控件详解
一、ListView 控件核心概念与功能 ListView 是 WinForm 中用于展示结构化数据的多功能列表控件,支持多列、多视图模式及复杂交互,常用于文件资源管理器、数据报表等场景。 核心特点: 支持 5种视图模式:Details&…...
Python + Playwright:规避常见的UI自动化测试反模式
Python + Playwright:规避常见的UI自动化测试反模式 前言反模式一:整体式页面对象(POM)反模式二:具有逻辑的页面对象 - POM 的“越界”行为反模式三:基于 UI 的测试设置 - 缓慢且脆弱的“舞台搭建”反模式四:功能测试过载 - “试图覆盖一切”的测试反模式之间的关联与核…...
从服务器多线程批量下载文件到本地
1、客户端安装 aria2 下载地址:aria2 解压文件,然后将文件目录添加到系统环境变量Path中,然后打开cmd,输入:aria2c 文件地址,就可以下载文件了 2、服务端配置nginx文件服务器 server {listen 8080…...
循环神经网络 - 深层循环神经网络
如果将深度定义为网络中信息传递路径长度的话,循环神经网络可以看作既“深”又“浅”的网络。 一方面来说,如果我们把循环网络按时间展开,长时间间隔的状态之间的路径很长,循环网络可以看作一个非常深的网络。 从另一方面来 说&…...
linux运维篇-Ubuntu(debian)系操作系统创建源仓库
适用范围 适用于Ubuntu(Debian)及其衍生版本的linux系统 例如,国产化操作系统kylin-desktop-v10 简介 先来看下我们需要创建出来的仓库目录结构 Deb_conf_test apt源的主目录 conf 配置文件存放目录 conf目录下存放两个配置文件&…...
深度学习之微积分
2.4.1 导数和微分 2.4.2 偏导数 