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

如何在Jetpack Compose中实现FAB展开为Sheet的动效?

实现Jetpack Compose Material3风格FAB展开为选项Sheet

要实现你要的FAB展开为底部选项Sheet的效果,核心是用BottomSheetScaffold配合状态动画,以下是完整的实现代码:

import androidx.compose.animation.animateDpAsState
import androidx.compose.animation.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberStandardBottomSheetState
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.draw.rotate
import androidx.compose.ui.unit.dp

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FabToSheetDemo() {
    // 控制Sheet展开/收起的状态
    var isSheetExpanded by remember { mutableStateOf(false) }
    // Sheet状态,设置初始隐藏
    val sheetState = rememberStandardBottomSheetState(
        initialValue = if (isSheetExpanded) SheetState.Expanded else SheetState.Hidden
    )
    val scaffoldState = rememberBottomSheetScaffoldState(bottomSheetState = sheetState)

    // FAB旋转动画:展开时旋转180度(加号变叉号)
    val fabRotation by animateFloatAsState(
        targetValue = if (isSheetExpanded) 180f else 0f,
        animationSpec = tween(durationMillis = 300), label = ""
    )
    // Sheet高度过渡动画
    val sheetPeekHeight by animateDpAsState(
        targetValue = if (isSheetExpanded) 200.dp else 0.dp,
        animationSpec = tween(durationMillis = 300), label = ""
    )

    BottomSheetScaffold(
        scaffoldState = scaffoldState,
        sheetPeekHeight = sheetPeekHeight,
        sheetContent = {
            // Sheet内的选项列表
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(16.dp)
            ) {
                OptionItem(icon = Icons.Default.Upload, text = "上传文件")
                OptionItem(icon = Icons.Default.Share, text = "分享内容")
                OptionItem(icon = Icons.Default.Person, text = "个人中心")
            }
        },
        floatingActionButton = {
            FloatingActionButton(
                onClick = {
                    isSheetExpanded = !isSheetExpanded
                },
                modifier = Modifier.rotate(fabRotation)
            ) {
                Icon(
                    imageVector = if (isSheetExpanded) Icons.Default.Close else Icons.Default.Add,
                    contentDescription = if (isSheetExpanded) "关闭" else "打开选项"
                )
            }
        }
    ) { innerPadding ->
        // 主内容区域,这里可以替换成你的页面内容
        Scaffold(modifier = Modifier.padding(innerPadding)) {
            Column(
                modifier = Modifier.fillMaxWidth(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                Text(text = "主页面内容", style = MaterialTheme.typography.headlineMedium)
            }
        }
    }
}

@Composable
fun OptionItem(icon: androidx.compose.ui.graphics.vector.ImageVector, text: String) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        IconButton(onClick = { /* 处理选项点击 */ }) {
            Icon(imageVector = icon, contentDescription = text)
        }
        Text(text = text, style = MaterialTheme.typography.bodyLarge)
    }
}

关键实现细节:

  • BottomSheetScaffold:Material3官方提供的底部Sheet脚手架,天然支持Sheet与FAB的布局关联,避免手动处理层级问题。
  • 状态动画:
    • 用animateFloatAsState实现FAB图标的旋转过渡,让加号平滑转成叉号。
    • 用animateDpAsState控制Sheet的高度变化,实现从隐藏到展开的平滑过渡。
  • Sheet状态管理:通过isSheetExpanded变量切换Sheet的显示状态,配合rememberStandardBottomSheetState确保状态持久化。

你之前尝试的AnimatedContent切换FAB和Column的方式,问题在于没有利用BottomSheet的原生布局特性,导致动画和层级出现偏差,用上述方案可以完美匹配Material3官网的效果。

内容的提问来源于stack exchange,提问作者Chandresh Parmar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 02:57:36