You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose自定义日程布局中实现垂直拖拽时段并修改拖拽表面颜色的问题

Jetpack Compose自定义日程布局中实现垂直拖拽时段并修改拖拽表面颜色的问题

嘿,我看了你遇到的问题——给自定义日程布局加了拖拽后屏幕冻住,没法滚动,还不知道怎么改拖拽时的颜色对吧?咱们一步步来解决:

首先,你当前的核心问题:

你把draggable直接加在了整个DynamicSchedule的Modifier上,这会导致整个布局的所有触摸事件都被拖拽逻辑拦截,自然没法滚动或者处理其他交互了。而且你要拖拽的是单个时段,不是整个日程表,所以得把拖拽逻辑移到每个预约项上才行。

解决方案分步走:

1. 把拖拽逻辑绑定到单个预约项

你需要把draggable从外层的Layout移到每个预约项的Box里,这样只有触摸到具体的时段卡片时才会触发拖拽,不影响其他区域的交互。

2. 实现拖拽时的颜色变化

我们可以用InteractionSource来监听拖拽状态,然后根据是否正在拖拽来修改卡片的背景色。

3. 处理拖拽位移,更新预约时间

拖拽时的delta是像素值,需要把它转换成对应的小时偏移,然后更新ViewModel里的预约数据,让布局自动重组更新位置。

修改后的代码示例:

首先,调整DynamicSchedule里的内容部分:

@Composable
fun DynamicSchedule(
    viewModel: CalenderViewModel,
    modifier: Modifier = Modifier,
    appointmentContent: @Composable (appointment: Appointment, isDragging: Boolean) -> Unit = { appointment, isDragging ->
        ScheduleCard(
            appointment = appointment,
            backgroundColor = if (isDragging) Color.LightBlue // 拖拽时的颜色
            else MaterialTheme.colors.surface // 默认颜色
        )
    },
    minDate: LocalDate,
    maxDate: LocalDate,
    dayWidth: Dp,
    hourHeight: Dp
) {
    val numDays = ChronoUnit.DAYS.between(minDate, maxDate).toInt() + 1
    val dividerColor = if (MaterialTheme.colors.isLight) Color.LightGray else Color.DarkGray
    // 移除外层的draggable,移到单个项上

    Layout(
        content = {
            viewModel.state.value.appointmentList.sortedBy { it.startDate }
                .forEach { appointment ->
                    val interactionSource = remember { MutableInteractionSource() }
                    val isDragging = interactionSource.collectIsDraggingAsState().value

                    Box(
                        modifier = Modifier
                            .appointmentData(appointment)
                            .draggable(
                                orientation = Orientation.Vertical,
                                interactionSource = interactionSource,
                                state = rememberDraggableState { delta ->
                                    // 把像素delta转换成小时偏移
                                    val hourOffset = delta / hourHeight.toPx()
                                    // 计算新的开始和结束时间
                                    val newStartTime = appointment.startDate.time.plusHours(hourOffset.toLong())
                                    val newEndTime = appointment.endDate.time.plusHours(hourOffset.toLong())
                                    
                                    // 这里可以加边界判断,比如不能拖到0点前或24点后
                                    if (newStartTime >= LocalTime.MIN && newEndTime <= LocalTime.MAX) {
                                        viewModel.updateAppointmentTime(appointment.id, newStartTime, newEndTime)
                                    }
                                }
                            )
                    ) {
                        appointmentContent(appointment, isDragging)
                    }
                }
        },
        modifier = modifier
            .drawBehind {
                // 保持原来的划线逻辑不变
                repeat(23) {
                    drawLine(
                        dividerColor,
                        start = Offset(0f, (it + 1) * hourHeight.toPx()),
                        end = Offset(size.width, (it + 1) * hourHeight.toPx()),
                        strokeWidth = 1.dp.toPx()
                    )
                }
                repeat(numDays - 1) {
                    drawLine(
                        dividerColor,
                        start = Offset((it + 1) * dayWidth.toPx(), 0f),
                        end = Offset((it + 1) * dayWidth.toPx(), size.height),
                        strokeWidth = 1.dp.toPx()
                    )
                }
            }
            .pointerInput(Unit) {
                // 保持原来的点击逻辑不变
                detectTapGestures {
                    val x = it.x.toDp()
                    val y = it.y.toDp()
                    val time = y.value.toInt() / hourHeight.value
                    val date = (x.value.toInt() / dayWidth.value)
                    println("X: $x, Y: $y")
                    println("Day: $date, Time: $time")
                }
            },
    ) { measurables, constraints ->
        // 保持原来的布局测量逻辑不变
        val height = hourHeight.roundToPx() * 24
        val width = dayWidth.roundToPx() * numDays
        val placeablesWithAppointment = measurables.map { measurable ->
            val appointment = measurable.parentData as Appointment
            val appointmentDurationInMinutes =
                ChronoUnit.MINUTES.between(
                    appointment.startDate.time.toJavaLocalTime(),
                    appointment.endDate.time.toJavaLocalTime()
                )
            val appointmentHeight =
                ((appointmentDurationInMinutes / 60f) * hourHeight.toPx()).roundToInt()
            val placeable = measurable.measure(
                constraints.copy(
                    minWidth = dayWidth.roundToPx(),
                    maxWidth = dayWidth.roundToPx(),
                    minHeight = appointmentHeight,
                    maxHeight = appointmentHeight
                )
            )
            Pair(placeable, appointment)
        }

        layout(width, height) {
            placeablesWithAppointment.forEach { (placeable, appointment) ->
                val appointmentOffsetMinutes =
                    ChronoUnit.MINUTES.between(
                        LocalTime.MIN,
                        appointment.startDate.time.toJavaLocalTime()
                    )
                val appointmentY =
                    ((appointmentOffsetMinutes / 60f) * hourHeight.toPx()).roundToInt()
                val appointmentOffsetDays =
                    ChronoUnit.DAYS.between(
                        minDate,
                        appointment.startDate.date.toJavaLocalDate()
                    ).toInt()
                val appointmentX = appointmentOffsetDays * dayWidth.roundToPx()
                placeable.place(appointmentX, appointmentY)
            }
        }
    }
}

然后,你需要在CalenderViewModel里添加更新预约时间的方法,比如:

fun updateAppointmentTime(appointmentId: String, newStartTime: LocalTime, newEndTime: LocalTime) {
    val updatedList = state.value.appointmentList.map { appointment ->
        if (appointment.id == appointmentId) {
            appointment.copy(
                startDate = appointment.startDate.copy(time = newStartTime),
                endDate = appointment.endDate.copy(time = newEndTime)
            )
        } else {
            appointment
        }
    }
    state.value = state.value.copy(appointmentList = updatedList)
}

关键点说明:

  • 拖拽范围缩小到单个项:现在只有点击/触摸到预约卡片时才会触发拖拽,整个日程表的其他区域(比如空白处)可以正常响应点击和滚动(如果你的外层有scrollable或verticalScroll的话)。
  • 拖拽状态监听:用interactionSource.collectIsDraggingAsState()来获取当前是否正在拖拽,动态修改卡片颜色。
  • 位移转换与边界控制:把像素偏移转换成小时偏移,并且限制时间在0-24点之间,避免出现不合理的时段。

这样修改后,你的屏幕不会再冻结,而且拖拽时段时会自动改变颜色,同时拖拽结束后预约的位置也会更新。

备注:内容来源于stack exchange,提问作者EliodeBeirut

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.23 07:59:31