在 Flutter 开发中,按钮是交互的核心载体(提交、取消、操作、跳转)。原生ElevatedButton/OutlinedButton/TextButton存在样式配置繁琐、状态管理分散(加载、禁用、点击态)、交互反馈单一等问题。
本文封装的CommonButtonWidget 通用按钮组件,整合 "多样式(纯色 / 渐变 / 边框 / 文字)+ 多状态(加载 / 禁用 / 点击态)+ 交互优化(防抖 / 长按 / 水波纹)+ 全样式自定义" 四大核心能力,一行代码调用,覆盖 95%+ 按钮使用场景。
一、核心优势
| 核心能力 | 解决痛点 | 核心价值 |
|---|---|---|
| 🎨 多样式自由切换 | 不同样式按钮需重复封装 | 支持纯色 / 渐变 / 边框 / 文字 4 种基础样式,参数一键切换,无需重复写布局 |
| 🚦 多状态智能适配 | 加载 / 禁用 / 点击态需手动判断 | 内置加载中、禁用、点击态、长按态样式,状态联动自动适配 |
| ⚡ 交互体验优化 | 快速点击重复触发、反馈不友好 | 内置防抖、长按回调、水波纹反馈,符合移动端交互规范 |
| 🛠️ 样式全自定义 | 原生按钮样式定制繁琐 | 圆角、高度、内边距、文本样式、加载动画均可配置,支持图标 + 文本组合 |
| 📱 适配性强 | 深色模式 / 全面屏适配复杂 | 自动适配深色模式,支持自定义水波纹颜色,点击区域符合人机规范 |
| 🎯 极简调用 | 原生按钮参数多、配置复杂 | 一行代码调用,默认配置覆盖 80% 场景,自定义配置灵活扩展 |
二、核心配置速览
| 配置分类 | 核心参数 | 类型 | 默认值 | 核心作用 |
|---|---|---|---|---|
| 必选配置 | text | String | -(必传) | 按钮文本 |
| onTap | VoidCallback | -(必传) | 点击回调 | |
| 样式配置 | buttonType | ButtonType | ButtonType.solid | 按钮类型(纯色 / 渐变 / 边框 / 文字) |
| bgColor | Color | Colors.blue | 纯色按钮背景色 | |
| gradient | Gradient? | null | 渐变按钮渐变(优先级高于 bgColor) | |
| borderColor | Color | Colors.blue | 边框 / 文字按钮边框 / 文本色 | |
| radius | double | 8.0 | 按钮圆角 | |
| height | double | 48.0 | 按钮高度(建议≥44px) | |
| textStyle | TextStyle | 16 号白色粗体 | 文本样式 | |
| 状态配置 | isLoading | bool | false | 是否加载中(自动禁用点击) |
| isDisabled | bool | false | 是否禁用 | |
| loadingText | String | "加载中..." | 加载中文本 | |
| loadingSize | double | 20.0 | 加载图标大小 | |
| 交互配置 | debounceDuration | Duration | 300ms | 防抖时长 |
| onLongPress | VoidCallback? | null | 长按回调 | |
| splashColor | Color? | null | 水波纹颜色 | |
| expand | bool | true | 是否宽度占满 | |
| 扩展配置 | prefixIcon/suffixIcon | Widget? | null | 前缀 / 后缀图标 |
| iconSize | double | 24.0 | 图标大小 | |
| adaptDarkMode | bool | true | 深色模式适配 |
三、完整代码(可直接复制使用)
dart
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
/// 按钮类型枚举
enum ButtonType {
solid, // 纯色按钮
gradient, // 渐变按钮
outline, // 边框按钮
text // 文字按钮
}
/// 通用按钮组件
class CommonButtonWidget extends StatefulWidget {
// 必选参数
final String text; // 按钮文本
final VoidCallback onTap; // 点击回调
// 样式配置
final ButtonType buttonType; // 按钮类型(默认纯色)
final Color bgColor; // 背景色(纯色按钮)
final Gradient? gradient; // 渐变(渐变按钮,优先级高于bgColor)
final Color borderColor; // 边框色(边框/文字按钮)
final double borderWidth; // 边框宽度(默认1px)
final double radius; // 圆角(默认8px)
final double height; // 按钮高度(默认48px)
final EdgeInsetsGeometry padding; // 内边距(默认水平16px)
final TextStyle textStyle; // 文本样式
final Color disabledColor; // 禁用背景色
final Color disabledTextColor; // 禁用文本色
// 状态配置
final bool isLoading; // 是否加载中(默认false)
final bool isDisabled; // 是否禁用(默认false)
final String loadingText; // 加载中文本(默认"加载中...")
final double loadingSize; // 加载图标大小(默认20px)
final Color loadingColor; // 加载图标颜色
// 交互配置
final Duration debounceDuration; // 防抖时长(默认300ms)
final VoidCallback? onLongPress; // 长按回调
final Color? splashColor; // 水波纹颜色
final bool enableFeedback; // 是否开启点击反馈(默认true)
final bool expand; // 是否宽度占满(默认true)
// 扩展配置
final Widget? prefixIcon; // 前缀图标
final Widget? suffixIcon; // 后缀图标
final double iconSize; // 图标大小(默认24px)
final double iconTextSpacing; // 图标与文本间距(默认8px)
final bool adaptDarkMode; // 适配深色模式(默认true)
const CommonButtonWidget({
super.key,
required this.text,
required this.onTap,
// 样式配置
this.buttonType = ButtonType.solid,
this.bgColor = Colors.blue,
this.gradient,
this.borderColor = Colors.blue,
this.borderWidth = 1.0,
this.radius = 8.0,
this.height = 48.0,
this.padding = const EdgeInsets.symmetric(horizontal: 16),
this.textStyle = const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w500),
this.disabledColor = const Color(0xFFE0E0E0),
this.disabledTextColor = const Color(0xFF999999),
// 状态配置
this.isLoading = false,
this.isDisabled = false,
this.loadingText = "加载中...",
this.loadingSize = 20.0,
this.loadingColor = Colors.white,
// 交互配置
this.debounceDuration = const Duration(milliseconds: 300),
this.onLongPress,
this.splashColor,
this.enableFeedback = true,
this.expand = true,
// 扩展配置
this.prefixIcon,
this.suffixIcon,
this.iconSize = 24.0,
this.iconTextSpacing = 8.0,
this.adaptDarkMode = true,
});
@override
State<CommonButtonWidget> createState() => _CommonButtonWidgetState();
}
class _CommonButtonWidgetState extends State<CommonButtonWidget> {
bool _isClicking = false; // 防抖标记
/// 深色模式颜色适配
Color _adaptDarkMode(Color lightColor, Color darkColor) {
if (!widget.adaptDarkMode) return lightColor;
return MediaQuery.platformBrightnessOf(context) == Brightness.dark
? darkColor
: lightColor;
}
/// 防抖点击处理(带异常捕获)
Future<void> _handleTap() async {
// 防抖/加载/禁用状态拦截
if (_isClicking || widget.isLoading || widget.isDisabled) return;
_isClicking = true;
try {
widget.onTap(); // 执行点击回调
} catch (e) {
// 全局异常捕获,避免按钮点击崩溃
EasyLoading.showError("操作失败:${e.toString()}");
debugPrint("按钮点击异常:$e");
} finally {
// 防抖延迟后重置标记
await Future.delayed(widget.debounceDuration);
if (mounted) {
setState(() => _isClicking = false);
}
}
}
/// 构建按钮背景/边框装饰
Decoration? _buildDecoration() {
final isDisabled = widget.isDisabled || widget.isLoading;
// 基础颜色适配(深色模式+禁用状态)
Color bgColor = _adaptDarkMode(widget.bgColor, Colors.blueAccent);
Color borderColor = _adaptDarkMode(widget.borderColor, Colors.blueAccent);
// 禁用状态颜色覆盖
if (isDisabled) {
bgColor = _adaptDarkMode(widget.disabledColor, const Color(0xFF444444));
borderColor = _adaptDarkMode(widget.disabledColor, const Color(0xFF555555));
}
switch (widget.buttonType) {
case ButtonType.solid:
return BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(widget.radius),
);
case ButtonType.gradient:
return BoxDecoration(
gradient: widget.gradient ?? LinearGradient(
colors: [bgColor, bgColor.withOpacity(0.8)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(widget.radius),
);
case ButtonType.outline:
return BoxDecoration(
border: Border.all(
color: borderColor,
width: isDisabled ? widget.borderWidth * 0.5 : widget.borderWidth,
),
borderRadius: BorderRadius.circular(widget.radius),
color: Colors.transparent,
);
case ButtonType.text:
return null; // 文字按钮无装饰
}
}
/// 构建按钮文本样式(适配状态+深色模式)
TextStyle _buildTextStyle() {
final isDisabled = widget.isDisabled || widget.isLoading;
Color textColor = widget.textStyle.color ?? Colors.white;
// 边框/文字按钮默认文本色适配
if (widget.buttonType == ButtonType.outline || widget.buttonType == ButtonType.text) {
textColor = _adaptDarkMode(widget.borderColor, Colors.blueAccent);
}
// 禁用状态文本色覆盖
if (isDisabled) {
textColor = _adaptDarkMode(widget.disabledTextColor, const Color(0xFF777777));
}
// 最终深色模式适配
textColor = _adaptDarkMode(textColor, widget.textStyle.color ?? Colors.white70);
return widget.textStyle.copyWith(
color: textColor,
fontSize: widget.textStyle.fontSize ?? 16,
fontWeight: widget.textStyle.fontWeight ?? FontWeight.w500,
decoration: isDisabled ? TextDecoration.none : widget.textStyle.decoration,
);
}
/// 构建按钮内容(图标+文本+加载动画组合)
Widget _buildButtonContent() {
final isLoading = widget.isLoading;
final displayText = isLoading ? widget.loadingText : widget.text;
final loadingColor = _adaptDarkMode(widget.loadingColor, Colors.white70);
List<Widget> contentWidgets = [];
// 加载中图标(优先级最高)
if (isLoading) {
contentWidgets.add(
SizedBox(
width: widget.loadingSize,
height: widget.loadingSize,
child: CircularProgressIndicator(
strokeWidth: 2,
color: loadingColor,
valueColor: AlwaysStoppedAnimation<Color>(loadingColor),
),
),
);
if (displayText.isNotEmpty) {
contentWidgets.add(SizedBox(width: widget.iconTextSpacing));
}
} else {
// 前缀图标(非加载状态显示)
if (widget.prefixIcon != null) {
contentWidgets.add(
SizedBox(
width: widget.iconSize,
height: widget.iconSize,
child: widget.prefixIcon,
),
);
contentWidgets.add(SizedBox(width: widget.iconTextSpacing));
}
}
// 按钮文本(支持空文本)
if (displayText.isNotEmpty) {
contentWidgets.add(
Expanded(
child: Text(
displayText,
style: _buildTextStyle(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
);
}
// 后缀图标(非加载状态显示)
if (!isLoading && widget.suffixIcon != null) {
contentWidgets.add(SizedBox(width: widget.iconTextSpacing));
contentWidgets.add(
SizedBox(
width: widget.iconSize,
height: widget.iconSize,
child: widget.suffixIcon,
),
);
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: contentWidgets,
);
}
@override
Widget build(BuildContext context) {
final isDisabled = widget.isDisabled || widget.isLoading;
// 水波纹颜色适配(默认使用背景色半透明)
final splashColor = widget.splashColor ?? _adaptDarkMode(
widget.bgColor.withOpacity(0.2),
Colors.blueAccent.withOpacity(0.3),
);
// 基础按钮容器(统一布局)
Widget buttonContainer = Container(
width: widget.expand ? double.infinity : null,
height: widget.height,
padding: widget.padding,
decoration: _buildDecoration(),
alignment: Alignment.center,
// 点击区域扩大(最小44x44,符合iOS人机规范)
constraints: BoxConstraints(
minWidth: 44,
minHeight: 44,
maxHeight: widget.height,
),
child: _buildButtonContent(),
);
// 交互层封装(区分水波纹/纯点击)
Widget button;
if (widget.buttonType != ButtonType.text && !isDisabled) {
// 带水波纹的点击(InkWell)
button = InkWell(
onTap: _handleTap,
onLongPress: isDisabled ? null : widget.onLongPress,
splashColor: splashColor,
highlightColor: Colors.transparent, // 去除高亮色,仅保留水波纹
borderRadius: BorderRadius.circular(widget.radius),
enableFeedback: widget.enableFeedback,
child: buttonContainer,
);
} else {
// 纯点击(GestureDetector)
button = GestureDetector(
onTap: _handleTap,
onLongPress: isDisabled ? null : widget.onLongPress,
behavior: HitTestBehavior.opaque,
enableFeedback: widget.enableFeedback && !isDisabled,
child: buttonContainer,
);
}
// 禁用状态透明度处理
if (isDisabled) {
button = Opacity(
opacity: 0.6,
child: button,
);
}
return button;
}
}
// pubspec.yaml依赖(如需使用EasyLoading)
/*
dependencies:
flutter:
sdk: flutter
flutter_easyloading: ^3.0.5
*/
四、四大高频场景示例
场景 1:提交按钮(纯色 + 加载态)
适用场景:表单提交、数据保存,需显示加载状态并禁用重复点击
dart
class SubmitButtonDemo extends StatefulWidget {
@override
State<SubmitButtonDemo> createState() => _SubmitButtonDemoState();
}
class _SubmitButtonDemoState extends State<SubmitButtonDemo> {
bool _isSubmitting = false;
// 模拟表单提交逻辑
Future<void> _submitForm() async {
setState(() => _isSubmitting = true);
try {
// 模拟接口请求(2秒)
await Future.delayed(const Duration(seconds: 2));
EasyLoading.showSuccess("提交成功!");
} catch (e) {
EasyLoading.showError("提交失败:$e");
} finally {
if (mounted) {
setState(() => _isSubmitting = false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("提交按钮示例")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 48),
child: CommonButtonWidget(
text: "提交表单",
onTap: _submitForm,
buttonType: ButtonType.solid,
bgColor: Colors.green, // 成功色
radius: 24, // 大圆角
height: 52, // 加高按钮
isLoading: _isSubmitting, // 加载状态联动
loadingText: "提交中...", // 加载文本
loadingColor: Colors.white, // 加载图标颜色
disabledColor: Colors.grey[300]!, // 禁用背景色
debounceDuration: const Duration(milliseconds: 500), // 加长防抖
),
),
);
}
}
场景 2:渐变按钮(图标 + 文本)
适用场景:支付、主要操作按钮,需视觉突出并带图标增强识别
dart
class GradientButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("渐变按钮示例")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 48),
child: CommonButtonWidget(
text: "立即支付",
onTap: () => EasyLoading.showToast("支付功能已触发"),
buttonType: ButtonType.gradient,
// 橙红渐变
gradient: const LinearGradient(
colors: [Colors.orange, Colors.redAccent],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
radius: 8,
height: 48,
prefixIcon: const Icon(Icons.payment, color: Colors.white), // 支付图标
iconSize: 20, // 小图标
iconTextSpacing: 10, // 加大图标间距
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
splashColor: Colors.white.withOpacity(0.2), // 白色水波纹
),
),
);
}
}
场景 3:边框按钮(取消 / 确认组合)
适用场景:弹窗操作、列表操作,需区分主次按钮
dart
class OutlineButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("边框按钮示例")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Row(
children: [
// 取消按钮(边框样式)
Expanded(
child: CommonButtonWidget(
text: "取消",
onTap: () => Navigator.pop(context),
buttonType: ButtonType.outline,
borderColor: Colors.grey[500]!,
borderWidth: 1.0,
radius: 4,
height: 44,
textStyle: const TextStyle(color: Colors.grey[700]),
),
),
const SizedBox(width: 16),
// 确认按钮(纯色样式)
Expanded(
child: CommonButtonWidget(
text: "确认",
onTap: () => EasyLoading.showToast("确认操作已触发"),
buttonType: ButtonType.solid,
bgColor: Colors.blue,
radius: 4,
height: 44,
),
),
],
),
),
);
}
}
场景 4:文字按钮(辅助操作)
适用场景:找回密码、查看更多、辅助说明,需轻量样式
dart
class TextButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("文字按钮示例")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 48),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("忘记密码?", style: TextStyle(fontSize: 16)),
const SizedBox(height: 8),
CommonButtonWidget(
text: "点击找回密码",
onTap: () => EasyLoading.showToast("跳转到找回密码页面"),
buttonType: ButtonType.text,
borderColor: Colors.blue, // 文本色继承borderColor
radius: 0, // 无圆角
height: 36, // 矮按钮
textStyle: const TextStyle(
color: Colors.blue,
fontSize: 14,
decoration: TextDecoration.underline, // 下划线
),
suffixIcon: const Icon( // 右侧箭头
Icons.arrow_forward_ios,
color: Colors.blue,
size: 16,
),
iconSize: 16,
expand: false, // 宽度自适应
),
],
),
),
);
}
}
五、核心封装技巧
1. 多样式统一封装
通过ButtonType枚举切换 4 种按钮样式,核心逻辑复用:
- 纯色按钮:
BoxDecoration设置背景色 - 渐变按钮:优先使用
gradient,无渐变则降级为纯色 - 边框按钮:透明背景 + 边框
- 文字按钮:无装饰,仅文本样式避免为每种按钮单独封装组件,减少重复代码。
2. 防抖逻辑内置
- 通过
_isClicking标记实现防抖,避免快速重复点击 - 防抖时长可配置(默认 300ms),适配不同场景
finally块确保标记重置,避免按钮永久禁用- 异常捕获:包裹
onTap回调,避免点击逻辑崩溃导致按钮卡死
3. 状态联动适配
isLoading/isDisabled自动禁用点击,无需外部判断- 禁用状态自动调整颜色、透明度、交互反馈
- 加载状态自动显示加载动画,隐藏图标,替换文本
- 状态变更时 UI 自动刷新,无需手动调用
setState
4. 内容灵活组合
- 支持前缀 / 后缀图标、加载动画与文本的自由组合
- 加载状态优先级最高,自动隐藏图标
- 文本支持单行省略,适配长文本场景
- 图标大小、间距可配置,满足不同布局需求
5. 交互体验优化
- 水波纹优化:仅非文字按钮显示,自定义颜色,去除高亮色
- 点击反馈:
enableFeedback控制震动反馈,禁用状态自动关闭 - 点击区域扩大:最小 44x44,符合 iOS 人机交互规范
- 长按回调:支持长按操作,禁用状态自动失效
六、避坑指南
1. 防抖时长适配
- 建议值:200-500ms(过短无法防抖,过长影响响应)
- 表单提交 / 支付等关键操作:500ms
- 普通操作 / 页面跳转:200-300ms
- 禁用防抖:设置
debounceDuration: Duration.zero
2. 加载状态交互
isLoading为 true 时,自动禁用点击,无需额外设置isDisabled- 加载文本建议简短(如 "加载中..."),避免文本溢出
- 加载图标大小建议 16-24px,过大会导致布局变形
3. 深色模式兼容
- 自定义颜色需通过
_adaptDarkMode方法适配,避免深色模式下颜色冲突 - 水波纹颜色默认适配深色模式,无需单独配置
- 禁用状态颜色需同时适配浅色 / 深色模式
4. 样式优先级
- 渐变按钮中
gradient优先级高于bgColor,设置渐变后bgColor仅作为降级 - 文本颜色优先级:禁用状态 > 按钮类型默认 > 自定义
textStyle - 边框宽度在禁用状态下自动减半,增强视觉区分
5. 点击区域规范
- 按钮高度建议≥44px(符合移动端交互规范)
expand: false时,按钮宽度自适应,需确保最小点击区域constraints确保最小 44x44 点击区域,适配小按钮场景
6. 水波纹注意事项
- 文字按钮默认关闭水波纹,如需开启需自定义
InkWell - 水波纹颜色需与背景色对比明显,增强反馈
InkWell需有背景色父容器,否则水波纹可能不显示
七、扩展能力(按需定制)
1. 添加点击动画
dart
// 在_buildButtonContent外层添加缩放动画
Widget _buildButtonContent() {
return AnimatedScale(
scale: _isClicking ? 0.98 : 1.0,
duration: const Duration(milliseconds: 100),
child: Row(/* 原有内容 */),
);
}
2. 支持自定义加载组件
dart
// 添加配置参数
final Widget? customLoadingWidget;
// 在_buildButtonContent中替换加载图标
if (isLoading) {
contentWidgets.add(
widget.customLoadingWidget ??
SizedBox(/* 原有加载图标 */)
);
}
3. 支持圆角为圆形
dart
// 使用BorderRadius.circular(widget.radius == double.infinity ? widget.height/2 : widget.radius)
// 调用时设置radius: double.infinity即可实现圆形按钮
CommonButtonWidget(
text: "圆形按钮",
onTap: () {},
radius: double.infinity,
height: 48,
expand: false,
)
八、总结
CommonButtonWidget 组件解决了原生按钮样式定制繁琐、状态管理复杂、交互体验差的问题,通过统一封装实现了多样式、多状态、高自定义的按钮组件。
组件具备以下特性:
- 🎨 4 种基础样式,一键切换
- 🚦 加载 / 禁用 / 点击态自动适配
- ⚡ 内置防抖、长按、水波纹优化
- 🛠️ 全样式自定义,满足品牌需求
- 📱 适配深色模式、全面屏、人机规范
一行代码即可调用,覆盖表单提交、支付、弹窗操作、辅助说明等 95%+ 按钮场景,大幅提升开发效率和用户体验。
欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。