snap_wish/lib/presentation/widgets/custom_button.dart
2025-09-17 11:42:38 +08:00

508 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
}