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

Jetpack Compose共享元素过渡异常问题求助

Jetpack Compose 共享元素过渡异常问题

初始状态下,列表页与详情页(新增/编辑页)的共享元素过渡正常,但触发以下操作后过渡效果失效,仅显示元素消失后重新弹出,不再有首次的ResizeMode.ScaleToBounds过渡效果:

  • 通过FAB进入新增笔记页
  • 搜索笔记
  • 快速删除并恢复笔记

怀疑是笔记重组导致的异常,已尝试切换为shareElement modifier并调整其位置,但问题仍未解决。

相关代码

NoteContent 代码

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun NoteContent(
    state: NoteState,
    query: String,
    onOrderChange: (NoteOrder) -> Unit,
    onToggleOrderSectionClick: () -> Unit,
    onDeleteClick: (Note) -> Unit,
    onUndoClick: () -> Unit,
    navigateToAddEdit: (Int, Int) -> Unit,
    onQueryChange: (String) -> Unit,
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    val snackbarHostState = remember { SnackbarHostState() }
    val scope = rememberCoroutineScope()
    val staggeredGridState = rememberLazyStaggeredGridState()

    with(sharedTransitionScope){
        Scaffold(
            floatingActionButton = {
                FloatingActionButton(
                    onClick = {navigateToAddEdit(-1,-1)},
                    containerColor = MaterialTheme.colorScheme.primary,
                ) {
                    Icon(imageVector = Icons.Default.Add, contentDescription = "Add Note")
                }
            },
            snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
        ) { padding ->
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(horizontal = 8.dp)
            ) {
                LazyVerticalStaggeredGrid(
                    state = staggeredGridState,
                    columns = StaggeredGridCells.Fixed(2),
                    modifier = Modifier.fillMaxWidth(),
                    contentPadding = PaddingValues(bottom = 4.dp),
                    horizontalArrangement = Arrangement.spacedBy(4.dp),
                    verticalItemSpacing = 4.dp
                ) {
                    item(span = StaggeredGridItemSpan.FullLine) {
                        SearchBar(
                            query = query,
                            onQueryChange = onQueryChange,
                            onToggleOrderSectionClick = onToggleOrderSectionClick
                        )
                    }

                    item(span = StaggeredGridItemSpan.FullLine) {
                        AnimatedVisibility(
                            visible = state.isOrderSectionVisible,
                            enter = fadeIn() + slideInVertically(),
                            exit = fadeOut() + slideOutVertically()
                        ) {
                            OrderRadio(modifier = Modifier
                                .fillMaxWidth()
                                .padding(vertical = 8.dp, horizontal = 8.dp),
                                noteOrder = state.noteOrder,
                                onOrderChange = { order ->
                                    onOrderChange(order)
                                })
                        }
                    }

                    items(state.notes, key = { it.id ?: 0 }) { note ->
                        AnimatedVisibility(
                            visible = state.notes.isNotEmpty(),
                            enter = fadeIn(),
                            exit = fadeOut(),
                        ) {
                            NoteItem(note = note,
                                modifier = Modifier
                                    .padding(4.dp)
                                    .fillMaxWidth()
                                    .animateItem(
                                        placementSpec = tween(
                                            durationMillis = 300, delayMillis = 0
                                        )
                                    )
                                    .sharedBounds(
                                        rememberSharedContentState(key = "note/${note.id}"),
                                        animatedVisibilityScope = animatedVisibilityScope,
                                        enter = fadeIn(),
                                        exit = fadeOut(),
                                        resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds()
                                    ),
                                onDeleteClick = {
                                    onDeleteClick(note)
                                    scope.launch {
                                        val result = snackbarHostState.showSnackbar(
                                            message = "Note deleted", actionLabel = "Undo"
                                        )
                                        if (result == SnackbarResult.ActionPerformed) {
                                            onUndoClick()
                                        }
                                    }
                                },
                                onClick = {
                                    note.id?.let { it1 -> navigateToAddEdit(it1, note.color) }
                                })
                        }
                    }
                }

                AnimatedVisibility(
                    visible = state.notes.isEmpty(),
                    enter = fadeIn(),
                    exit = fadeOut(),
                ) {
                    EmptyNote(
                        isSearchActive = state.isSearchActive
                    )
                }
            }
        }
    }
}

