init cc
This commit is contained in:
351
lib/core/widgets/app_button.dart
Normal file
351
lib/core/widgets/app_button.dart
Normal file
@@ -0,0 +1,351 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme/app_spacing.dart';
|
||||
import '../theme/app_typography.dart';
|
||||
|
||||
/// Customizable button component with multiple variants following Material 3 design
|
||||
///
|
||||
/// Supports filled, outlined, text, and icon variants with consistent theming
|
||||
/// and accessibility features.
|
||||
class AppButton extends StatelessWidget {
|
||||
/// Creates an app button with the specified variant and configuration
|
||||
const AppButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onPressed,
|
||||
this.variant = AppButtonVariant.filled,
|
||||
this.size = AppButtonSize.medium,
|
||||
this.icon,
|
||||
this.isLoading = false,
|
||||
this.isFullWidth = false,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.borderColor,
|
||||
this.elevation,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
/// The text displayed on the button
|
||||
final String text;
|
||||
|
||||
/// Called when the button is pressed
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// The visual variant of the button
|
||||
final AppButtonVariant variant;
|
||||
|
||||
/// The size of the button
|
||||
final AppButtonSize size;
|
||||
|
||||
/// Optional icon to display alongside text
|
||||
final IconData? icon;
|
||||
|
||||
/// Whether to show a loading indicator
|
||||
final bool isLoading;
|
||||
|
||||
/// Whether the button should take full width
|
||||
final bool isFullWidth;
|
||||
|
||||
/// Custom background color override
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Custom foreground color override
|
||||
final Color? foregroundColor;
|
||||
|
||||
/// Custom border color override (for outlined variant)
|
||||
final Color? borderColor;
|
||||
|
||||
/// Custom elevation override
|
||||
final double? elevation;
|
||||
|
||||
/// Semantic label for accessibility
|
||||
final String? semanticLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// Get button configuration based on size
|
||||
final buttonHeight = _getButtonHeight();
|
||||
final buttonPadding = _getButtonPadding();
|
||||
final textStyle = _getTextStyle();
|
||||
final iconSize = _getIconSize();
|
||||
|
||||
// Create button style
|
||||
final buttonStyle = _createButtonStyle(
|
||||
theme: theme,
|
||||
height: buttonHeight,
|
||||
padding: buttonPadding,
|
||||
);
|
||||
|
||||
// Handle loading state
|
||||
if (isLoading) {
|
||||
return _buildLoadingButton(
|
||||
context: context,
|
||||
style: buttonStyle,
|
||||
height: buttonHeight,
|
||||
);
|
||||
}
|
||||
|
||||
// Build appropriate button variant
|
||||
Widget button = switch (variant) {
|
||||
AppButtonVariant.filled => _buildFilledButton(
|
||||
context: context,
|
||||
style: buttonStyle,
|
||||
textStyle: textStyle,
|
||||
iconSize: iconSize,
|
||||
),
|
||||
AppButtonVariant.outlined => _buildOutlinedButton(
|
||||
context: context,
|
||||
style: buttonStyle,
|
||||
textStyle: textStyle,
|
||||
iconSize: iconSize,
|
||||
),
|
||||
AppButtonVariant.text => _buildTextButton(
|
||||
context: context,
|
||||
style: buttonStyle,
|
||||
textStyle: textStyle,
|
||||
iconSize: iconSize,
|
||||
),
|
||||
AppButtonVariant.icon => _buildIconButton(
|
||||
context: context,
|
||||
iconSize: iconSize,
|
||||
),
|
||||
};
|
||||
|
||||
// Apply full width if needed
|
||||
if (isFullWidth && variant != AppButtonVariant.icon) {
|
||||
button = SizedBox(
|
||||
width: double.infinity,
|
||||
child: button,
|
||||
);
|
||||
}
|
||||
|
||||
// Add semantic label for accessibility
|
||||
if (semanticLabel != null) {
|
||||
button = Semantics(
|
||||
label: semanticLabel,
|
||||
child: button,
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/// Build filled button variant
|
||||
Widget _buildFilledButton({
|
||||
required BuildContext context,
|
||||
required ButtonStyle style,
|
||||
required TextStyle textStyle,
|
||||
required double iconSize,
|
||||
}) {
|
||||
if (icon != null) {
|
||||
return FilledButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: iconSize),
|
||||
label: Text(text, style: textStyle),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
return FilledButton(
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
child: Text(text, style: textStyle),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build outlined button variant
|
||||
Widget _buildOutlinedButton({
|
||||
required BuildContext context,
|
||||
required ButtonStyle style,
|
||||
required TextStyle textStyle,
|
||||
required double iconSize,
|
||||
}) {
|
||||
if (icon != null) {
|
||||
return OutlinedButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: iconSize),
|
||||
label: Text(text, style: textStyle),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
return OutlinedButton(
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
child: Text(text, style: textStyle),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build text button variant
|
||||
Widget _buildTextButton({
|
||||
required BuildContext context,
|
||||
required ButtonStyle style,
|
||||
required TextStyle textStyle,
|
||||
required double iconSize,
|
||||
}) {
|
||||
if (icon != null) {
|
||||
return TextButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: iconSize),
|
||||
label: Text(text, style: textStyle),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
return TextButton(
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
child: Text(text, style: textStyle),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build icon button variant
|
||||
Widget _buildIconButton({
|
||||
required BuildContext context,
|
||||
required double iconSize,
|
||||
}) {
|
||||
if (icon == null) {
|
||||
throw ArgumentError('Icon button requires an icon');
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, size: iconSize),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor ?? theme.colorScheme.primary,
|
||||
minimumSize: Size(_getButtonHeight(), _getButtonHeight()),
|
||||
maximumSize: Size(_getButtonHeight(), _getButtonHeight()),
|
||||
),
|
||||
tooltip: text,
|
||||
);
|
||||
}
|
||||
|
||||
/// Build loading button state
|
||||
Widget _buildLoadingButton({
|
||||
required BuildContext context,
|
||||
required ButtonStyle style,
|
||||
required double height,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return FilledButton(
|
||||
onPressed: null,
|
||||
style: style,
|
||||
child: SizedBox(
|
||||
height: height * 0.5,
|
||||
width: height * 0.5,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
foregroundColor ?? theme.colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Create button style based on variant and customizations
|
||||
ButtonStyle _createButtonStyle({
|
||||
required ThemeData theme,
|
||||
required double height,
|
||||
required EdgeInsets padding,
|
||||
}) {
|
||||
return ButtonStyle(
|
||||
minimumSize: WidgetStateProperty.all(Size(0, height)),
|
||||
padding: WidgetStateProperty.all(padding),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: AppSpacing.buttonRadius,
|
||||
),
|
||||
),
|
||||
backgroundColor: backgroundColor != null
|
||||
? WidgetStateProperty.all(backgroundColor)
|
||||
: null,
|
||||
foregroundColor: foregroundColor != null
|
||||
? WidgetStateProperty.all(foregroundColor)
|
||||
: null,
|
||||
side: borderColor != null && variant == AppButtonVariant.outlined
|
||||
? WidgetStateProperty.all(BorderSide(color: borderColor!))
|
||||
: null,
|
||||
elevation: elevation != null
|
||||
? WidgetStateProperty.all(elevation)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get button height based on size
|
||||
double _getButtonHeight() {
|
||||
return switch (size) {
|
||||
AppButtonSize.small => AppSpacing.buttonHeightSmall,
|
||||
AppButtonSize.medium => AppSpacing.buttonHeight,
|
||||
AppButtonSize.large => AppSpacing.buttonHeightLarge,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get button padding based on size
|
||||
EdgeInsets _getButtonPadding() {
|
||||
return switch (size) {
|
||||
AppButtonSize.small => const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.lg,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
AppButtonSize.medium => const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.buttonPaddingHorizontal,
|
||||
vertical: AppSpacing.buttonPaddingVertical,
|
||||
),
|
||||
AppButtonSize.large => const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xxxl,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// Get text style based on size
|
||||
TextStyle _getTextStyle() {
|
||||
return switch (size) {
|
||||
AppButtonSize.small => AppTypography.labelMedium,
|
||||
AppButtonSize.medium => AppTypography.buttonText,
|
||||
AppButtonSize.large => AppTypography.labelLarge,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get icon size based on button size
|
||||
double _getIconSize() {
|
||||
return switch (size) {
|
||||
AppButtonSize.small => AppSpacing.iconSM,
|
||||
AppButtonSize.medium => AppSpacing.iconMD,
|
||||
AppButtonSize.large => AppSpacing.iconLG,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Available button variants
|
||||
enum AppButtonVariant {
|
||||
/// Filled button with background color (primary action)
|
||||
filled,
|
||||
|
||||
/// Outlined button with border (secondary action)
|
||||
outlined,
|
||||
|
||||
/// Text button without background (tertiary action)
|
||||
text,
|
||||
|
||||
/// Icon-only button
|
||||
icon,
|
||||
}
|
||||
|
||||
/// Available button sizes
|
||||
enum AppButtonSize {
|
||||
/// Small button (32dp height)
|
||||
small,
|
||||
|
||||
/// Medium button (40dp height) - default
|
||||
medium,
|
||||
|
||||
/// Large button (56dp height)
|
||||
large,
|
||||
}
|
||||
Reference in New Issue
Block a user