第3章:基础组件 —— 3.2 按钮

3.2 按钮

📚 章节概览

按钮是用户交互的核心组件。Flutter 的 Material 库提供了多种按钮组件,本章节将学习:

  • ElevatedButton - 漂浮按钮(带阴影和背景)
  • TextButton - 文本按钮(透明背景)
  • OutlinedButton - 边框按钮
  • IconButton - 图标按钮
  • 带图标的按钮 - .icon 构造函数
  • 按钮样式自定义 - ButtonStyle 和 styleFrom
  • 按钮状态管理 - 启用、禁用、加载中

🎯 核心知识点

Material 按钮的共同特点

所有 Material 库中的按钮都有以下共同点:

  1. 水波动画:点击时会出现水波扩散的涟漪动画
  2. onPressed 回调 :设置点击事件,为 null 时按钮禁用
  3. 继承自 RawMaterialButton:大多数属性都相同
dart 复制代码
// 启用状态
ElevatedButton(
  onPressed: () {
    print('按钮被点击');
  },
  child: Text('启用'),
)

// 禁用状态
ElevatedButton(
  onPressed: null,  // null = 禁用
  child: Text('禁用'),
)

1️⃣ ElevatedButton(漂浮按钮)

特点

  • 默认外观:带阴影和背景色(主题色)
  • 交互效果:按下时阴影变大
  • 适用场景:主要操作、强调的按钮

基本用法

dart 复制代码
ElevatedButton(
  onPressed: () {
    print('ElevatedButton 被点击');
  },
  child: Text('Normal'),
)

自定义样式

dart 复制代码
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.red,        // 背景色
    foregroundColor: Colors.white,     // 前景色(文字/图标)
    elevation: 5,                      // 阴影高度
    shadowColor: Colors.black,         // 阴影颜色
    padding: EdgeInsets.symmetric(
      horizontal: 24,
      vertical: 12,
    ),
    shape: RoundedRectangleBorder(      // 形状
      borderRadius: BorderRadius.circular(8),
    ),
  ),
  child: Text('自定义样式'),
)

2️⃣ TextButton(文本按钮)

特点

  • 默认外观:透明背景,无阴影
  • 交互效果:按下时出现背景色
  • 适用场景:次要操作、对话框按钮

基本用法

dart 复制代码
TextButton(
  onPressed: () {
    print('TextButton 被点击');
  },
  child: Text('Normal'),
)

常见用例:对话框

dart 复制代码
AlertDialog(
  title: Text('提示'),
  content: Text('确定要删除吗?'),
  actions: [
    TextButton(
      onPressed: () => Navigator.pop(context),
      child: Text('取消'),
    ),
    TextButton(
      onPressed: () {
        // 执行删除
        Navigator.pop(context);
      },
      child: Text('确定', style: TextStyle(color: Colors.red)),
    ),
  ],
)

3️⃣ OutlinedButton(边框按钮)

特点

  • 默认外观:带边框,透明背景,无阴影
  • 交互效果:按下时边框变亮,出现背景和阴影
  • 适用场景:中等强调的操作

基本用法

dart 复制代码
OutlinedButton(
  onPressed: () {
    print('OutlinedButton 被点击');
  },
  child: Text('Normal'),
)

自定义边框

dart 复制代码
OutlinedButton(
  onPressed: () {},
  style: OutlinedButton.styleFrom(
    side: BorderSide(
      color: Colors.red,   // 边框颜色
      width: 2,            // 边框宽度
    ),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(20),
    ),
  ),
  child: Text('自定义边框'),
)

4️⃣ IconButton(图标按钮)

特点

  • 默认外观:只包含图标,无背景
  • 交互效果:点击时出现圆形背景
  • 适用场景:工具栏、AppBar、紧凑布局

基本用法

dart 复制代码
IconButton(
  icon: Icon(Icons.thumb_up),
  onPressed: () {
    print('IconButton 被点击');
  },
  tooltip: '点赞',  // 长按显示的提示
)

实用示例:收藏功能

dart 复制代码
class FavoriteButton extends StatefulWidget {
  @override
  _FavoriteButtonState createState() => _FavoriteButtonState();
}

