350 lines
8.3 KiB
Dart
350 lines
8.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Custom button widget with loading state and consistent styling
|
|
///
|
|
/// This widget provides a reusable button component with:
|
|
/// - Loading indicator support
|
|
/// - Disabled state
|
|
/// - Customizable colors, icons, and text
|
|
/// - Consistent padding and styling
|
|
///
|
|
/// Usage:
|
|
/// ```dart
|
|
/// CustomButton(
|
|
/// text: 'Login',
|
|
/// onPressed: _handleLogin,
|
|
/// isLoading: _isLoading,
|
|
/// )
|
|
///
|
|
/// CustomButton.outlined(
|
|
/// text: 'Cancel',
|
|
/// onPressed: _handleCancel,
|
|
/// )
|
|
///
|
|
/// CustomButton.text(
|
|
/// text: 'Skip',
|
|
/// onPressed: _handleSkip,
|
|
/// )
|
|
/// ```
|
|
class CustomButton extends StatelessWidget {
|
|
/// Button text
|
|
final String text;
|
|
|
|
/// Callback when button is pressed
|
|
final VoidCallback? onPressed;
|
|
|
|
/// Whether the button is in loading state
|
|
final bool isLoading;
|
|
|
|
/// Optional icon to display before text
|
|
final IconData? icon;
|
|
|
|
/// Button style variant
|
|
final ButtonStyle? style;
|
|
|
|
/// Whether this is an outlined button
|
|
final bool isOutlined;
|
|
|
|
/// Whether this is a text button
|
|
final bool isTextButton;
|
|
|
|
/// Minimum button width (null for full width)
|
|
final double? minWidth;
|
|
|
|
/// Minimum button height
|
|
final double? minHeight;
|
|
|
|
/// Background color (only for elevated buttons)
|
|
final Color? backgroundColor;
|
|
|
|
/// Foreground/text color
|
|
final Color? foregroundColor;
|
|
|
|
/// Border color (only for outlined buttons)
|
|
final Color? borderColor;
|
|
|
|
/// Font size
|
|
final double? fontSize;
|
|
|
|
/// Font weight
|
|
final FontWeight? fontWeight;
|
|
|
|
const CustomButton({
|
|
super.key,
|
|
required this.text,
|
|
required this.onPressed,
|
|
this.isLoading = false,
|
|
this.icon,
|
|
this.style,
|
|
this.minWidth,
|
|
this.minHeight,
|
|
this.backgroundColor,
|
|
this.foregroundColor,
|
|
this.fontSize,
|
|
this.fontWeight,
|
|
}) : isOutlined = false,
|
|
isTextButton = false,
|
|
borderColor = null;
|
|
|
|
/// Create an outlined button variant
|
|
const CustomButton.outlined({
|
|
super.key,
|
|
required this.text,
|
|
required this.onPressed,
|
|
this.isLoading = false,
|
|
this.icon,
|
|
this.style,
|
|
this.minWidth,
|
|
this.minHeight,
|
|
this.foregroundColor,
|
|
this.borderColor,
|
|
this.fontSize,
|
|
this.fontWeight,
|
|
}) : isOutlined = true,
|
|
isTextButton = false,
|
|
backgroundColor = null;
|
|
|
|
/// Create a text button variant
|
|
const CustomButton.text({
|
|
super.key,
|
|
required this.text,
|
|
required this.onPressed,
|
|
this.isLoading = false,
|
|
this.icon,
|
|
this.style,
|
|
this.minWidth,
|
|
this.minHeight,
|
|
this.foregroundColor,
|
|
this.fontSize,
|
|
this.fontWeight,
|
|
}) : isOutlined = false,
|
|
isTextButton = true,
|
|
backgroundColor = null,
|
|
borderColor = null;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colorScheme = theme.colorScheme;
|
|
|
|
// Determine if button should be disabled
|
|
final bool isDisabled = onPressed == null || isLoading;
|
|
|
|
// Build button content
|
|
Widget content;
|
|
if (isLoading) {
|
|
content = SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
isTextButton
|
|
? foregroundColor ?? colorScheme.primary
|
|
: foregroundColor ?? colorScheme.onPrimary,
|
|
),
|
|
),
|
|
);
|
|
} else if (icon != null) {
|
|
content = Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(icon, size: 20),
|
|
const SizedBox(width: 8),
|
|
Flexible(
|
|
child: Text(
|
|
text,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
} else {
|
|
content = Text(text);
|
|
}
|
|
|
|
// Build button style
|
|
final ButtonStyle buttonStyle = style ??
|
|
(isTextButton
|
|
? _buildTextButtonStyle(context)
|
|
: isOutlined
|
|
? _buildOutlinedButtonStyle(context)
|
|
: _buildElevatedButtonStyle(context));
|
|
|
|
// Build appropriate button widget
|
|
if (isTextButton) {
|
|
return TextButton(
|
|
onPressed: isDisabled ? null : onPressed,
|
|
style: buttonStyle,
|
|
child: content,
|
|
);
|
|
} else if (isOutlined) {
|
|
return OutlinedButton(
|
|
onPressed: isDisabled ? null : onPressed,
|
|
style: buttonStyle,
|
|
child: content,
|
|
);
|
|
} else {
|
|
return ElevatedButton(
|
|
onPressed: isDisabled ? null : onPressed,
|
|
style: buttonStyle,
|
|
child: content,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Build elevated button style
|
|
ButtonStyle _buildElevatedButtonStyle(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return ElevatedButton.styleFrom(
|
|
backgroundColor: backgroundColor ?? colorScheme.primary,
|
|
foregroundColor: foregroundColor ?? colorScheme.onPrimary,
|
|
minimumSize: Size(
|
|
minWidth ?? double.infinity,
|
|
minHeight ?? 48,
|
|
),
|
|
textStyle: TextStyle(
|
|
fontSize: fontSize ?? 16,
|
|
fontWeight: fontWeight ?? FontWeight.w600,
|
|
),
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 12,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Build outlined button style
|
|
ButtonStyle _buildOutlinedButtonStyle(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return OutlinedButton.styleFrom(
|
|
foregroundColor: foregroundColor ?? colorScheme.primary,
|
|
side: BorderSide(
|
|
color: borderColor ?? colorScheme.primary,
|
|
width: 1.5,
|
|
),
|
|
minimumSize: Size(
|
|
minWidth ?? double.infinity,
|
|
minHeight ?? 48,
|
|
),
|
|
textStyle: TextStyle(
|
|
fontSize: fontSize ?? 16,
|
|
fontWeight: fontWeight ?? FontWeight.w600,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 12,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Build text button style
|
|
ButtonStyle _buildTextButtonStyle(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return TextButton.styleFrom(
|
|
foregroundColor: foregroundColor ?? colorScheme.primary,
|
|
minimumSize: Size(
|
|
minWidth ?? 0,
|
|
minHeight ?? 48,
|
|
),
|
|
textStyle: TextStyle(
|
|
fontSize: fontSize ?? 16,
|
|
fontWeight: fontWeight ?? FontWeight.w600,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Icon button with loading state
|
|
class CustomIconButton extends StatelessWidget {
|
|
/// Icon to display
|
|
final IconData icon;
|
|
|
|
/// Callback when button is pressed
|
|
final VoidCallback? onPressed;
|
|
|
|
/// Whether the button is in loading state
|
|
final bool isLoading;
|
|
|
|
/// Icon size
|
|
final double? iconSize;
|
|
|
|
/// Icon color
|
|
final Color? color;
|
|
|
|
/// Background color
|
|
final Color? backgroundColor;
|
|
|
|
/// Tooltip text
|
|
final String? tooltip;
|
|
|
|
const CustomIconButton({
|
|
super.key,
|
|
required this.icon,
|
|
required this.onPressed,
|
|
this.isLoading = false,
|
|
this.iconSize,
|
|
this.color,
|
|
this.backgroundColor,
|
|
this.tooltip,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final bool isDisabled = onPressed == null || isLoading;
|
|
|
|
Widget button = IconButton(
|
|
icon: isLoading
|
|
? SizedBox(
|
|
height: iconSize ?? 24,
|
|
width: iconSize ?? 24,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(
|
|
color ?? theme.colorScheme.primary,
|
|
),
|
|
),
|
|
)
|
|
: Icon(
|
|
icon,
|
|
size: iconSize ?? 24,
|
|
color: color,
|
|
),
|
|
onPressed: isDisabled ? null : onPressed,
|
|
style: backgroundColor != null
|
|
? IconButton.styleFrom(
|
|
backgroundColor: backgroundColor,
|
|
)
|
|
: null,
|
|
);
|
|
|
|
if (tooltip != null) {
|
|
return Tooltip(
|
|
message: tooltip!,
|
|
child: button,
|
|
);
|
|
}
|
|
|
|
return button;
|
|
}
|
|
}
|