Jetpack Compose : 超简单实现侧滑删除(威力加强版)

前言

上一篇 Jetpack Compose : 超简单实现侧滑删除 - 掘金 (juejin.cn) 很多人喜欢并且有同学想要威力加强版,今天它来了。

按照惯例效果图先行:

思路

威力加强版新增双侧滑动、多段展开和展开动画以便满足大家的日常需求。

虽然增加很多功能但思路不变还是通过Box堆叠并加上拖动手势实现,伪代码如下:

Kotlin 复制代码
Box {
	Box()
	Box(modifier = Modifier
            .offset {
                IntOffset(
                    x = state.offset.roundToInt(),
                    y = 0,
                )
            }
	)
}

AnchoredDraggable

AnchoredDraggable 是一个基础 API,用于构建处于锚定状态的可拖动组件。

使用 DraggableAnchors 构建器方法定义锚点。然后,将它们传递给 AnchoredDraggableState的构造函数:

Kotlin 复制代码
// 定义锚点左侧展开,左侧全展开,居中, 右侧展开,右侧全展开
enum class DragAnchors { Start, StartFill, Center, End, EndFill }

val anchors = DraggableAnchors {
        Start at 100.dp.toPx() //这边直接使用数值.dp,方便大家直观理解
		StartFill at 200.dp.toPx()
        Center at 0f
        End at -100.dp.toPx()
		EndFill at -200.dp.toPx()
    }
val state = remember {
        AnchoredDraggableState(
            initialValue = DragAnchors.Center, //初始值,默认居中状态
            animationSpec = TweenSpec(durationMillis = 350), //动画
            anchors = anchors,
            positionalThreshold = { distance -> distance * 0.5f }, //位置阈值 50% 时进行锚点切换
            velocityThreshold = { with(density) { 100.dp.toPx() } }, //速度阈值,这个不是固定值,只是我喜欢写100
        )
    }

完整代码

Kotlin 复制代码
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SwipeBox(
    modifier: Modifier = Modifier,
    actionWidth: Dp, //锚点宽度
    startAction: List<@Composable BoxScope.() -> Unit>, //左侧展开锚点
    startFillAction: (@Composable BoxScope.() -> Unit)? = null, //左侧全展开锚点
    endAction: List<@Composable BoxScope.() -> Unit>, //右侧展开锚点
    endFillAction: (@Composable BoxScope.() -> Unit)? = null, //右侧全展开锚点
    content: @Composable BoxScope.() -> Unit
) {
    val density = LocalDensity.current
    val actionWidthPx = with(density) {
        actionWidth.toPx()
    }
    val startWidth = actionWidthPx * startAction.size //左侧锚点宽度
    val startActionSize = startAction.size + 1 //左侧锚点总数 = startAction + startFillAction
    val endWidth = actionWidthPx * endAction.size  //右侧锚点宽度
    val endActionSize = endAction.size + 1 //右侧锚点总数 =  endAction + endFillAction
    var contentWidth by remember { mutableFloatStateOf(0f) } //内容组件宽度
    var contentHeight by remember { mutableFloatStateOf(0f) }
    val state = remember(startWidth, endWidth, contentWidth) {
        AnchoredDraggableState(
            initialValue = DragAnchors.Center,
            animationSpec = TweenSpec(durationMillis = 350),
            anchors = DraggableAnchors {
                DragAnchors.Start at (if (startFillAction != null) actionWidthPx else 0f) + startWidth //左侧全展开锚点宽度 + 左侧展开锚点宽度
                DragAnchors.StartFill at (if (startFillAction != null) contentWidth else 0f) + startWidth // 内容组件宽度 + 左侧展开锚点宽度
                DragAnchors.Center at 0f
                DragAnchors.End at (if (endFillAction != null) -actionWidthPx else 0f) - endWidth //右侧全展开锚点宽度 + 右侧展开锚点宽度
                DragAnchors.EndFill at (if (endFillAction != null) -contentWidth else 0f) - endWidth // 内容组件宽度 + 右侧展开锚点宽度
            },
            positionalThreshold = { distance -> distance * 0.5f },
            velocityThreshold = { with(density) { 100.dp.toPx() } },
        )
    }

    Box(
        modifier = modifier
            .anchoredDraggable(
                state = state,
                orientation = Orientation.Horizontal,
            )
            .clipToBounds()
    ) {
        startAction.forEachIndexed { index, action ->
            Box(
                modifier = Modifier
                    .align(Alignment.CenterStart)
                    .width(actionWidth)
                    .height(with(density) {
                        contentHeight.toDp()
                    })
                    .offset {
                        IntOffset(
                            x = if (state.offset <= actionWidthPx * startActionSize) {
                                (-actionWidthPx + state.offset / startActionSize * (startActionSize - index)).roundToInt()
                            } else {
                                (-actionWidthPx * (index + 1) + state.offset).roundToInt()
                            },
                            y = 0,
                        )
                    }
            ) {
                action()
            }
        }
        startFillAction?.let {
            Box(
                modifier = Modifier
                    .align(Alignment.CenterStart)
                    .height(with(density) {
                        contentHeight.toDp()
                    })
                    .offset {
                        IntOffset(
                            x = if (state.offset <= actionWidthPx * startActionSize) {
                                (-contentWidth + state.offset / startActionSize).roundToInt()
                            } else {
                                (-contentWidth - startWidth + state.offset).roundToInt()
                            },
                            y = 0,
                        )
                    }
            ) {
                it()
            }
        }
        endAction.forEachIndexed { index, action ->
            Box(
                modifier = Modifier
                    .align(Alignment.CenterEnd)
                    .width(actionWidth)
                    .height(with(density) {
                        contentHeight.toDp()
                    })
                    .offset {
                        IntOffset(
                            x = if (state.offset >= -(actionWidthPx * endActionSize)) {
                                (actionWidthPx + state.offset / endActionSize * (endActionSize - index)).roundToInt()
                            } else {
                                (actionWidthPx * (index + 1) + state.offset).roundToInt()
                            },
                            y = 0,
                        )
                    }
            ) {
                action()
            }
        }
        endFillAction?.let {
            Box(
                modifier = Modifier
                    .align(Alignment.CenterEnd)
                    .height(with(density) {
                        contentHeight.toDp()
                    })
                    .offset {
                        IntOffset(
                            x = if (state.offset >= -(actionWidthPx * endActionSize)) {
                                (contentWidth + state.offset / endActionSize).roundToInt()
                            } else {
                                (contentWidth + endWidth + state.offset).roundToInt()
                            },
                            y = 0,
                        )
                    }
            ) {
                it()
            }
        }
        Box(
            modifier = Modifier
                .onSizeChanged {
                    contentWidth = it.width.toFloat()
                    contentHeight = it.height.toFloat()
                }
                .offset {
                    IntOffset(
                        x = state.offset.roundToInt(),
                        y = 0,
                    )
                }
        ) {
            content()
        }
    }
}

