如何在Jetpack Compose中实现网格项全屏宽度的动画切换?
实现网格项动画切换全屏大图方案
核心思路
通过状态控制每个网格项的展开/折叠状态,结合AnimatedContent处理布局切换,同时利用Modifier的动画属性(如animateContentSize、animateOffset)实现从网格位置平滑过渡到全屏宽度的效果。用Box容器将网格布局和展开的大图内容分层,确保展开时大图显示在最上层。
代码实现示例
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp // 模拟网格项数据 data class GridItem(val id: Int, val title: String, val imageUrl: String) @Composable fun ExpandableGridDemo() { val gridItems = remember { List(12) { GridItem(it, "Item $it", "https://picsum.photos/800/600?random=$it") } } // 记录当前展开的项ID,null表示无展开项 var expandedItemId by remember { mutableStateOf<Int?>(null) } // 记录展开项的初始位置(用于动画过渡) var itemInitialOffset by remember { mutableStateOf(IntOffset.Zero) } Box(modifier = Modifier.fillMaxSize()) { // 网格布局 LazyVerticalGrid( columns = GridCells.Fixed(2), modifier = Modifier.fillMaxSize(), contentPadding = 16.dp ) { items(gridItems) { item -> AnimatedContent( targetState = expandedItemId == item.id, transitionSpec = { spring(stiffness = Spring.StiffnessMediumLow) } ) { isExpanded -> if (isExpanded) { // 展开时显示占位(避免网格布局跳动) Box( modifier = Modifier .padding(8.dp) .fillMaxWidth() .animateContentSize() ) } else { // 普通网格项按钮 Button( onClick = { expandedItemId = item.id }, modifier = Modifier .padding(8.dp) .fillMaxWidth() .onGloballyPositioned { coordinates -> // 获取按钮在窗口中的位置,用于展开动画的起始点 val bounds = coordinates.boundsInWindow() itemInitialOffset = IntOffset(bounds.left.toInt(), bounds.top.toInt()) } ) { Text(item.title) } } } } } // 展开的全屏大图内容 expandedItemId?.let { id -> val item = gridItems.first { it.id == id } val backgroundColor by animateColorAsState( targetValue = Color.Black, animationSpec = spring(stiffness = Spring.StiffnessMediumLow) ) AnimatedContent( targetState = true, modifier = Modifier.fillMaxSize() ) { Box( modifier = Modifier .background(backgroundColor) .clickable { expandedItemId = null } .fillMaxSize(), contentAlignment = Alignment.Center ) { Image( painter = androidx.compose.ui.res.painterResource(id = androidx.compose.ui.R.drawable.ic_launcher_foreground), // 替换为实际图片加载逻辑 contentDescription = item.title, modifier = Modifier .fillMaxWidth() .padding(16.dp) ) Text( text = "点击返回", color = Color.White, modifier = Modifier .align(Alignment.TopEnd) .padding(16.dp) ) } } } } }
关键细节说明
- 层级管理:用
Box将网格和展开内容叠加,展开的大图会自动覆盖在网格上方,无需额外设置层级。 - 尺寸动画:
AnimatedContent配合animateContentSize实现网格项占位的平滑尺寸变化,避免布局跳动;展开的大图直接用fillMaxWidth()实现全屏宽度。 - 位置过渡(可选):如果需要从按钮位置平滑放大到全屏,可以在展开的
Image上添加animateOffset,结合itemInitialOffset实现位置动画,增强视觉连贯性。 - 状态控制:通过
expandedItemId控制单个项的展开/折叠,确保同一时间只有一个项处于全屏状态。
内容的提问来源于stack exchange,提问作者Valeriya
相关产品推荐
相关产品推荐

