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

3.2 按钮

📚 章节概览

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

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

🎯 核心知识点

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: 使用 GestureDetectorInkWell 包裹:

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 - 根据状态设置样式

状态管理

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

🔗 相关资源


相关推荐
●VON20 小时前
Flutter 与鸿蒙深度整合:如何实现原生功能调用
flutter·华为·harmonyos
A懿轩A1 天前
【2025版 OpenHarmony】GitCode 口袋工具 v1.0.3:Flutter + HarmonyOS 深色模式全面启用
flutter·harmonyos·openharmony·gitcode·开源鸿蒙
食品一少年1 天前
【Day7-10】开源鸿蒙Flutter 常用组件封装实战(2)
flutter·华为·harmonyos
谢斯2 天前
编译AppFlowy
flutter
灰灰勇闯IT2 天前
Flutter×鸿蒙深度融合指南:从跨端适配到分布式能力落地(2025最新实战)
分布式·flutter·harmonyos
x.Jessica2 天前
关于Flutter在Windows上开发的基本配置时遇到的问题及解决方法
windows·flutter
名字被你们想完了2 天前
flutter 封装一个 tab
flutter
AiFlutter2 天前
Flutter实现手电筒亮度修改
flutter
食品一少年2 天前
【Day7-10】开源鸿蒙之Flutter 的自定义组件封装(1)
flutter·开源·harmonyos
勇气要爆发2 天前
【第五阶段—高级特性和架构】第六章:自定义Widget开发指南
flutter