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

2,787 阅读3分钟

前言

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

按照惯例效果图先行:

SVID_20240327_110007_1 .gif

思路

威力加强版新增双侧滑动、多段展开和展开动画以便满足大家的日常需求。
虽然增加很多功能但思路不变还是通过Box堆叠并加上拖动手势实现,伪代码如下:

Box {
    Box(modifier = Modifier.offset {
        IntOffset(
            x = state.offset.roundToInt(),
            y = 0,
        )
    }
}

AnchoredDraggable

AnchoredDraggable 是一个基础 API,用于构建处于锚定状态的可拖动组件。
使用 DraggableAnchors 构建器方法定义锚点。然后,将它们传递给 AnchoredDraggableState的构造函数:

// 定义锚点左侧展开,左侧全展开,居中, 右侧展开,右侧全展开
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, //初始值,默认居中状态
            anchors = anchors,
        )
}

完整代码

fun SwipeBox(
    actionWidth: Dp,
    modifier: Modifier = Modifier,
    control: SwipeBoxControl = rememberSwipeBoxControl(),
    startAction: List<@Composable BoxScope.() -> Unit> = listOf(),
    startFillAction: (@Composable BoxScope.() -> Unit)? = null,
    endAction: List<@Composable BoxScope.() -> Unit> = listOf(),
    endFillAction: (@Composable BoxScope.() -> Unit)? = null,
    content: @Composable BoxScope.() -> Unit
) {
    val scope = rememberCoroutineScope()
    val density = LocalDensity.current
    val actionWidthPx = with(density) {
        actionWidth.toPx()
    }
    val startWidth = actionWidthPx * startAction.size
    // startAction + startFillAction
    val startActionSize = if (startFillAction == null) startAction.size else startAction.size + 1
    val endWidth = actionWidthPx * endAction.size
    // endAction + endFillAction
    val endActionSize = if (endFillAction == null) endAction.size else endAction.size + 1
    var contentWidth by remember { mutableFloatStateOf(0f) }
    var contentHeight by remember { mutableFloatStateOf(0f) }
    val state = remember(startWidth, endWidth, contentWidth) {
        AnchoredDraggableState(
            initialValue = DragAnchors.Center,
            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
            },
        )
    }
    LaunchedEffect(control, state) {
        with(control) {
            handleControlEvents(
                onStart = {
                    scope.launch {
                        state.animateTo(DragAnchors.Start)
                    }
                },
                onStartFill = {
                    scope.launch {
                        state.animateTo(DragAnchors.StartFill)
                    }
                },
                onCenter = {
                    scope.launch {
                        state.animateTo(DragAnchors.Center)
                    }
                },
                onEnd = {
                    scope.launch {
                        state.animateTo(DragAnchors.End)
                    }
                },
                onEndFill = {
                    scope.launch {
                        state.animateTo(DragAnchors.EndFill)
                    }
                }
            )
        }
    }
    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()
        }
    }
}

@Stable
class SwipeBoxControl(
    private val scope: CoroutineScope
) {
    private sealed interface ControlEvent {
        data object Start : ControlEvent
        data object StartFill : ControlEvent
        data object Center : ControlEvent
        data object End : ControlEvent
        data object EndFill : ControlEvent
    }

    private val controlEvents: MutableSharedFlow<ControlEvent> = MutableSharedFlow()

    @OptIn(FlowPreview::class)
    internal suspend fun handleControlEvents(
        onStart: () -> Unit = {},
        onStartFill: () -> Unit = {},
        onCenter: () -> Unit = {},
        onEnd: () -> Unit = {},
        onEndFill: () -> Unit = {},
    ) = withContext(Dispatchers.Main) {
        controlEvents.debounce(350).collect { event ->
            when (event) {
                ControlEvent.Start -> onStart()
                ControlEvent.StartFill -> onStartFill()
                ControlEvent.Center -> onCenter()
                ControlEvent.End -> onEnd()
                ControlEvent.EndFill -> onEndFill()
            }
        }
    }

    fun start() {
        scope.launch { controlEvents.emit(ControlEvent.Start) }
    }

    fun startFill() {
        scope.launch { controlEvents.emit(ControlEvent.Start) }
    }

    fun center() {
        scope.launch { controlEvents.emit(ControlEvent.Center) }
    }

    fun end() {
        scope.launch { controlEvents.emit(ControlEvent.End) }
    }

    fun endFill() {
        scope.launch { controlEvents.emit(ControlEvent.EndFill) }
    }
}

@Composable
fun rememberSwipeBoxControl(
    coroutineScope: CoroutineScope = rememberCoroutineScope()
): SwipeBoxControl = remember(coroutineScope) { SwipeBoxControl(coroutineScope) }

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

如何使用

@Composable
fun SwipeBoxScreen() {
    val context = LocalContext.current
    val control: SwipeBoxControl = rememberSwipeBoxControl()
    SwipeBox(
        control = control,
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight(),
        actionWidth = 70.dp,
        startAction = listOf {
            Box(
                modifier = Modifier
                    .background(colorResource(R.color.green))
                    .fillMaxSize()
                    .clickable {
                        control.center()
                        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 {
                            control.center()
                            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 {
                            control.center()
                            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 {
                            control.center()
                            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 {
                        control.center()
                        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

以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。
如果觉得本篇文章对您有帮助的话请点个赞让更多人看到吧,您的鼓励是我前进的动力。
谢谢~~

源代码地址