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

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

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

谢谢~~

源代码地址

推荐阅读

相关推荐
OpenTiny社区几秒前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠30 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞34 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js