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

Android Compose自定义网格日历HorizontalPager分页性能问题咨询

自定义Compose日历分页性能优化问题

我正在使用Compose开发自定义日历,通过LazyVerticalGrid显示整月日历,并借助HorizontalPager实现分页切换前后日历。当前日历视图可正常展示,但分页切换时性能表现不佳。想咨询:该日历的实现结构是否存在问题?是否因绘制视图过多导致性能下降?

实现代码

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CalendarPager(
    todayClicked: (LocalDate) -> Unit,
    pagedMonth: (LocalDate) -> Unit,
){
    val yearRange = IntRange(1970, 2100)
    val initialPage = (LocalDate.now().year - yearRange.first) * 12 + LocalDate.now().monthValue - 1
    val pagerState = rememberPagerState(initialPage = initialPage) {
        (yearRange.last - yearRange.first) * 12 // 모든 년도의 월 수 만큼 pageCount
    }

    HorizontalPager(state = pagerState) { page ->
        val month = LocalDate.of(
            yearRange.first + pagerState.currentPage / 12,
            pagerState.currentPage % 12 + 1,
            1
        )
        pagedMonth.invoke(month)

        val displayDate by remember {
            derivedStateOf {
                LocalDate.of(
                    yearRange.first + page / 12,
                    page % 12 + 1,
                    1
                )
            }
        }

        TestGrid(
            modifier = Modifier.padding(horizontal = 10.dp),
            calendarDate = displayDate,
            onSelectDate = todayClicked
        )
    }.also {
        DlogUtil.d(MyTag, "Recomposition Check HorizontalPager")
    }
}

@Composable
fun TestGrid(
    modifier: Modifier,
    calendarDate: LocalDate,

    onSelectDate: (LocalDate) -> Unit
) {

    val lastDay = calendarDate.lengthOfMonth()
    val days = IntRange(1, lastDay).toList()
    
    val firstDayOfWeek = calendarDate.dayOfWeek.value
    val lastDayColumn = (firstDayOfWeek + lastDay - 1) % 7
    
    val afterBoxCount = 7 - lastDayColumn - 1
    
    
    val totalGridCount = if (firstDayOfWeek == 7)
        0
    else
        firstDayOfWeek + days.size + afterBoxCount
    
    
    val boxHeight = if (totalGridCount > 35) {
        45.dp
    } else {
        54.dp
    }

    LazyVerticalGrid(
        modifier = modifier,
        columns = GridCells.Fixed(7)
    ) {
        
        if (firstDayOfWeek != 7) {
            repeat(firstDayOfWeek) {
                item {
                    EmptyDay(boxHeight)
                }
            }
        }

        items(days) { day ->
            val date = calendarDate.withDayOfMonth(day)

            TestDay(
                height = boxHeight,
                displayDate = date,
                isToday = date == LocalDate.now(),
                isSelected = false,
                onSelectDate = onSelectDate
            )
        }
        
        repeat(afterBoxCount) {
            item {
                EmptyDay(boxHeight)
            }
        }
    }.also {
        DlogUtil.d(MyTag, "Recomposition Check TestGrid")
    }
}

@Composable
fun TestDay(
    height: Dp,
    displayDate: LocalDate,
    isToday: Boolean,
    isSelected: Boolean,

    onSelectDate: (LocalDate) -> Unit
) {
    val dayOfWeek = displayDate.dayOfWeek

    Box(
        modifier = Modifier
            .height(height)
            .clip(shape = RoundedCornerShape(10.dp))
            .calendarBorder(isSelected)                    // 선택 Day Gray Border
            .noRippleClickable { onSelectDate(displayDate) },
        contentAlignment = Alignment.TopCenter
    ) {

        Box(
            modifier = Modifier
                .width(18.dp)
                .wrapContentHeight()
                .padding(top = 2.dp)
                .clip(shape = RoundedCornerShape(4.dp))
                .calendarBackground(isToday, getTodayBgColor(dayOfWeek)),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = displayDate.dayOfMonth.toString(),
                color = getTextColor(isToday, dayOfWeek),
                fontSize = 12.sp,
                fontWeight = FontWeight.Bold
            )
        }
    }.also {
        DlogUtil.d(MyTag, "Recomposition Check TestDay Box")
    }
}

问题分析与优化方案

一、实现结构的核心问题

  1. HorizontalPager日期计算错误
    在HorizontalPager的content lambda中,你用pagerState.currentPage计算month,但正确的应该用传入的page参数。currentPage是当前选中的页面,而page是当前正在组合的页面,混用会触发不必要的重组。同时displayDate的计算不需要derivedStateOf,直接用page计算即可。

  2. 不稳定变量触发频繁重组

    • TestGrid中days = IntRange(1, lastDay).toList()每次重组都会生成新列表,导致子组件重复重组,需要用remember包裹稳定化。
    • TestDay中直接调用LocalDate.now()判断isToday,每次重组都会重新计算,应该把当前日期提升到上层缓存后传递。
  3. 冗余布局处理
    LazyVerticalGrid中的空项EmptyDay用多次repeat生成独立item,可以简化逻辑;且EmptyDay如果仅做占位,应尽量减少布局层级。

二、具体优化步骤

  1. 修正HorizontalPager日期逻辑

    HorizontalPager(state = pagerState) { page ->
        val displayDate = LocalDate.of(
            yearRange.first + page / 12,
            page % 12 + 1,
            1
        )
        pagedMonth.invoke(displayDate)
    
        TestGrid(
            modifier = Modifier.padding(horizontal = 10.dp),
            calendarDate = displayDate,
            onSelectDate = todayClicked
        )
    }
    
  2. 稳定化TestGrid内的变量

    // 在TestGrid中用remember缓存日期列表
    val days = remember(calendarDate) { 
        IntRange(1, calendarDate.lengthOfMonth()).toList() 
    }
    
  3. 缓存当前日期避免重复计算
    在CalendarPager中缓存当前日期,传递给子组件:

    val today = rememberSaveable { LocalDate.now() }
    

    然后在TestGrid的items中传递isToday:

    TestDay(
        height = boxHeight,
        displayDate = date,
        isToday = date == today,
        isSelected = false,
        onSelectDate = onSelectDate
    )
    
  4. 简化EmptyDay组件
    如果仅做占位,直接用Spacer替代:

    // 在LazyVerticalGrid的空项中
    repeat(firstDayOfWeek) {
        item {
            Spacer(modifier = Modifier.height(boxHeight))
        }
    }
    
  5. 调整HorizontalPager预加载策略
    默认HorizontalPager会预加载前后各一页,若不需要可减少预加载数量:

    HorizontalPager(
        state = pagerState,
        offscreenLimit = 0 // 仅加载当前页,按需调整
    ) { page ->
        // ...
    }
    

三、性能下降的根本原因

不是因为绘制视图过多,而是不必要的重组导致的:

  • 不稳定的参数(如每次生成的新列表、频繁计算的LocalDate.now())触发大量组件重复重组,增加Compose计算开销。
  • 混用currentPage与page参数导致页面切换时错误的状态更新,触发额外重组。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 10:17:42