class _FavoriteButtonState extends State<FavoriteButton> {
  bool _isFavorite = false;

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(
        _isFavorite ? Icons.favorite : Icons.favorite_border,
      ),
      color: _isFavorite ? Colors.red : Colors.grey,
      onPressed: () {
        setState(() {
          _isFavorite = !_isFavorite;
        });
      },
      tooltip: _isFavorite ? '取消收藏' : '收藏',
    );
  }
}

5️⃣ 带图标的按钮

.icon 构造函数

所有主要按钮类型都提供了 .icon 构造函数,可以轻松创建带图标的按钮。

dart 复制代码
// ElevatedButton.icon
ElevatedButton.icon(
  icon: Icon(Icons.send),
  label: Text('发送'),
  onPressed: () {},
)

// TextButton.icon
TextButton.icon(
  icon: Icon(Icons.info),
  label: Text('详情'),
  onPressed: () {},
)

// OutlinedButton.icon
OutlinedButton.icon(
  icon: Icon(Icons.add),
  label: Text('添加'),
  onPressed: () {},
)

图标位置

默认情况下,图标在左侧。如果需要图标在右侧,可以自定义布局:

dart 复制代码
ElevatedButton(
  onPressed: () {},
  child: Row(
    mainAxisSize: MainAxisSize.min,
    children: [
      Text('下载'),
      SizedBox(width: 8),
      Icon(Icons.download),
    ],
  ),
)

🎨 按钮样式系统

ButtonStyle vs styleFrom

Flutter 提供了两种方式自定义按钮样式:

方法1:styleFrom(推荐)
dart 复制代码
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,
    foregroundColor: Colors.white,
    elevation: 5,
    padding: EdgeInsets.all(16),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(8),
    ),
  ),
  child: Text('按钮'),
)
方法2:ButtonStyle(更灵活)
dart 复制代码
ElevatedButton(
  onPressed: () {},
  style: ButtonStyle(
    backgroundColor: MaterialStateProperty.all(Colors.blue),
    foregroundColor: MaterialStateProperty.all(Colors.white),
    elevation: MaterialStateProperty.all(5),
    padding: MaterialStateProperty.all(
      EdgeInsets.all(16),
    ),
    shape: MaterialStateProperty.all(
      RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(8),
      ),
    ),
  ),
  child: Text('按钮'),
)

MaterialStateProperty

ButtonStyle 使用 MaterialStateProperty 来根据按钮状态设置不同的样式。

dart 复制代码
ElevatedButton(
  onPressed: () {},
  style: ButtonStyle(
    backgroundColor: MaterialStateProperty.resolveWith<Color>(
      (Set<MaterialState> states) {
        if (states.contains(MaterialState.pressed)) {
          return Colors.red;      // 按下时红色
        }
        if (states.contains(MaterialState.hovered)) {
          return Colors.orange;   // 悬停时橙色
        }
        if (states.contains(MaterialState.disabled)) {
          return Colors.grey;     // 禁用时灰色
        }
        return Colors.blue;       // 默认蓝色
      },
    ),
  ),
  child: Text('状态按钮'),
)

常见状态

状态 说明
MaterialState.pressed 按下状态
MaterialState.hovered 鼠标悬停(Web/Desktop)
MaterialState.focused 获得焦点
MaterialState.disabled 禁用状态
MaterialState.selected 选中状态

📐 按钮尺寸控制

方法1:padding

dart 复制代码
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    padding: EdgeInsets.symmetric(
      horizontal: 32,
      vertical: 20,
    ),
  ),
  child: Text('大按钮'),
)

方法2:minimumSize

dart 复制代码
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    minimumSize: Size(200, 50),  // 最小宽200,高50
  ),
  child: Text('固定尺寸'),
)

方法3:外层包裹 SizedBox

dart 复制代码
// 全宽按钮
SizedBox(
  width: double.infinity,
  child: ElevatedButton(
    onPressed: () {},
    child: Text('全宽按钮'),
  ),
)

// 固定宽度按钮
SizedBox(
  width: 200,
  child: ElevatedButton(
    onPressed: () {},
    child: Text('200宽按钮'),
  ),
)

🔄 按钮状态管理

启用/禁用状态

