底部面板:BottomSheet

  1. 是什么
  2. 常用属性
  3. Persistent vs Modal 区别
  4. 案例
    1. AnimationController

是什么

在 Flutter 里,BottomSheet 是一个 从屏幕底部滑出的面板。常见有两种用法:

  1. Persistent BottomSheet(持久化底部栏)

    • 使用 Scaffold.of(context).showBottomSheet()ScaffoldState.showBottomSheet() 创建。
    • 一旦显示,会固定在屏幕底部,不会遮罩背景。
    • 常用于 持久显示某些操作按钮或输入区域
  2. Modal BottomSheet(模态底部弹窗)

    • 使用 showModalBottomSheet() 创建。
    • 遮罩背景,阻止用户点击弹窗之外区域。
    • 适合 临时选择或操作界面

BottomSheet 组件本身是一个 抽象类,通常我们用 Scaffold 提供的方法 来实例化它。

常用属性

如果你直接使用 BottomSheet Widget(而不是 showModalBottomSheet),可以配置:

属性 含义
onClosing 持久化 BottomSheet 关闭时回调
builder 返回 BottomSheet 内容的 Widget
enableDrag 是否允许拖拽关闭
backgroundColor 背景颜色
elevation 阴影高度
shape 弹窗形状(圆角等)
clipBehavior 裁剪行为
animationController 自定义动画控制器

Persistent vs Modal 区别

特性 Persistent Modal
背景遮罩
阻止底层交互
手动关闭
常用场景 底部操作栏、工具栏 临时选择界面、弹窗
  • Persistent BottomSheet → 固定在底部,可一直显示,不阻塞交互
  • Modal BottomSheet → 模态弹窗,覆盖底部,阻止背景交互,适合选择/操作
  • showModalBottomSheet() 是最常用的方式,底层就是 BottomSheet

案例

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(body: const BottomSheetDirectDemo()),
    );
  }
}

class BottomSheetDirectDemo extends StatefulWidget {
  const BottomSheetDirectDemo({super.key});
  @override
  State<BottomSheetDirectDemo> createState() => _BottomSheetDirectDemoState();
}

class _BottomSheetDirectDemoState extends State<BottomSheetDirectDemo>
    with SingleTickerProviderStateMixin {
  PersistentBottomSheetController? _controller;
  late AnimationController _animationController;
  String _result = "未选择";

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _showBottomSheet() {
    _controller = Scaffold.of(context).showBottomSheet((context) {
      return BottomSheet(
        onClosing: () {},
        animationController: _animationController,
        enableDrag: true,
        showDragHandle: true,
        backgroundColor: Colors.transparent,
        builder: (context) {
          return Container(
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
              boxShadow: const [
                BoxShadow(color: Colors.black26, blurRadius: 8),
              ],
            ),
            padding: const EdgeInsets.all(16),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  "请选择一个选项",
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 10),
                ListView.builder(
                  shrinkWrap: true,
                  itemCount: 5,
                  itemBuilder: (context, index) {
                    return ListTile(
                      title: Text("选项 ${index + 1}"),
                      onTap: () {
                        setState(() {
                          _result = "选项 ${index + 1}";
                        });
                        // 点击选项时,关闭弹窗
                        _controller?.close();
                      },
                    );
                  },
                ),
                const SizedBox(height: 10),
                Row(
                  mainAxisAlignment: MainAxisAlignment.end,
                  children: [
                    TextButton(
                      onPressed: () {
                        // 关闭弹窗
                        _controller?.close();
                      },
                      child: const Text("取消"),
                    ),
                    TextButton(
                      onPressed: () {
                        // 关闭弹窗
                        _controller?.close();
                      },
                      child: const Text("确认"),
                    ),
                  ],
                ),
              ],
            ),
          );
        },
      );
    });

    _controller?.closed.then((_) {
      debugPrint("BottomSheet 已关闭,选择结果:$_result");
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("BottomSheet 直接使用 Demo")),
      body: Builder(
        builder: (context) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text("选择结果:$_result", style: const TextStyle(fontSize: 18)),
                const SizedBox(height: 20),
                ElevatedButton(
                  onPressed: _showBottomSheet,
                  child: const Text("打开 BottomSheet"),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}
  1. 直接使用 BottomSheet,而不是 showModalBottomSheet
  2. 动画控制:通过 _animationController 自定义动画。
  3. 拖拽关闭enableDrag: true,并显示拖拽手柄 showDragHandle: true
  4. 圆角 + 阴影:通过 Container + BoxDecoration 设置。
  5. 确认/取消按钮:自定义按钮,不会影响 BottomSheet 的关闭逻辑。
  6. 监听关闭_controller?.closed.then(...) 可以在关闭时获取状态。

AnimationController

AnimationController 是一个 特殊的 Animation 对象,继承自 Animation<double>。它的常用功能如下:

_controller = AnimationController(
  vsync: this, // 避免屏幕不显示时仍然消耗资源
  duration: const Duration(seconds: 2), // 动画时长
);

// 启动动画(从 0 → 1)
_controller.forward();

// 倒放动画(从 1 → 0)
_controller.reverse();

// 无限重复(可选是否反向)
_controller.repeat(reverse: true);

// 立即停止
_controller.stop();

在 Flutter 里,BottomSheet 默认就是自带动画的,如果你不给它传入 animationController,它会自动内部创建一个。

  • BottomSheet 本质上是一个 可动画展开/收起的面板
  • 如果你 不传 animationController,它会用 ScaffoldState 内部的 _showBottomSheet 方法来创建一个 AnimationController,并且自己管理它的生命周期。
  • 如果你 手动传入 animationController,那么 BottomSheet 就会用你提供的控制器。此时 生命周期管理也需要你来处理(比如 dispose)。

×

喜欢就点赞,疼爱就打赏