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

Android Compose多重叠时间段并排展示实现方案咨询

Android Compose 日程布局:多重叠事件并排展示实现方案

核心思路

实现重叠事件并排展示的关键是识别所有直接/间接重叠的事件组,为组内每个事件分配对应的横向宽度和偏移量,同时保持纵向位置与时间对应。

具体实现步骤

1. 预处理当天事件

先筛选出当前选中日期的事件,并按开始时间排序,方便后续识别重叠关系:

val dayBookings = bookings
    .filter { booking ->
        booking.startTimestamp.toJavaLocalDateTime().toLocalDate() == currentSelectedDay
    }
    .sortedBy { it.startTimestamp.toJavaLocalDateTime() }

2. 划分重叠事件组

编写工具函数,将所有存在直接或间接重叠的事件归为同一组:

private fun groupOverlappingBookings(bookings: List<Booking>): List<List<Booking>> {
    if (bookings.isEmpty()) return emptyList()
    val sortedBookings = bookings.sortedBy { it.startTimestamp.toJavaLocalDateTime() }
    val groups = mutableListOf<MutableList<Booking>>()
    var currentGroup = mutableListOf(sortedBookings[0])
    groups.add(currentGroup)
    
    for (i in 1 until sortedBookings.size) {
        val currentBooking = sortedBookings[i]
        val lastInGroup = currentGroup.last()
        if (currentBooking.startTimestamp.toJavaLocalDateTime() <= lastInGroup.endTimestamp.toJavaLocalDateTime()) {
            currentGroup.add(currentBooking)
            // 合并所有与当前事件有重叠的组
            val overlappingGroups = groups.filter { group ->
                group.first().startTimestamp.toJavaLocalDateTime() <= currentBooking.endTimestamp.toJavaLocalDateTime()
            }
            if (overlappingGroups.size > 1) {
                val mergedGroup = overlappingGroups.flatten().toMutableList()
                groups.removeAll(overlappingGroups)
                groups.add(mergedGroup)
                currentGroup = mergedGroup
            }
        } else {
            currentGroup = mutableListOf(currentBooking)
            groups.add(currentGroup)
        }
    }
    return groups
}

3. 构建事件布局参数映射

为每个事件记录它在组中的索引和组的总事件数,用于计算宽度和横向偏移:

val bookingLayoutMap = mutableMapOf<Booking, Pair<Int, Int>>() // <事件, (组内索引, 组大小)>
overlappingGroups.forEach { group ->
    group.forEachIndexed { index, booking ->
        bookingLayoutMap[booking] = Pair(index, group.size)
    }
}

4. 修改测量与布局逻辑

在测量阶段根据组大小计算事件宽度,布局阶段根据组内索引计算横向偏移:

// 测量阶段:计算事件宽度
val placeablesWithEvents = measureables.map { measurable ->
    val booking = measurable.parentData as Booking
    val eventDurationMinutes = ChronoUnit.MINUTES.between(
        booking.startTimestamp.toJavaLocalDateTime(),
        booking.endTimestamp.toJavaLocalDateTime()
    )
    val eventHeight = ((eventDurationMinutes / 60f) * hourHeight.toPx()).roundToInt()
    val (_, groupSize) = bookingLayoutMap[booking] ?: Pair(0, 1)
    val eventWidth = (constraints.maxWidth - 30) / groupSize
    val placeable = measurable.measure(Constraints.fixed(eventWidth, eventHeight))

    Pair(placeable, booking)
}

// 布局阶段:计算横向偏移并放置事件
layout(constraints.maxWidth, height) {
    placeablesWithEvents.forEach { (placeable, booking) ->
        val eventOffsetMinutes = ChronoUnit.MINUTES.between(
            LocalTime.MIN, booking.startTimestamp.toJavaLocalDateTime()
        )
        val eventY = ((eventOffsetMinutes / 60f) * hourHeight.toPx()).roundToInt()
        val (index, groupSize) = bookingLayoutMap[booking] ?: Pair(0, 1)
        val eventWidth = (constraints.maxWidth - 30) / groupSize
        val eventX = index * eventWidth

        placeable.place(eventX, eventY)
    }
}

完整修改后的代码