enum class DragAnchors { Start, StartFill, Center, End, EndFill }

如何使用

Kotlin 复制代码
@Composable
fun SwipeBoxScreen() {
    val context = LocalContext.current
    SwipeBox(
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight(),
        actionWidth = 70.dp,
        startAction = listOf {
            Box(
                modifier = Modifier
                    .background(colorResource(R.color.green))
                    .fillMaxSize()
                    .clickable {
                        Toast.makeText(context, "置顶", Toast.LENGTH_SHORT).show()
                    }
            ) {
                Text(
                    text = "置顶",
                    modifier = Modifier.align(Alignment.Center),
                    style = TextStyle.Default.copy(
                        color = colorResource(R.color.white),
                        fontSize = 12.sp
                    )
                )
            }
        },
        startFillAction = {
            Box(
                modifier = Modifier
                    .background(colorResource(R.color.pink))
                    .fillMaxSize()
            ) {
                Box(
                    modifier = Modifier
                        .align(Alignment.CenterEnd)
                        .width(70.dp)
                        .fillMaxHeight()
                        .clickable {
                            Toast.makeText(context, "取消置顶", Toast.LENGTH_SHORT).show()
                        }
                ) {
                    Text(
                        text = "取消置顶",
                        modifier = Modifier.align(Alignment.Center),
                        style = TextStyle.Default.copy(
                            color = colorResource(R.color.white),
                            fontSize = 12.sp
                        )
                    )
                }
            }
        },
        endAction = listOf(
            {
                Box(
                    modifier = Modifier
                        .background(colorResource(R.color.blue))
                        .fillMaxSize()
                        .clickable {
                            Toast.makeText(context, "标为未读", Toast.LENGTH_SHORT).show()
                        }
                ) {
                    Text(
                        text = "标为未读",
                        modifier = Modifier.align(Alignment.Center),
                        style = TextStyle.Default.copy(
                            color = colorResource(R.color.white),
                            fontSize = 12.sp
                        )
                    )
                }
            },
            {
                Box(
                    modifier = Modifier
                        .background(colorResource(R.color.yellow))
                        .fillMaxSize()
                        .clickable {
                            Toast.makeText(context, "不显示", Toast.LENGTH_SHORT).show()
                        }
                ) {
                    Text(
                        text = "不显示",
                        modifier = Modifier.align(Alignment.Center),
                        style = TextStyle.Default.copy(
                            color = colorResource(R.color.white),
                            fontSize = 12.sp
                        )
                    )
                }
            }
        ),
        endFillAction = {
            Box(
                modifier = Modifier
                    .background(colorResource(R.color.red))
                    .fillMaxSize()
                    .clickable {
                        Toast.makeText(context, "删除", Toast.LENGTH_SHORT).show()
                    }
            ) {
                Box(
                    modifier = Modifier
                        .align(Alignment.CenterStart)
                        .width(70.dp)
                        .fillMaxHeight()
                ) {
                    Text(
                        text = "删除",
                        modifier = Modifier.align(Alignment.Center),
                        style = TextStyle.Default.copy(
                            color = colorResource(R.color.white),
                            fontSize = 12.sp
                        )
                    )
                }
            }
        }
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .background(colorResource(R.color.white))
                .padding(20.dp, 10.dp)
        ) {
            Text(
                text = "小美",
                color = colorResource(R.color.text_333),
                fontSize = 14.sp
            )
            Spacer(Modifier.size(5.dp))
            Text(
                text = "我的电脑坏了,你能过来看看嘛。",
                color = colorResource(R.color.text_666),
                fontSize = 12.sp
            )
        }
    }
}

Thanks

以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。

如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。

谢谢~~

源代码地址

推荐阅读

相关推荐
活宝小娜2 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点2 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow2 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o2 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic3 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā3 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年4 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder4 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_882727574 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架
SoaringHeart5 小时前
Flutter进阶:基于 MLKit 的 OCR 文字识别
前端·flutter