dart 复制代码
class ToggleButton extends StatefulWidget {
  @override
  _ToggleButtonState createState() => _ToggleButtonState();
}

class _ToggleButtonState extends State<ToggleButton> {
  bool _isEnabled = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Switch(
          value: _isEnabled,
          onChanged: (value) {
            setState(() {
              _isEnabled = value;
            });
          },
        ),
        ElevatedButton(
          onPressed: _isEnabled ? () {} : null,  // null = 禁用
          child: Text(_isEnabled ? '启用' : '禁用'),
        ),
      ],
    );
  }
}

加载状态

dart 复制代码
class LoadingButton extends StatefulWidget {
  @override
  _LoadingButtonState createState() => _LoadingButtonState();
}

class _LoadingButtonState extends State<LoadingButton> {
  bool _isLoading = false;

  Future<void> _handleSubmit() async {
    setState(() {
      _isLoading = true;
    });
    
    // 模拟网络请求
    await Future.delayed(Duration(seconds: 2));
    
    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _isLoading ? null : _handleSubmit,
      child: _isLoading
          ? SizedBox(
              height: 20,
              width: 20,
              child: CircularProgressIndicator(
                strokeWidth: 2,
                valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
              ),
            )
          : Text('提交'),
    );
  }
}

🎨 高级样式技巧

1. 渐变背景按钮

dart 复制代码
Ink(
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: [Colors.blue, Colors.purple],
    ),
    borderRadius: BorderRadius.circular(8),
  ),
  child: ElevatedButton(
    onPressed: () {},
    style: ElevatedButton.styleFrom(
      backgroundColor: Colors.transparent,
      shadowColor: Colors.transparent,
    ),
    child: Text('渐变按钮'),
  ),
)

2. 圆形按钮

dart 复制代码
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    shape: CircleBorder(),
    padding: EdgeInsets.all(20),
  ),
  child: Icon(Icons.favorite),
)

3. 描边文字按钮

dart 复制代码
TextButton(
  onPressed: () {},
  child: Text(
    '描边文字',
    style: TextStyle(
      foreground: Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2
        ..color = Colors.blue,
    ),
  ),
)

4. 不同圆角的按钮

dart 复制代码
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.only(
        topLeft: Radius.circular(20),
        topRight: Radius.circular(5),
        bottomLeft: Radius.circular(5),
        bottomRight: Radius.circular(20),
      ),
    ),
  ),
  child: Text('不规则圆角'),
)

💡 最佳实践

1. 按钮选择指南

场景 推荐按钮 理由
主要操作(如提交、保存) ElevatedButton 视觉突出,强调重要性
次要操作(如取消、返回) TextButton 低调,不抢主按钮风头
中等强调(如添加、编辑) OutlinedButton 有边界感,但不过于突出
工具栏、列表操作 IconButton 节省空间,简洁

2. 按钮组合布局

dart 复制代码
// 对话框按钮组
Row(
  mainAxisAlignment: MainAxisAlignment.end,
  children: [
    TextButton(
      onPressed: () => Navigator.pop(context),
      child: Text('取消'),
    ),
    SizedBox(width: 8),
    ElevatedButton(
      onPressed: () {
        // 确认操作
      },
      child: Text('确定'),
    ),
  ],
)

// 底部操作栏
Row(
  children: [
    Expanded(
      child: OutlinedButton(
        onPressed: () {},
        child: Text('取消'),
      ),
    ),
    SizedBox(width: 16),
    Expanded(
      child: ElevatedButton(
        onPressed: () {},
        child: Text('确定'),
      ),
    ),
  ],
)

3. 使用全局主题

dart 复制代码
MaterialApp(
  theme: ThemeData(
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
        elevation: 5,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
    ),
    textButtonTheme: TextButtonThemeData(
      style: TextButton.styleFrom(
        foregroundColor: Colors.blue,
      ),
    ),
  ),
  home: MyHomePage(),
)

4. 避免常见错误

❌ 错误:按钮文字过长导致溢出

dart 复制代码
// 不好
ElevatedButton(
  onPressed: () {},
  child: Text('这是一个非常非常非常长的按钮文字'),
)

✅ 正确:限制文字或使用自适应布局

