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

如何让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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 00:28:10