如何让LazyColumn/LazyRow兼容双列表,实现日历视图平滑切换?
解决Compose日历月/周视图平滑切换的方案
要实现月视图(1个单元对应1个月)和周视图(1个单元对应1周,1个月对应4-6周)的双向平滑切换,核心是让Lazy列表能识别两种视图单元的关联关系,通过统一的Key体系和共享动画状态来实现过渡。
1. 建立关联式Key体系
首先定义一个密封类,把月、周的Key封装起来,让周Key携带所属月的信息,确保两种视图的Key能建立关联:
sealed class CalendarPageKey { // 月视图Key:对应完整月份 data class Month(val yearMonth: YearMonth) : CalendarPageKey() // 周视图Key:包含所属的父月Key,用于关联月视图单元 data class Week(val yearWeek: String, val parentMonth: YearMonth) : CalendarPageKey() }
修改你的状态类逻辑:
- 月视图模式:
state.getKey(page)返回CalendarPageKey.Month(yearMonth) - 周视图模式:
state.getKey(page)返回CalendarPageKey.Week(yearWeek, 对应YearMonth)
2. 调整LazyRow的项动画逻辑
不再让LazyRow直接替换月/周UI,而是通过AnimatedContent基于父月Key共享动画状态,确保同属一个月的周单元和月单元能平滑过渡。
迁移到你的日历核心代码
@OptIn(ExperimentalFoundationApi::class) @Composable fun CalendarView( state: CalendarState, isMonthView: Boolean, modifier: Modifier = Modifier ) { val lazyState = rememberLazyRowState() LazyRow( modifier = modifier, state = lazyState, flingBehavior = rememberSnapFlingBehavior(lazyState) ) { items( count = if (isMonthView) state.monthCount else state.weekCount, key = { page -> state.getKey(page) } ) { page -> val pageKey = state.getKey(page) // 提取父月Key,用于共享动画状态 val parentMonthKey = when (pageKey) { is CalendarPageKey.Month -> pageKey is CalendarPageKey.Week -> CalendarPageKey.Month(pageKey.parentMonth) } AnimatedContent( targetState = Pair(isMonthView, parentMonthKey), label = "calendar-view-switch" ) { (currentIsMonthView, currentParentKey) -> when (currentIsMonthView) { true -> { // 月视图内容:对应currentParentKey的月份 MonthField( date = currentParentKey.yearMonth, modifier = Modifier.animateContentSize() ) } false -> { // 周视图内容:当前page对应的周 val weekDate = state.getDateByPage(page) WeekField( date = weekDate, modifier = Modifier.animateContentSize() ) } } } } } }
3. 同步切换时的滚动位置
切换视图时,需要将滚动位置同步到对应单元:
// 切换视图的回调 fun switchView() { val currentKey = lazyState.firstVisibleItemKey val targetPage = when (currentKey) { is CalendarPageKey.Month -> state.getFirstWeekPageOfMonth(currentKey.yearMonth) is CalendarPageKey.Week -> state.getMonthPageOfWeek(currentKey.yearWeek) } lazyState.scrollToItem(targetPage) }
修复你提供的最小复现示例
针对你的测试代码,修改后实现全项平滑动画:
@OptIn(ExperimentalFoundationApi::class) @Composable fun MainScreen(innerPadding: PaddingValues) { val list = listOf(0, 1, 2, 3, 4, 5) private interface View { val count: Int fun key(index: Int): Any fun content(index: Int): List<Int> // 新增:获取当前项所属的父组Key fun parentKey(index: Int): Any } val view1 = object : View { override val count: Int get() = list.size / 3 override fun key(index: Int) = list[index * 3] override fun content(index: Int) = list.subList(index * 3, (index + 1) * 3) override fun parentKey(index: Int) = key(index) } val view2 = object : View { override val count: Int get() = list.size override fun key(index: Int) = list[index] override fun content(index: Int) = list.subList(index / 3 * 3, (index / 3 + 1) * 3) override fun parentKey(index: Int) = list[index / 3 * 3] } var view: View by remember { mutableStateOf(view1) } val state = rememberLazyListState() LazyColumn( modifier = Modifier .height(100.dp) .clickable { view = if (view == view1) view2 else view1 }, state = state, flingBehavior = rememberSnapFlingBehavior(state) ) { items( count = view.count, key = view::key, ) { index -> val parentKey = view.parentKey(index) // 基于父组Key共享动画状态 AnimatedContent(targetState = Pair(view, parentKey), label = "view-switch") { (currentView, _) -> Row(modifier = Modifier.fillParentMaxHeight()) { currentView.content(index).forEach { item -> val isVisible = currentView == view1 || currentView.key(index) == item AnimatedVisibility(visible = isVisible, label = "item-visibility") { Text( "$item", modifier = Modifier .width(100.dp) .fillMaxHeight() .border(1.dp, Color(0xFF6200EE)), textAlign = TextAlign.Center, verticalAlignment = Alignment.CenterVertically ) } } } } } } }
内容的提问来源于stack exchange,提问作者Fan Applelin
相关产品推荐
相关产品推荐