dart 复制代码
// 好
ElevatedButton(
  onPressed: () {},
  child: Text(
    '这是一个非常非常非常长的按钮文字',
    overflow: TextOverflow.ellipsis,
    maxLines: 1,
  ),
)

🤔 常见问题(FAQ)

Q1: 如何去除按钮的水波动画?

A: 使用 splashFactory 设置为 NoSplash.splashFactory:

dart 复制代码
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    splashFactory: NoSplash.splashFactory,  // 禁用水波
  ),
  child: Text('无水波'),
)

Q2: 如何让按钮宽度自适应内容?

A: 默认按钮就是自适应内容的。如果被外层组件约束,可以使用 Wrap 或设置 mainAxisSize:

dart 复制代码
Row(
  mainAxisSize: MainAxisSize.min,  // 自适应
  children: [
    ElevatedButton(
      onPressed: () {},
      child: Text('短'),
    ),
  ],
)

Q3: 为什么我的按钮点击没反应?

A: 检查以下几点:

  1. onPressed 是否为 null(会导致禁用)
  2. 是否被其他 Widget 遮挡(如透明的 Overlay)
  3. 是否在 IgnorePointer 内部
dart 复制代码
// 检查是否禁用
ElevatedButton(
  onPressed: null,  // ❌ 禁用状态
  child: Text('按钮'),
)

// 正确
ElevatedButton(
  onPressed: () {
    print('点击了');
  },  // ✅ 启用状态
  child: Text('按钮'),
)

Q4: 如何实现按钮防抖(防止重复点击)?

A: 使用状态管理或延迟:

dart 复制代码
class DebounceButton extends StatefulWidget {
  @override
  _DebounceButtonState createState() => _DebounceButtonState();
}

class _DebounceButtonState extends State<DebounceButton> {
  bool _isProcessing = false;

  Future<void> _handleClick() async {
    if (_isProcessing) return;  // 防止重复点击
    
    setState(() {
      _isProcessing = true;
    });
    
    // 执行操作
    await Future.delayed(Duration(seconds: 2));
    
    setState(() {
      _isProcessing = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _isProcessing ? null : _handleClick,
      child: Text(_isProcessing ? '处理中...' : '提交'),
    );
  }
}

Q5: 如何实现按钮长按效果?

A: 使用 GestureDetector 或 InkWell 包裹:

dart 复制代码
GestureDetector(
  onLongPress: () {
    print('长按了');
  },
  child: ElevatedButton(
    onPressed: () {
      print('普通点击');
    },
    child: Text('长按试试'),
  ),
)

🎯 跟着做练习

练习1:实现一个全宽的提交按钮

目标: 创建一个全宽的绿色提交按钮,带圆角,点击后显示 SnackBar

步骤:

  1. 使用 SizedBox 设置全宽
  2. 使用 ElevatedButton.styleFrom 自定义颜色和圆角
  3. 点击时显示 ScaffoldMessenger.of(context).showSnackBar

💡 查看答案

dart 复制代码
class FullWidthButton extends StatelessWidget {
  const FullWidthButton({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      child: ElevatedButton(
        onPressed: () {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('提交成功!'),
              duration: Duration(seconds: 2),
            ),
          );
        },
        style: ElevatedButton.styleFrom(
          backgroundColor: Colors.green,
          foregroundColor: Colors.white,
          padding: const EdgeInsets.symmetric(vertical: 16),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
        child: const Text(
          '提交',
          style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
        ),
      ),
    );
  }
}

练习2:实现一个计数按钮

目标: 按钮显示点击次数,每次点击次数+1,点击5次后禁用

步骤:

  1. 使用 StatefulWidget 管理计数状态
  2. 在 onPressed 中更新计数
  3. 当计数达到5时,onPressed 设为 null

💡 查看答案

dart 复制代码
class CounterButton extends StatefulWidget {
  const CounterButton({super.key});

  @override
  State<CounterButton> createState() => _CounterButtonState();
}

class _CounterButtonState extends State<CounterButton> {
  int _count = 0;
  final int _maxCount = 5;