@Composable
fun BasicSchedule(
    bookings: List<Booking> = mainViewModel.listOfBookings,
    modifier: Modifier,
    currentSelectedDay: LocalDate = mainViewModel.currentSelectedDay,
    hourHeight: Dp = 64.dp,
) {
    val dividerColor = if (MaterialTheme.colors.isLight) Color.LightGray else Color.DarkGray
    var height = 0

    Layout(content = {
        bookings.sortedBy(Booking::startTimestamp).forEach { booking ->
            if (currentSelectedDay == booking.startTimestamp.toJavaLocalDateTime()
                    .toLocalDate()
            ) {
                Box(modifier = Modifier.eventData(booking)) {
                    BookingViewComposable(booking = booking)
                }
            }
        }
    }, modifier = modifier
        .fillMaxSize()
        .drawBehind {
            repeat(23) {
                drawLine(
                    dividerColor,
                    start = Offset(0f, (it + 1) * hourHeight.toPx()),
                    end = Offset(size.width, (it + 1) * hourHeight.toPx()),
                    strokeWidth = 1.dp.toPx()
                )
            }
        }
        .pointerInput(Unit) {
            detectTapGestures(onLongPress = { offset ->
                mainViewModel.newBookingStartTime = offset.y
                    .div(height / 24)
                    .roundToInt()
                mainViewModel.createNewBookingDialogOpen = true
            })
        }) { measureables, constraints ->
        height = hourHeight.roundToPx() * 24

        // 筛选并排序当天事件
        val dayBookings = bookings
            .filter { booking ->
                booking.startTimestamp.toJavaLocalDateTime().toLocalDate() == currentSelectedDay
            }
            .sortedBy { it.startTimestamp.toJavaLocalDateTime() }

        // 划分重叠事件组
        val overlappingGroups = groupOverlappingBookings(dayBookings)

        // 构建事件布局参数映射
        val bookingLayoutMap = mutableMapOf<Booking, Pair<Int, Int>>()
        overlappingGroups.forEach { group ->
            group.forEachIndexed { index, booking ->
                bookingLayoutMap[booking] = Pair(index, group.size)
            }
        }

        val placeablesWithEvents = measureables.map { measurable ->
            val booking = measurable.parentData as Booking
            val eventDurationMinutes = ChronoUnit.MINUTES.between(
                booking.startTimestamp.toJavaLocalDateTime(),
                booking.endTimestamp.toJavaLocalDateTime()
            )
            val eventHeight = ((eventDurationMinutes / 60f) * hourHeight.toPx()).roundToInt()
            val (_, groupSize) = bookingLayoutMap[booking] ?: Pair(0, 1)
            val eventWidth = (constraints.maxWidth - 30) / groupSize
            val placeable = measurable.measure(Constraints.fixed(eventWidth, eventHeight))

            Pair(placeable, booking)
        }
        
        layout(constraints.maxWidth, height) {
            placeablesWithEvents.forEach { (placeable, booking) ->
                val eventOffsetMinutes = ChronoUnit.MINUTES.between(
                    LocalTime.MIN, booking.startTimestamp.toJavaLocalDateTime()
                )
                val eventY = ((eventOffsetMinutes / 60f) * hourHeight.toPx()).roundToInt()
                val (index, groupSize) = bookingLayoutMap[booking] ?: Pair(0, 1)
                val eventWidth = (constraints.maxWidth - 30) / groupSize
                val eventX = index * eventWidth

                placeable.place(eventX, eventY)
            }
        }
    }
}

// 重叠事件分组工具函数
private fun groupOverlappingBookings(bookings: List<Booking>): List<List<Booking>> {
    if (bookings.isEmpty()) return emptyList()
    val sortedBookings = bookings.sortedBy { it.startTimestamp.toJavaLocalDateTime() }
    val groups = mutableListOf<MutableList<Booking>>()
    var currentGroup = mutableListOf(sortedBookings[0])
    groups.add(currentGroup)
    
    for (i in 1 until sortedBookings.size) {
        val currentBooking = sortedBookings[i]
        val lastInGroup = currentGroup.last()
        if (currentBooking.startTimestamp.toJavaLocalDateTime() <= lastInGroup.endTimestamp.toJavaLocalDateTime()) {
            currentGroup.add(currentBooking)
            val overlappingGroups = groups.filter { group ->
                group.first().startTimestamp.toJavaLocalDateTime() <= currentBooking.endTimestamp.toJavaLocalDateTime()
            }
            if (overlappingGroups.size > 1) {
                val mergedGroup = overlappingGroups.flatten().toMutableList()
                groups.removeAll(overlappingGroups)
                groups.add(mergedGroup)
                currentGroup = mergedGroup
            }
        } else {
            currentGroup = mutableListOf(currentBooking)
            groups.add(currentGroup)
        }
    }
    return groups
}

// 扩展Modifier存储Booking数据
private fun Modifier.eventData(booking: Booking): Modifier = this.then(
    object : ParentDataModifier {
        override fun Density.modifyParentData(parentData: Any?): Any = booking
    }
)

内容的提问来源于stack exchange,提问作者jmlinar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 16:02:56