508 lines
16 KiB
Dart
508 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
||
import '../../../core/theme/app_theme.dart';
|
||
|
||
/// 自定义按钮组件 - 提供多种样式和状态的可复用按钮
|
||
/// 支持主要按钮、次要按钮、文本按钮、图标按钮等多种类型
|
||
/// 提供加载状态、禁用状态等交互反馈
|
||
class CustomButton extends StatelessWidget {
|
||
/// 按钮文本内容
|
||
final String text;
|
||
|
||
/// 按钮点击回调函数
|
||
final VoidCallback? onPressed;
|
||
|
||
/// 按钮类型,决定外观样式
|
||
final ButtonType buttonType;
|
||
|
||
/// 按钮尺寸,影响整体大小
|
||
final ButtonSize buttonSize;
|
||
|
||
/// 是否显示加载状态
|
||
final bool isLoading;
|
||
|
||
/// 是否禁用按钮
|
||
final bool isDisabled;
|
||
|
||
/// 前置图标,显示在文本左侧
|
||
final IconData? prefixIcon;
|
||
|
||
/// 后置图标,显示在文本右侧
|
||
final IconData? suffixIcon;
|
||
|
||
/// 自定义背景颜色,优先级高于buttonType
|
||
final Color? backgroundColor;
|
||
|
||
/// 自定义文本颜色,优先级高于buttonType
|
||
final Color? textColor;
|
||
|
||
/// 按钮圆角半径,null时使用默认值
|
||
final double? borderRadius;
|
||
|
||
/// 按钮最小宽度,null时根据内容自适应
|
||
final double? minWidth;
|
||
|
||
/// 按钮最大宽度,null时无限制
|
||
final double? maxWidth;
|
||
|
||
/// 内边距,null时使用默认内边距
|
||
final EdgeInsetsGeometry? padding;
|
||
|
||
/// 阴影高度,null时根据按钮类型使用默认阴影
|
||
final double? elevation;
|
||
|
||
const CustomButton({
|
||
super.key,
|
||
required this.text,
|
||
required this.onPressed,
|
||
this.buttonType = ButtonType.primary,
|
||
this.buttonSize = ButtonSize.medium,
|
||
this.isLoading = false,
|
||
this.isDisabled = false,
|
||
this.prefixIcon,
|
||
this.suffixIcon,
|
||
this.backgroundColor,
|
||
this.textColor,
|
||
this.borderRadius,
|
||
this.minWidth,
|
||
this.maxWidth,
|
||
this.padding,
|
||
this.elevation,
|
||
});
|
||
|
||
/// 创建主要按钮 - 用于主要操作,视觉突出
|
||
factory CustomButton.primary({
|
||
required String text,
|
||
required VoidCallback? onPressed,
|
||
bool isLoading = false,
|
||
bool isDisabled = false,
|
||
IconData? prefixIcon,
|
||
IconData? suffixIcon,
|
||
double? minWidth,
|
||
double? maxWidth,
|
||
}) {
|
||
return CustomButton(
|
||
text: text,
|
||
onPressed: onPressed,
|
||
buttonType: ButtonType.primary,
|
||
isLoading: isLoading,
|
||
isDisabled: isDisabled,
|
||
prefixIcon: prefixIcon,
|
||
suffixIcon: suffixIcon,
|
||
minWidth: minWidth,
|
||
maxWidth: maxWidth,
|
||
);
|
||
}
|
||
|
||
/// 创建次要按钮 - 用于次要操作,视觉弱化
|
||
factory CustomButton.secondary({
|
||
required String text,
|
||
required VoidCallback? onPressed,
|
||
bool isLoading = false,
|
||
bool isDisabled = false,
|
||
IconData? prefixIcon,
|
||
IconData? suffixIcon,
|
||
double? minWidth,
|
||
double? maxWidth,
|
||
}) {
|
||
return CustomButton(
|
||
text: text,
|
||
onPressed: onPressed,
|
||
buttonType: ButtonType.secondary,
|
||
isLoading: isLoading,
|
||
isDisabled: isDisabled,
|
||
prefixIcon: prefixIcon,
|
||
suffixIcon: suffixIcon,
|
||
minWidth: minWidth,
|
||
maxWidth: maxWidth,
|
||
);
|
||
}
|
||
|
||
/// 创建文本按钮 - 无边框无背景,适合嵌入其他组件
|
||
factory CustomButton.text({
|
||
required String text,
|
||
required VoidCallback? onPressed,
|
||
bool isLoading = false,
|
||
bool isDisabled = false,
|
||
IconData? prefixIcon,
|
||
IconData? suffixIcon,
|
||
Color? textColor,
|
||
double? minWidth,
|
||
double? maxWidth,
|
||
}) {
|
||
return CustomButton(
|
||
text: text,
|
||
onPressed: onPressed,
|
||
buttonType: ButtonType.text,
|
||
isLoading: isLoading,
|
||
isDisabled: isDisabled,
|
||
prefixIcon: prefixIcon,
|
||
suffixIcon: suffixIcon,
|
||
textColor: textColor,
|
||
minWidth: minWidth,
|
||
maxWidth: maxWidth,
|
||
);
|
||
}
|
||
|
||
/// 创建轮廓按钮 - 有边框无填充,适合次要操作
|
||
factory CustomButton.outlined({
|
||
required String text,
|
||
required VoidCallback? onPressed,
|
||
bool isLoading = false,
|
||
bool isDisabled = false,
|
||
IconData? prefixIcon,
|
||
IconData? suffixIcon,
|
||
double? minWidth,
|
||
double? maxWidth,
|
||
}) {
|
||
return CustomButton(
|
||
text: text,
|
||
onPressed: onPressed,
|
||
buttonType: ButtonType.outlined,
|
||
isLoading: isLoading,
|
||
isDisabled: isDisabled,
|
||
prefixIcon: prefixIcon,
|
||
suffixIcon: suffixIcon,
|
||
minWidth: minWidth,
|
||
maxWidth: maxWidth,
|
||
);
|
||
}
|
||
|
||
/// 创建危险按钮 - 用于删除等危险操作,通常使用红色主题
|
||
factory CustomButton.danger({
|
||
required String text,
|
||
required VoidCallback? onPressed,
|
||
bool isLoading = false,
|
||
bool isDisabled = false,
|
||
IconData? prefixIcon,
|
||
IconData? suffixIcon,
|
||
double? minWidth,
|
||
double? maxWidth,
|
||
}) {
|
||
return CustomButton(
|
||
text: text,
|
||
onPressed: onPressed,
|
||
buttonType: ButtonType.danger,
|
||
isLoading: isLoading,
|
||
isDisabled: isDisabled,
|
||
prefixIcon: prefixIcon,
|
||
suffixIcon: suffixIcon,
|
||
minWidth: minWidth,
|
||
maxWidth: maxWidth,
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
final appColors = AppColors.of(context);
|
||
|
||
// 确定按钮是否可点击
|
||
final bool buttonEnabled = !isLoading && !isDisabled && onPressed != null;
|
||
|
||
// 获取按钮样式配置
|
||
final buttonStyle = _getButtonStyle(theme, appColors);
|
||
final textStyle = _getTextStyle(theme, appColors);
|
||
final paddingValue = _getPadding();
|
||
|
||
// 构建按钮内容
|
||
final buttonContent = isLoading
|
||
? SizedBox(
|
||
width: textStyle.fontSize,
|
||
height: textStyle.fontSize,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2,
|
||
valueColor: AlwaysStoppedAnimation<Color>(
|
||
buttonStyle.foregroundColor?.resolve({}) ??
|
||
theme.colorScheme.onPrimary,
|
||
),
|
||
),
|
||
)
|
||
: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
if (prefixIcon != null) ...[
|
||
Icon(
|
||
prefixIcon,
|
||
size: textStyle.fontSize! * 1.2,
|
||
color: textStyle.color,
|
||
),
|
||
const SizedBox(width: 8),
|
||
],
|
||
Text(
|
||
text,
|
||
style: textStyle,
|
||
overflow: TextOverflow.ellipsis,
|
||
maxLines: 1,
|
||
),
|
||
if (suffixIcon != null) ...[
|
||
const SizedBox(width: 8),
|
||
Icon(
|
||
suffixIcon,
|
||
size: textStyle.fontSize! * 1.2,
|
||
color: textStyle.color,
|
||
),
|
||
],
|
||
],
|
||
);
|
||
|
||
// 根据按钮类型构建不同的按钮组件
|
||
switch (buttonType) {
|
||
case ButtonType.primary:
|
||
case ButtonType.secondary:
|
||
case ButtonType.danger:
|
||
return ConstrainedBox(
|
||
constraints: BoxConstraints(
|
||
minWidth: minWidth ?? 0,
|
||
maxWidth: maxWidth ?? double.infinity,
|
||
minHeight: _getMinHeight(),
|
||
),
|
||
child: ElevatedButton(
|
||
onPressed: buttonEnabled ? onPressed : null,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: buttonStyle.backgroundColor?.resolve({}),
|
||
foregroundColor: buttonStyle.foregroundColor?.resolve({}),
|
||
elevation: elevation ?? (buttonEnabled ? 2 : 0),
|
||
padding: paddingValue,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(
|
||
borderRadius ?? _getDefaultBorderRadius()),
|
||
),
|
||
disabledBackgroundColor:
|
||
buttonStyle.backgroundColor?.resolve({MaterialState.disabled}),
|
||
disabledForegroundColor:
|
||
buttonStyle.foregroundColor?.resolve({MaterialState.disabled}),
|
||
),
|
||
child: buttonContent,
|
||
),
|
||
);
|
||
|
||
case ButtonType.outlined:
|
||
return ConstrainedBox(
|
||
constraints: BoxConstraints(
|
||
minWidth: minWidth ?? 0,
|
||
maxWidth: maxWidth ?? double.infinity,
|
||
minHeight: _getMinHeight(),
|
||
),
|
||
child: OutlinedButton(
|
||
onPressed: buttonEnabled ? onPressed : null,
|
||
style: OutlinedButton.styleFrom(
|
||
foregroundColor: buttonStyle.foregroundColor?.resolve({}),
|
||
padding: paddingValue,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(
|
||
borderRadius ?? _getDefaultBorderRadius()),
|
||
),
|
||
side: BorderSide(
|
||
color: buttonEnabled
|
||
? (buttonStyle.foregroundColor?.resolve({}) ??
|
||
theme.colorScheme.primary)
|
||
: (buttonStyle.foregroundColor?.resolve({MaterialState.disabled}) ??
|
||
theme.colorScheme.onSurface.withOpacity(0.12)),
|
||
),
|
||
disabledForegroundColor:
|
||
buttonStyle.foregroundColor?.resolve({MaterialState.disabled}),
|
||
),
|
||
child: buttonContent,
|
||
),
|
||
);
|
||
|
||
case ButtonType.text:
|
||
return ConstrainedBox(
|
||
constraints: BoxConstraints(
|
||
minWidth: minWidth ?? 0,
|
||
maxWidth: maxWidth ?? double.infinity,
|
||
minHeight: _getMinHeight(),
|
||
),
|
||
child: TextButton(
|
||
onPressed: buttonEnabled ? onPressed : null,
|
||
style: TextButton.styleFrom(
|
||
foregroundColor: buttonStyle.foregroundColor?.resolve({}),
|
||
padding: paddingValue,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(
|
||
borderRadius ?? _getDefaultBorderRadius()),
|
||
),
|
||
disabledForegroundColor:
|
||
buttonStyle.foregroundColor?.resolve({MaterialState.disabled}),
|
||
),
|
||
child: buttonContent,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 获取按钮样式配置 - 根据按钮类型和主题返回对应的样式
|
||
ButtonStyle _getButtonStyle(ThemeData theme, AppColors appColors) {
|
||
switch (buttonType) {
|
||
case ButtonType.primary:
|
||
return ButtonStyle(
|
||
backgroundColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||
if (states.contains(MaterialState.disabled)) {
|
||
return backgroundColor?.withOpacity(0.12) ??
|
||
theme.colorScheme.primary.withOpacity(0.12);
|
||
}
|
||
return backgroundColor ?? theme.colorScheme.primary;
|
||
}),
|
||
foregroundColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||
if (states.contains(MaterialState.disabled)) {
|
||
return textColor?.withOpacity(0.38) ??
|
||
theme.colorScheme.onPrimary.withOpacity(0.38);
|
||
}
|
||
return textColor ?? theme.colorScheme.onPrimary;
|
||
}),
|
||
);
|
||
|
||
case ButtonType.secondary:
|
||
return ButtonStyle(
|
||
backgroundColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||
if (states.contains(MaterialState.disabled)) {
|
||
return (backgroundColor ?? theme.colorScheme.secondaryContainer)
|
||
.withOpacity(0.12);
|
||
}
|
||
return backgroundColor ?? theme.colorScheme.secondaryContainer;
|
||
}),
|
||
foregroundColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||
if (states.contains(MaterialState.disabled)) {
|
||
return (textColor ?? theme.colorScheme.onSecondaryContainer)
|
||
.withOpacity(0.38);
|
||
}
|
||
return textColor ?? theme.colorScheme.onSecondaryContainer;
|
||
}),
|
||
);
|
||
|
||
case ButtonType.danger:
|
||
return ButtonStyle(
|
||
backgroundColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||
if (states.contains(MaterialState.disabled)) {
|
||
return (backgroundColor ?? AppColors.error).withOpacity(0.12);
|
||
}
|
||
return backgroundColor ?? AppColors.error;
|
||
}),
|
||
foregroundColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||
if (states.contains(MaterialState.disabled)) {
|
||
return (textColor ?? AppColors.onError).withOpacity(0.38);
|
||
}
|
||
return textColor ?? AppColors.onError;
|
||
}),
|
||
);
|
||
|
||
case ButtonType.outlined:
|
||
case ButtonType.text:
|
||
return ButtonStyle(
|
||
foregroundColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||
if (states.contains(MaterialState.disabled)) {
|
||
return (textColor ?? theme.colorScheme.primary).withOpacity(0.38);
|
||
}
|
||
return textColor ?? theme.colorScheme.primary;
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 获取文本样式 - 根据按钮尺寸返回对应的文本样式
|
||
TextStyle _getTextStyle(ThemeData theme, AppColors appColors) {
|
||
final baseStyle = theme.textTheme.labelLarge ?? const TextStyle();
|
||
|
||
switch (buttonSize) {
|
||
case ButtonSize.small:
|
||
return baseStyle.copyWith(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: _getButtonStyle(theme, appColors)
|
||
.foregroundColor
|
||
?.resolve(isDisabled || isLoading
|
||
? {MaterialState.disabled}
|
||
: {}),
|
||
);
|
||
case ButtonSize.medium:
|
||
return baseStyle.copyWith(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: _getButtonStyle(theme, appColors)
|
||
.foregroundColor
|
||
?.resolve(isDisabled || isLoading
|
||
? {MaterialState.disabled}
|
||
: {}),
|
||
);
|
||
case ButtonSize.large:
|
||
return baseStyle.copyWith(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w700,
|
||
color: _getButtonStyle(theme, appColors)
|
||
.foregroundColor
|
||
?.resolve(isDisabled || isLoading
|
||
? {MaterialState.disabled}
|
||
: {}),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 获取内边距 - 根据按钮尺寸返回对应的内边距
|
||
EdgeInsetsGeometry _getPadding() {
|
||
if (padding != null) return padding!;
|
||
|
||
switch (buttonSize) {
|
||
case ButtonSize.small:
|
||
return const EdgeInsets.symmetric(horizontal: 12, vertical: 8);
|
||
case ButtonSize.medium:
|
||
return const EdgeInsets.symmetric(horizontal: 16, vertical: 12);
|
||
case ButtonSize.large:
|
||
return const EdgeInsets.symmetric(horizontal: 24, vertical: 16);
|
||
}
|
||
}
|
||
|
||
/// 获取最小高度 - 根据按钮尺寸返回对应的最小高度
|
||
double _getMinHeight() {
|
||
switch (buttonSize) {
|
||
case ButtonSize.small:
|
||
return 32;
|
||
case ButtonSize.medium:
|
||
return 40;
|
||
case ButtonSize.large:
|
||
return 48;
|
||
}
|
||
}
|
||
|
||
/// 获取默认圆角半径 - 根据按钮尺寸返回对应的圆角半径
|
||
double _getDefaultBorderRadius() {
|
||
switch (buttonSize) {
|
||
case ButtonSize.small:
|
||
return 4;
|
||
case ButtonSize.medium:
|
||
return 8;
|
||
case ButtonSize.large:
|
||
return 12;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 按钮类型枚举 - 定义按钮的视觉样式
|
||
enum ButtonType {
|
||
/// 主要按钮 - 用于主要操作,视觉突出,使用主题主色调
|
||
primary,
|
||
|
||
/// 次要按钮 - 用于次要操作,视觉弱化,使用主题次要色调
|
||
secondary,
|
||
|
||
/// 轮廓按钮 - 有边框无填充,适合次要操作
|
||
outlined,
|
||
|
||
/// 文本按钮 - 无边框无背景,适合嵌入其他组件
|
||
text,
|
||
|
||
/// 危险按钮 - 用于删除等危险操作,通常使用红色主题
|
||
danger,
|
||
}
|
||
|
||
/// 按钮尺寸枚举 - 定义按钮的大小
|
||
enum ButtonSize {
|
||
/// 小尺寸 - 适合紧凑布局,内边距较小
|
||
small,
|
||
|
||
/// 中等尺寸 - 默认尺寸,适合大多数场景
|
||
medium,
|
||
|
||
/// 大尺寸 - 适合重要操作或触摸友好的场景
|
||
large,
|
||
} |