AddEditScreen 代码

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun AddEditNoteScreen(
    navController: NavController,
    noteColor : Int,
    noteId:Int?,
    viewModel: AddEditViewModel = hiltViewModel(),
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
){
    val titleState = viewModel.title.value
    val contentState = viewModel.content.value

    val snackbarHostState = remember { SnackbarHostState() }

    Log.d("test",  noteColor.toString())

    val backgroundAnimateableColor = remember {
        Animatable(
            Color(if (noteColor != -1) noteColor else viewModel.color.value)
        )
    }

    if(noteColor != -1){
        viewModel.onEvent(AddEditNoteEvent.ChangeColor(noteColor))
    }

    val scope = rememberCoroutineScope()

    LaunchedEffect(key1 = true) {
        viewModel.eventFlow.collectLatest { event ->
            when (event) {
                is AddEditViewModel.UiEvent.ShowSnackbar -> {
                    snackbarHostState.showSnackbar(
                        message = event.message
                    )
                }
                is AddEditViewModel.UiEvent.SaveNote -> {
                    navController.navigateUp()
                }
            }
        }
    }

    with(sharedTransitionScope){
        Scaffold(
            floatingActionButton = {
                FloatingActionButton(
                    onClick = {
                        viewModel.onEvent(AddEditNoteEvent.SaveNote)
                    },
                    containerColor = MaterialTheme.colorScheme.primary
                ){
                    Icon(imageVector = Icons.Default.Check , contentDescription = "Save" )
                }
            },
            modifier = Modifier
                .sharedBounds(
                    rememberSharedContentState(key = "note/$noteId"),
                    animatedVisibilityScope = animatedVisibilityScope,
                    enter = fadeIn(),
                    exit = fadeOut(),
                    resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds()
                ),
            snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
            ) { paddingValues ->
            Column(
                modifier = Modifier
                    .background(backgroundAnimateableColor.value)
                    .fillMaxSize()
                    .padding(20.dp)
                    .padding(paddingValues)
            ){
                Row(modifier = Modifier
                    .fillMaxWidth()
                    .padding(8.dp),
                    horizontalArrangement = Arrangement.SpaceBetween
                ){
                    Utils.noteColors.forEach{ color ->
                        val colorInt = color.toArgb()
                        Box(
                            modifier = Modifier
                                .size(50.dp)
                                .shadow(15.dp, CircleShape)
                                .clip(CircleShape)
                                .background(color)
                                .border(
                                    width = 3.dp,
                                    color = if (backgroundAnimateableColor.value.toArgb() == colorInt) {
                                        Color.Black
                                    } else Color.Transparent,
                                    shape = CircleShape
                                )
                                .clickable {
                                    scope.launch {
                                        backgroundAnimateableColor.animateTo(
                                            targetValue = Color(colorInt),
                                            animationSpec = tween(
                                                delayMillis = 300
                                            )
                                        )
                                    }
                                    viewModel.onEvent(AddEditNoteEvent.ChangeColor(colorInt))
                                }
                        )
                    }
                }
                Spacer(modifier = Modifier.height(16.dp))
                TrasnparentHintTextField(
                    text = titleState.text,
                    hint = titleState.hint,
                    onValueChange = {
                        viewModel.onEvent(AddEditNoteEvent.EnteredTitle(it))
                    },
                    onFocusChange = {
                        viewModel.onEvent(AddEditNoteEvent.IsFocusTitle(it))
                    },
                    isHintVisible = titleState.isHintVisible,
                    singleLine = true,
                    textStyle = MaterialTheme.typography.headlineSmall
                )
                Spacer(modifier = Modifier.height(16.dp))
                TrasnparentHintTextField(
                    text = contentState.text,
                    hint = contentState.hint,
                    onValueChange = {
                        viewModel.onEvent(AddEditNoteEvent.EnteredContent(it))
                    },
                    onFocusChange = {
                        viewModel.onEvent(AddEditNoteEvent.IsFocusContent(it))
                    },
                    isHintVisible = contentState.isHintVisible,
                    singleLine = false,
                    textStyle = MaterialTheme.typography.bodyLarge
                )
            }
        }
    }
}

修复方案

1. 区分新增/编辑场景的共享元素逻辑

新增笔记时没有对应的源元素,不应启用共享元素过渡。修改AddEditScreen的Scaffold modifier:

modifier = if (noteId != -1) {
    Modifier.sharedBounds(
        rememberSharedContentState(key = "note/$noteId"),
        animatedVisibilityScope = animatedVisibilityScope,
        enter = fadeIn(),
        exit = fadeOut(),
        resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds()
    )
} else {
    Modifier
},

2. 移除列表项外层干扰性的AnimatedVisibility

列表项的AnimatedVisibility使用全局列表非空作为条件,会批量触发动画干扰共享元素过渡,直接移除该包裹:

// NoteContent中的列表项部分修改为:
items(state.notes, key = { it.id ?: 0 }) { note ->
    NoteItem(
        note = note,
        modifier = Modifier
            .padding(4.dp)
            .fillMaxWidth()
            .animateItem(placementSpec = tween(durationMillis = 300))
            .sharedBounds(
                rememberSharedContentState(key = "note/${note.id}"),
                animatedVisibilityScope = animatedVisibilityScope,
                enter = fadeIn(),
                exit = fadeOut(),
                resizeMode = SharedTransitionScope.ResizeMode.ScaleToBounds()
            ),
        onDeleteClick = { /* 原有逻辑不变 */ },
        onClick = { /* 原有逻辑不变 */ }
    )
}

3. 确保共享元素Key的稳定性

  • 删除恢复笔记时,保证笔记的id不被重新生成,维持note/${note.id}的一致性
  • 搜索时,确保过滤后的笔记列表中,保留原有的id,避免Key失效

4. 确认TransitionScope的正确性

确保sharedTransitionScope是在导航宿主(如NavHost所在的Composable)中通过rememberSharedTransitionScope()创建,并传递给所有需要共享过渡的子Composable,避免Scope因重组丢失状态。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 20:54:53