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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 04:09:28