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

Compose LazyColumn中ExoPlayer返回列表页时高度跳动问题求助

问题:从详情页返回列表页时ExoPlayer高度跳动

在Jetpack Compose中实现了以下功能:

  • LazyColumn顶部放置占屏幕高度一半的ExoPlayer播放器,下方是可跳转至详情页的列表项
  • 使用Accompanist Navigation实现页面间上下滑动转场:点击列表项时详情页向上滑入,返回时向下滑出
  • 遇到的问题:从详情页返回列表页时,顶部VideoPlayer视图出现高度跳动(先变大,随后恢复到计算好的半屏高度)

原因分析

  1. 屏幕尺寸计算时机偏差:通过context.resources.displayMetrics计算屏幕高度时,从详情页返回的重组阶段可能获取到临时不准确的尺寸值,导致高度计算出现波动
  2. LazyColumn重组特性影响:LazyColumn的item在重组时会重新触发测量布局,而AndroidView的初始化与尺寸约束存在时序差,造成初始布局高度异常
  3. 转场动画的布局波动:页面转场过程中,导航宿主的尺寸可能处于动态变化状态,VideoPlayer的高度约束未能及时生效

解决方案

1. 使用Compose官方API获取稳定屏幕尺寸

替代原生DisplayMetrics,改用LocalWindowInfo获取屏幕高度,确保在Compose生命周期内尺寸值稳定:

@Composable
fun ListScreen(navController: NavHostController?) {
    val windowInfo = LocalWindowInfo.current
    val screenHeightDp = remember(windowInfo) {
        windowInfo.screenSize.height.value.toDp()
    }
    val videoPlayerHeight = remember(screenHeightDp) {
        screenHeightDp / 2
    }

    Box {
        LazyColumn {
            item {
                VideoPlayerViewCompose(videoPlayerHeight)
            }
            items(sampleList) { sampleItem ->
                ListItemCompose(sampleItem) {
                    navController?.navigate("details_screen/${sampleItem.id}")
                }
            }
        }
    }
}

2. 给VideoPlayer添加稳定约束并避免过度重建

在VideoPlayerViewCompose中固定尺寸约束,同时用高度作为AndroidView的key,仅在高度变化时重建视图:

@Composable
fun VideoPlayerViewCompose(height: Dp) {
    Box(modifier = Modifier
        .height(height)
        .fillMaxWidth()) {
        AndroidView(
            factory = { context ->
                Logger.d { "PlayerView factory created" }
                PlayerView(context).apply {
                    hideController()
                    useController = false
                    player = exoPlayer
                    resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
                    layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                    clipToOutline = true
                }
            },
            update = { playerView ->
                val lifecycleOwner = LocalLifecycleOwner.current
                playerView.player?.apply {
                    when (lifecycleOwner.lifecycle.currentState) {
                        Lifecycle.State.RESUMED -> play()
                        else -> pause()
                    }
                }
            },
            key = height
        )
    }
}

3. 固定导航宿主尺寸避免转场波动

给AnimatedNavHost添加fillMaxSize修饰符,确保导航容器始终占满屏幕,消除转场过程中的布局尺寸变化:

@Composable
fun AppNavigationCompose() {
    val navAnimatedController = rememberAnimatedNavController()
    val tweenSpec = tween<Int>(durationMillis = 300)
    
    AnimatedNavHost(
        navController = navAnimatedController,
        startDestination = "list_screen",
        modifier = Modifier.fillMaxSize()
    ) {
        composable("list_screen") {
            ListScreen(navAnimatedController)
        }
        composable(
            route = "details_screen/{id}",
            arguments = listOf(
                navArgument(name = "id") {
                    type = NavType.IntType
                }
            ),
            enterTransition = {
                slideInVertically(
                    initialOffsetY = { fullHeight -> fullHeight },
                    animationSpec = tweenSpec
                )
            },
            exitTransition = {
                slideOutVertically(
                    targetOffsetY = { fullHeight -> fullHeight },
                    animationSpec = tweenSpec
                )
            },
            popEnterTransition = {
                slideInVertically(
                    initialOffsetY = { fullHeight -> fullHeight },
                    animationSpec = tweenSpec
                )
            },
            popExitTransition = {
                slideOutVertically(
                    targetOffsetY = { fullHeight -> fullHeight },
                    animationSpec = tweenSpec
                )
            }
        ) { navBackStackEntry ->
            DetailsScreen(
                listItemId = navBackStackEntry.arguments!!.getInt("id"),
                navController = navAnimatedController
            )
        }
    }
}

4. 保存LazyColumn滚动状态

用rememberLazyListState保存列表滚动状态,返回时恢复到之前的位置,减少不必要的重组:

@Composable
fun ListScreen(navController: NavHostController?) {
    // ... 其他代码 ...
    val listState = rememberLazyListState()
    
    Box {
        LazyColumn(state = listState) {
            // ... 内容项 ...
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 15:02:05