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
相关产品推荐
相关产品推荐