  void _incrementCounter() {
    if (_count < _maxCount) {
      setState(() {
        _count++;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ElevatedButton(
          onPressed: _count < _maxCount ? _incrementCounter : null,
          child: Text('点击次数: $_count'),
        ),
        const SizedBox(height: 8),
        if (_count >= _maxCount)
          const Text(
            '已达到最大点击次数',
            style: TextStyle(color: Colors.red, fontSize: 12),
          ),
        if (_count < _maxCount)
          Text(
            '还可以点击 ${_maxCount - _count} 次',
            style: const TextStyle(color: Colors.grey, fontSize: 12),
          ),
      ],
    );
  }
}

练习3:实现一个多功能工具栏

目标: 创建一行图标按钮,包括:编辑、删除、分享、更多

步骤:

  1. 使用 Row 布局
  2. 创建4个 IconButton
  3. 每个按钮点击时打印不同的信息

💡 查看答案

dart 复制代码
class Toolbar extends StatelessWidget {
  final VoidCallback? onEdit;
  final VoidCallback? onDelete;
  final VoidCallback? onShare;
  final VoidCallback? onMore;

  const Toolbar({
    super.key,
    this.onEdit,
    this.onDelete,
    this.onShare,
    this.onMore,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
        child: Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            IconButton(
              icon: const Icon(Icons.edit),
              onPressed: onEdit ??
                  () {
                    print('编辑');
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('编辑功能')),
                    );
                  },
              tooltip: '编辑',
              color: Colors.blue,
            ),
            IconButton(
              icon: const Icon(Icons.delete),
              onPressed: onDelete ??
                  () {
                    print('删除');
                    showDialog(
                      context: context,
                      builder: (context) => AlertDialog(
                        title: const Text('确认'),
                        content: const Text('确定要删除吗?'),
                        actions: [
                          TextButton(
                            onPressed: () => Navigator.pop(context),
                            child: const Text('取消'),
                          ),
                          TextButton(
                            onPressed: () {
                              print('已删除');
                              Navigator.pop(context);
                            },
                            child: const Text('确定'),
                          ),
                        ],
                      ),
                    );
                  },
              tooltip: '删除',
              color: Colors.red,
            ),
            IconButton(
              icon: const Icon(Icons.share),
              onPressed: onShare ??
                  () {
                    print('分享');
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('分享功能')),
                    );
                  },
              tooltip: '分享',
              color: Colors.green,
            ),
            IconButton(
              icon: const Icon(Icons.more_vert),
              onPressed: onMore ??
                  () {
                    print('更多');
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('更多功能')),
                    );
                  },
              tooltip: '更多',
            ),
          ],
        ),
      ),
    );
  }
}

// 使用示例
class ToolbarDemo extends StatelessWidget {
  const ToolbarDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Toolbar(
        onEdit: () => print('自定义编辑'),
        onDelete: () => print('自定义删除'),
      ),
    );
  }
}

📋 小结

核心要点

按钮类型 默认外观 适用场景
ElevatedButton 带阴影和背景 主要操作
TextButton 透明背景 次要操作、对话框
OutlinedButton 边框,透明背景 中等强调
IconButton 仅图标 工具栏、紧凑布局

样式自定义

  • 推荐 :styleFrom - 简洁直观
  • 高级 :ButtonStyle + MaterialStateProperty - 根据状态设置样式

状态管理

  • 启用/禁用 :onPressed 为 null 时禁用
  • 加载状态 :显示 CircularProgressIndicator + 禁用按钮

🔗 相关资源


相关推荐
梦想的颜色5 小时前
【编程实战】AI 时代 APP 开发全栈硬核指南:技术选型 + AI 架构 + 模型落地全维度决策
python·flutter·react native·react.js·ai·桌面应用·milvus
yuanlaile1 天前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
恋猫de小郭1 天前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白1 天前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
陆断枫2 天前
Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"
flutter
程序员老刘2 天前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端
m0_738185822 天前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙
恋猫de小郭2 天前
Flutter + EmbeddingGemma 2,谷歌发布完全端侧的 AI Edge Foresight
android·前端·flutter
君赏3 天前
基于 FlutterPatch 快速接入 Flutter 热更新
flutter
律宏阔3 天前
Dart FFI 内存管理:用 using + Arena 替代嵌套 try-finally
前端·flutter