Jetpack Compose中CenterAlignedTopAppBar内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可避免崩溃,但需要了解根因与正确修复方式。
疑问
- 为何TopAppBar内的
sharedBounds仅在首屏崩溃,后续屏幕正常? - 如何确保首屏加载时
sharedBounds正确初始化? - 这是否是
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

