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

Jetpack Compose中CenterAlignedTopAppBar内sharedBounds首屏崩溃问题

Jetpack Compose SharedTransitionLayout首屏TopAppBar内sharedBounds崩溃问题

问题描述

我在Jetpack Compose中使用SharedTransitionLayout实现跨页面元素动画时遇到以下问题:

  • ✅ 将Modifier.sharedBounds放在CenterAlignedTopAppBar标题外部时一切正常
  • ❌ 将Modifier.sharedBounds放在标题内部时,应用启动即崩溃,报错:

java.lang.IllegalStateException: Uninitialized LayoutCoordinates. Please make sure when using the SharedTransitionScope composable function, the modifier passed to the child content is being used, or use SharedTransitionLayout instead.

已观察到的现象

  • 移除sharedBounds修饰符后崩溃消失
  • 所有过渡相关对象(sharedTransitionScope、animatedVisibilityScope、sharedContentState)均已正确初始化
  • 仅首屏加载时崩溃,导航到其他包含TopAppBar内sharedBounds的屏幕可正常工作
  • 任何设置为首屏且包含该AppBar配置的屏幕都会崩溃

触发问题的简化代码

Scaffold(
    topBar = {
        CenterAlignedTopAppBar(
            title = {
                with(sharedTransitionScope) {
                    Text(
                        modifier = Modifier.sharedBounds(
                            sharedContentState = rememberSharedContentState(
                                key = "TITLE_TRANSITION_KEY"
                            ),
                            animatedVisibilityScope = animatedVisibilityScope
                        ),
                        text = "My App",
                        fontWeight = FontWeight.Bold,
                        textAlign = TextAlign.Center
                    )
                }
            }
        )
    }
) { /* Content */ }

临时解决方案

在应用sharedBounds前检查isReady可避免崩溃,但需要了解根因与正确修复方式。

疑问

  1. 为何TopAppBar内的sharedBounds仅在首屏崩溃,后续屏幕正常?
  2. 如何确保首屏加载时sharedBounds正确初始化?
  3. 这是否是SharedTransitionLayout的Bug,还是我的配置存在遗漏?

完整相关代码

AbstractActivity.kt

abstract class AbstractActivity : ComponentActivity() {

    @Inject
    lateinit var preferences: PreferencesUseCase

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val themeId = preferences.getInt(
            CommonConstants.PREFERENCE_THEME_ID_KEY, CommonConstants.THEME_DARK_KEY
        )

        val appTheme = ThemeHelper.getTheme(themeId, ThemeHelper.DEFAULT_THEME)

        setTheme(appTheme.resourceId)

        enableEdgeToEdge()

        setContent {
            ApplicationTheme(appTheme, direction = LayoutDirection.Rtl) {
                AppContent()
            }
        }
    }

    @Composable
    abstract fun AppContent()
}

AppActivity.kt

@AndroidEntryPoint
class AppActivity : AbstractActivity() {

    @Composable
    override fun AppContent() {
        val navController = rememberNavController()

        SharedTransitionLayout {
            NavHost(
                navController = navController,
                modifier = Modifier
                    .fillMaxSize()
                    .imePadding(),
                startDestination = AppDestinations.Main
            ) {
                composable<AppDestinations.Main> {
                    MainScreen(
                        sharedTransitionScope   = this@SharedTransitionLayout,
                        animatedVisibilityScope = this
                    )
                }

                composable<AppDestinations.Settings> {
                    SettingsScreen(
                        sharedTransitionScope   = this@SharedTransitionLayout,
                        animatedVisibilityScope = this
                    )
                }
            }
        }
    }
}

MainScreen.kt

@Composable
fun MainScreen(
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    Scaffold(
        topBar = {
            with(sharedTransitionScope) {
                CenterAlignedTopAppBar(
                    title = {
                        Text(
                            modifier = Modifier
                                .sharedBounds(
                                    sharedContentState = rememberSharedContentState(
                                        key = "TOOLBAR_TITLE_EXPLODE_BOUNDS"
                                    ),
                                    animatedVisibilityScope = animatedVisibilityScope
                                ),
                            text = stringResource(R.string.app_name),
                            fontWeight = FontWeight.Bold,
                            textAlign = TextAlign.Center
                        )
                    }
                )
            }
        }
    ) { innerPadding ->
        // 内容区域
    }
}

SettingsScreen.kt

@Composable
fun SettingsScreen(
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope,
) {
    Scaffold(
        topBar = {
            with(sharedTransitionScope) {
                TopAppBar(
                    title = {
                        Text(
                            modifier = Modifier
                                .sharedBounds(
                                    sharedContentState = rememberSharedContentState(
                                        key = "TOOLBAR_TITLE_EXPLODE_BOUNDS"
                                    ),
                                    animatedVisibilityScope = animatedVisibilityScope
                                ),
                            text = stringResource(R.string.toolbar_title_settings),
                            fontWeight = FontWeight.Bold,
                            textAlign = TextAlign.Center
                        )
                    }
                )
            }
        }
    ) { innerPadding ->
        // 内容区域
    }
}

解答

1. 首屏崩溃的原因

首屏加载时,SharedTransitionLayout的内部布局还未完成测量与布局,而TopAppBar的标题区域是延迟组合的内容(属于@Composable lambda参数),此时sharedBounds尝试获取LayoutCoordinates时,布局系统还未完成初始化,导致抛出未初始化异常。后续屏幕导航时,SharedTransitionLayout已经完成初始化,布局坐标已就绪,因此不会崩溃。

2. 正确修复方式

有两种可靠的修复方案:

方案一:延迟初始化sharedBounds

使用onGloballyPositioned监听布局就绪状态,等待布局完成后再应用sharedBounds修饰符:

@Composable
fun MainScreen(
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    val titleContentState = rememberSharedContentState(key = "TOOLBAR_TITLE_EXPLODE_BOUNDS")
    val isLayoutReady = remember { mutableStateOf(false) }
    
    Scaffold(
        topBar = {
            with(sharedTransitionScope) {
                CenterAlignedTopAppBar(
                    title = {
                        Text(
                            modifier = Modifier
                                .onGloballyPositioned { isLayoutReady.value = true }
                                .then(
                                    if (isLayoutReady.value) {
                                        Modifier.sharedBounds(
                                            sharedContentState = titleContentState,
                                            animatedVisibilityScope = animatedVisibilityScope
                                        )
                                    } else Modifier
                                ),
                            text = stringResource(R.string.app_name),
                            fontWeight = FontWeight.Bold,
                            textAlign = TextAlign.Center
                        )
                    }
                )
            }
        }
    ) { innerPadding ->
        // 内容区域
    }
}

方案二:将sharedBounds移到TopAppBar层级

保持sharedBounds在TopAppBar外部而非标题内部,这样布局初始化顺序更稳定,不会触发未初始化异常(这也是你之前验证可行的方式)。

3. 是否为Bug?

这不属于SharedTransitionLayout的Bug,而是布局初始化顺序的问题:首屏加载时,延迟组合的标题内容会先于SharedTransitionLayout的完整布局完成,导致sharedBounds无法获取有效布局坐标。后续屏幕导航时,SharedTransitionLayout已经完成初始化,因此不会出现此问题。你的配置本身没有遗漏,但需要注意sharedBounds的应用时机与布局初始化顺序的关系。


内容的提问来源于stack exchange,提问作者Mohammad ali Riazati

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 06:57:34