first commit
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
enum ButtonGroupVariant {
|
||||
segmented,
|
||||
toggle,
|
||||
tabs,
|
||||
chips,
|
||||
}
|
||||
|
||||
class ButtonGroupOption {
|
||||
final String text;
|
||||
final IconData? icon;
|
||||
final String value;
|
||||
final bool disabled;
|
||||
final Widget? badge;
|
||||
|
||||
const ButtonGroupOption({
|
||||
required this.text,
|
||||
required this.value,
|
||||
this.icon,
|
||||
this.disabled = false,
|
||||
this.badge,
|
||||
});
|
||||
}
|
||||
|
||||
class SophisticatedButtonGroup extends StatefulWidget {
|
||||
final List<ButtonGroupOption> options;
|
||||
final String? selectedValue;
|
||||
final List<String>? selectedValues; // For multi-select
|
||||
final Function(String)? onSelectionChanged;
|
||||
final Function(List<String>)? onMultiSelectionChanged;
|
||||
final ButtonGroupVariant variant;
|
||||
final bool multiSelect;
|
||||
final Color? backgroundColor;
|
||||
final Color? selectedColor;
|
||||
final Color? unselectedColor;
|
||||
final double? height;
|
||||
final EdgeInsets? padding;
|
||||
final bool animated;
|
||||
final bool fullWidth;
|
||||
|
||||
const SophisticatedButtonGroup({
|
||||
super.key,
|
||||
required this.options,
|
||||
this.selectedValue,
|
||||
this.selectedValues,
|
||||
this.onSelectionChanged,
|
||||
this.onMultiSelectionChanged,
|
||||
this.variant = ButtonGroupVariant.segmented,
|
||||
this.multiSelect = false,
|
||||
this.backgroundColor,
|
||||
this.selectedColor,
|
||||
this.unselectedColor,
|
||||
this.height,
|
||||
this.padding,
|
||||
this.animated = true,
|
||||
this.fullWidth = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SophisticatedButtonGroup> createState() => _SophisticatedButtonGroupState();
|
||||
}
|
||||
|
||||
class _SophisticatedButtonGroupState extends State<SophisticatedButtonGroup>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _slideController;
|
||||
late Animation<double> _slideAnimation;
|
||||
|
||||
String? _internalSelectedValue;
|
||||
List<String> _internalSelectedValues = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_slideController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _slideController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_internalSelectedValue = widget.selectedValue;
|
||||
_internalSelectedValues = widget.selectedValues ?? [];
|
||||
|
||||
if (widget.animated) {
|
||||
_slideController.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SophisticatedButtonGroup oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget.selectedValue != oldWidget.selectedValue) {
|
||||
_internalSelectedValue = widget.selectedValue;
|
||||
}
|
||||
|
||||
if (widget.selectedValues != oldWidget.selectedValues) {
|
||||
_internalSelectedValues = widget.selectedValues ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_slideController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (widget.variant) {
|
||||
case ButtonGroupVariant.segmented:
|
||||
return _buildSegmentedGroup();
|
||||
case ButtonGroupVariant.toggle:
|
||||
return _buildToggleGroup();
|
||||
case ButtonGroupVariant.tabs:
|
||||
return _buildTabsGroup();
|
||||
case ButtonGroupVariant.chips:
|
||||
return _buildChipsGroup();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildSegmentedGroup() {
|
||||
return AnimatedBuilder(
|
||||
animation: _slideAnimation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
height: widget.height ?? 48,
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.backgroundColor ?? AppTheme.backgroundLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppTheme.textHint.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: widget.options.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final option = entry.value;
|
||||
final isSelected = _isSelected(option.value);
|
||||
|
||||
return Expanded(
|
||||
child: _buildSegmentedButton(option, isSelected, index),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSegmentedButton(ButtonGroupOption option, bool isSelected, int index) {
|
||||
return AnimatedContainer(
|
||||
duration: widget.animated ? const Duration(milliseconds: 200) : Duration.zero,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? (widget.selectedColor ?? AppTheme.primaryColor)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: isSelected ? [
|
||||
BoxShadow(
|
||||
color: (widget.selectedColor ?? AppTheme.primaryColor).withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
] : null,
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: option.disabled ? null : () => _handleSelection(option.value),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: _buildButtonContent(option, isSelected),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggleGroup() {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: widget.options.map((option) {
|
||||
final isSelected = _isSelected(option.value);
|
||||
return _buildToggleButton(option, isSelected);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToggleButton(ButtonGroupOption option, bool isSelected) {
|
||||
return AnimatedContainer(
|
||||
duration: widget.animated ? const Duration(milliseconds: 200) : Duration.zero,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? (widget.selectedColor ?? AppTheme.primaryColor)
|
||||
: (widget.backgroundColor ?? Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? (widget.selectedColor ?? AppTheme.primaryColor)
|
||||
: AppTheme.textHint.withOpacity(0.3),
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: option.disabled ? null : () => _handleSelection(option.value),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: _buildButtonContent(option, isSelected),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabsGroup() {
|
||||
return Container(
|
||||
height: widget.height ?? 44,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: AppTheme.textHint.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: widget.options.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final option = entry.value;
|
||||
final isSelected = _isSelected(option.value);
|
||||
|
||||
return widget.fullWidth
|
||||
? Expanded(child: _buildTabButton(option, isSelected))
|
||||
: _buildTabButton(option, isSelected);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabButton(ButtonGroupOption option, bool isSelected) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: option.disabled ? null : () => _handleSelection(option.value),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: isSelected
|
||||
? (widget.selectedColor ?? AppTheme.primaryColor)
|
||||
: Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: _buildButtonContent(option, isSelected),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChipsGroup() {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: widget.options.map((option) {
|
||||
final isSelected = _isSelected(option.value);
|
||||
return _buildChip(option, isSelected);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChip(ButtonGroupOption option, bool isSelected) {
|
||||
return FilterChip(
|
||||
label: _buildButtonContent(option, isSelected),
|
||||
selected: isSelected,
|
||||
onSelected: option.disabled ? null : (selected) => _handleSelection(option.value),
|
||||
backgroundColor: widget.backgroundColor,
|
||||
selectedColor: widget.selectedColor ?? AppTheme.primaryColor,
|
||||
checkmarkColor: Colors.white,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? Colors.white : (widget.unselectedColor ?? AppTheme.textPrimary),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButtonContent(ButtonGroupOption option, bool isSelected) {
|
||||
final color = isSelected
|
||||
? Colors.white
|
||||
: (widget.unselectedColor ?? AppTheme.textSecondary);
|
||||
|
||||
if (option.icon != null) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
option.icon,
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
option.text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (option.badge != null) ...[
|
||||
const SizedBox(width: 6),
|
||||
option.badge!,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Text(
|
||||
option.text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
||||
bool _isSelected(String value) {
|
||||
if (widget.multiSelect) {
|
||||
return _internalSelectedValues.contains(value);
|
||||
}
|
||||
return _internalSelectedValue == value;
|
||||
}
|
||||
|
||||
void _handleSelection(String value) {
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
if (widget.multiSelect) {
|
||||
setState(() {
|
||||
if (_internalSelectedValues.contains(value)) {
|
||||
_internalSelectedValues.remove(value);
|
||||
} else {
|
||||
_internalSelectedValues.add(value);
|
||||
}
|
||||
});
|
||||
widget.onMultiSelectionChanged?.call(_internalSelectedValues);
|
||||
} else {
|
||||
setState(() {
|
||||
_internalSelectedValue = value;
|
||||
});
|
||||
widget.onSelectionChanged?.call(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
303
unionflow-mobile-apps/lib/shared/widgets/buttons/buttons.dart
Normal file
303
unionflow-mobile-apps/lib/shared/widgets/buttons/buttons.dart
Normal file
@@ -0,0 +1,303 @@
|
||||
// Export all sophisticated button components
|
||||
export 'sophisticated_button.dart';
|
||||
export 'floating_action_button.dart';
|
||||
export 'icon_button.dart';
|
||||
export 'button_group.dart';
|
||||
|
||||
// Predefined button styles for quick usage
|
||||
import 'package:flutter/material.dart';
|
||||
import 'sophisticated_button.dart';
|
||||
import 'floating_action_button.dart';
|
||||
import 'icon_button.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
// Quick button factory methods
|
||||
class QuickButtons {
|
||||
// Primary buttons
|
||||
static Widget primary({
|
||||
required String text,
|
||||
required VoidCallback onPressed,
|
||||
IconData? icon,
|
||||
ButtonSize size = ButtonSize.medium,
|
||||
bool loading = false,
|
||||
}) {
|
||||
return SophisticatedButton(
|
||||
text: text,
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: ButtonVariant.primary,
|
||||
size: size,
|
||||
loading: loading,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget secondary({
|
||||
required String text,
|
||||
required VoidCallback onPressed,
|
||||
IconData? icon,
|
||||
ButtonSize size = ButtonSize.medium,
|
||||
bool loading = false,
|
||||
}) {
|
||||
return SophisticatedButton(
|
||||
text: text,
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: ButtonVariant.secondary,
|
||||
size: size,
|
||||
loading: loading,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget outline({
|
||||
required String text,
|
||||
required VoidCallback onPressed,
|
||||
IconData? icon,
|
||||
ButtonSize size = ButtonSize.medium,
|
||||
Color? color,
|
||||
}) {
|
||||
return SophisticatedButton(
|
||||
text: text,
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: ButtonVariant.outline,
|
||||
size: size,
|
||||
backgroundColor: color,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget ghost({
|
||||
required String text,
|
||||
required VoidCallback onPressed,
|
||||
IconData? icon,
|
||||
ButtonSize size = ButtonSize.medium,
|
||||
Color? color,
|
||||
}) {
|
||||
return SophisticatedButton(
|
||||
text: text,
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: ButtonVariant.ghost,
|
||||
size: size,
|
||||
backgroundColor: color,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget gradient({
|
||||
required String text,
|
||||
required VoidCallback onPressed,
|
||||
IconData? icon,
|
||||
ButtonSize size = ButtonSize.medium,
|
||||
Gradient? gradient,
|
||||
}) {
|
||||
return SophisticatedButton(
|
||||
text: text,
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: ButtonVariant.gradient,
|
||||
size: size,
|
||||
gradient: gradient,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget glass({
|
||||
required String text,
|
||||
required VoidCallback onPressed,
|
||||
IconData? icon,
|
||||
ButtonSize size = ButtonSize.medium,
|
||||
}) {
|
||||
return SophisticatedButton(
|
||||
text: text,
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: ButtonVariant.glass,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget danger({
|
||||
required String text,
|
||||
required VoidCallback onPressed,
|
||||
IconData? icon,
|
||||
ButtonSize size = ButtonSize.medium,
|
||||
}) {
|
||||
return SophisticatedButton(
|
||||
text: text,
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: ButtonVariant.danger,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget success({
|
||||
required String text,
|
||||
required VoidCallback onPressed,
|
||||
IconData? icon,
|
||||
ButtonSize size = ButtonSize.medium,
|
||||
}) {
|
||||
return SophisticatedButton(
|
||||
text: text,
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: ButtonVariant.success,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
// Icon buttons
|
||||
static Widget iconPrimary({
|
||||
required IconData icon,
|
||||
required VoidCallback onPressed,
|
||||
double? size,
|
||||
String? tooltip,
|
||||
int? notificationCount,
|
||||
}) {
|
||||
return SophisticatedIconButton(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: IconButtonVariant.filled,
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
size: size,
|
||||
tooltip: tooltip,
|
||||
notificationCount: notificationCount,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget iconSecondary({
|
||||
required IconData icon,
|
||||
required VoidCallback onPressed,
|
||||
double? size,
|
||||
String? tooltip,
|
||||
int? notificationCount,
|
||||
}) {
|
||||
return SophisticatedIconButton(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: IconButtonVariant.filled,
|
||||
backgroundColor: AppTheme.secondaryColor,
|
||||
size: size,
|
||||
tooltip: tooltip,
|
||||
notificationCount: notificationCount,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget iconOutline({
|
||||
required IconData icon,
|
||||
required VoidCallback onPressed,
|
||||
double? size,
|
||||
String? tooltip,
|
||||
Color? color,
|
||||
}) {
|
||||
return SophisticatedIconButton(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: IconButtonVariant.outlined,
|
||||
foregroundColor: color ?? AppTheme.primaryColor,
|
||||
borderColor: color ?? AppTheme.primaryColor,
|
||||
size: size,
|
||||
tooltip: tooltip,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget iconGhost({
|
||||
required IconData icon,
|
||||
required VoidCallback onPressed,
|
||||
double? size,
|
||||
String? tooltip,
|
||||
Color? color,
|
||||
}) {
|
||||
return SophisticatedIconButton(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: IconButtonVariant.ghost,
|
||||
backgroundColor: color ?? AppTheme.primaryColor,
|
||||
size: size,
|
||||
tooltip: tooltip,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget iconGradient({
|
||||
required IconData icon,
|
||||
required VoidCallback onPressed,
|
||||
double? size,
|
||||
String? tooltip,
|
||||
Gradient? gradient,
|
||||
}) {
|
||||
return SophisticatedIconButton(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: IconButtonVariant.gradient,
|
||||
gradient: gradient,
|
||||
size: size,
|
||||
tooltip: tooltip,
|
||||
);
|
||||
}
|
||||
|
||||
// FAB buttons
|
||||
static Widget fab({
|
||||
required VoidCallback onPressed,
|
||||
IconData icon = Icons.add,
|
||||
FABVariant variant = FABVariant.primary,
|
||||
FABSize size = FABSize.regular,
|
||||
String? tooltip,
|
||||
}) {
|
||||
return SophisticatedFAB(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: variant,
|
||||
size: size,
|
||||
tooltip: tooltip,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget fabExtended({
|
||||
required String label,
|
||||
required VoidCallback onPressed,
|
||||
IconData icon = Icons.add,
|
||||
FABVariant variant = FABVariant.primary,
|
||||
String? tooltip,
|
||||
}) {
|
||||
return SophisticatedFAB(
|
||||
icon: icon,
|
||||
label: label,
|
||||
onPressed: onPressed,
|
||||
variant: variant,
|
||||
size: FABSize.extended,
|
||||
tooltip: tooltip,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget fabGradient({
|
||||
required VoidCallback onPressed,
|
||||
IconData icon = Icons.add,
|
||||
FABSize size = FABSize.regular,
|
||||
Gradient? gradient,
|
||||
String? tooltip,
|
||||
}) {
|
||||
return SophisticatedFAB(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
variant: FABVariant.gradient,
|
||||
size: size,
|
||||
gradient: gradient,
|
||||
tooltip: tooltip,
|
||||
);
|
||||
}
|
||||
|
||||
static Widget fabMorphing({
|
||||
required VoidCallback onPressed,
|
||||
required List<IconData> icons,
|
||||
FABSize size = FABSize.regular,
|
||||
Duration morphingDuration = const Duration(seconds: 2),
|
||||
String? tooltip,
|
||||
}) {
|
||||
return SophisticatedFAB(
|
||||
onPressed: onPressed,
|
||||
variant: FABVariant.morphing,
|
||||
size: size,
|
||||
morphingIcons: icons,
|
||||
morphingDuration: morphingDuration,
|
||||
tooltip: tooltip,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
enum FABVariant {
|
||||
primary,
|
||||
secondary,
|
||||
gradient,
|
||||
glass,
|
||||
morphing,
|
||||
}
|
||||
|
||||
enum FABSize {
|
||||
small,
|
||||
regular,
|
||||
large,
|
||||
extended,
|
||||
}
|
||||
|
||||
class SophisticatedFAB extends StatefulWidget {
|
||||
final IconData? icon;
|
||||
final String? label;
|
||||
final VoidCallback? onPressed;
|
||||
final FABVariant variant;
|
||||
final FABSize size;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final Gradient? gradient;
|
||||
final bool animated;
|
||||
final bool showPulse;
|
||||
final List<IconData>? morphingIcons;
|
||||
final Duration morphingDuration;
|
||||
final String? tooltip;
|
||||
|
||||
const SophisticatedFAB({
|
||||
super.key,
|
||||
this.icon,
|
||||
this.label,
|
||||
this.onPressed,
|
||||
this.variant = FABVariant.primary,
|
||||
this.size = FABSize.regular,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.gradient,
|
||||
this.animated = true,
|
||||
this.showPulse = false,
|
||||
this.morphingIcons,
|
||||
this.morphingDuration = const Duration(seconds: 2),
|
||||
this.tooltip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SophisticatedFAB> createState() => _SophisticatedFABState();
|
||||
}
|
||||
|
||||
class _SophisticatedFABState extends State<SophisticatedFAB>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _scaleController;
|
||||
late AnimationController _rotationController;
|
||||
late AnimationController _pulseController;
|
||||
late AnimationController _morphingController;
|
||||
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _rotationAnimation;
|
||||
late Animation<double> _pulseAnimation;
|
||||
|
||||
int _currentMorphingIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_scaleController = AnimationController(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_rotationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_pulseController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_morphingController = AnimationController(
|
||||
duration: widget.morphingDuration,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.9,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _scaleController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_rotationAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _rotationController,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
|
||||
_pulseAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.2,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _pulseController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
if (widget.showPulse) {
|
||||
_pulseController.repeat(reverse: true);
|
||||
}
|
||||
|
||||
if (widget.morphingIcons != null && widget.morphingIcons!.isNotEmpty) {
|
||||
_startMorphing();
|
||||
}
|
||||
}
|
||||
|
||||
void _startMorphing() {
|
||||
_morphingController.addListener(() {
|
||||
if (_morphingController.isCompleted) {
|
||||
setState(() {
|
||||
_currentMorphingIndex =
|
||||
(_currentMorphingIndex + 1) % widget.morphingIcons!.length;
|
||||
});
|
||||
_morphingController.reset();
|
||||
_morphingController.forward();
|
||||
}
|
||||
});
|
||||
_morphingController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scaleController.dispose();
|
||||
_rotationController.dispose();
|
||||
_pulseController.dispose();
|
||||
_morphingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = _getFABConfig();
|
||||
|
||||
Widget fab = AnimatedBuilder(
|
||||
animation: Listenable.merge([
|
||||
_scaleController,
|
||||
_rotationController,
|
||||
_pulseController,
|
||||
]),
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: widget.animated
|
||||
? _scaleAnimation.value * (widget.showPulse ? _pulseAnimation.value : 1.0)
|
||||
: 1.0,
|
||||
child: Transform.rotate(
|
||||
angle: widget.animated ? _rotationAnimation.value * 0.1 : 0.0,
|
||||
child: Container(
|
||||
width: _getSize(),
|
||||
height: _getSize(),
|
||||
decoration: _getDecoration(config),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: _handleTap,
|
||||
onTapDown: widget.animated ? (_) => _scaleController.forward() : null,
|
||||
onTapUp: widget.animated ? (_) => _scaleController.reverse() : null,
|
||||
onTapCancel: widget.animated ? () => _scaleController.reverse() : null,
|
||||
customBorder: const CircleBorder(),
|
||||
child: _buildContent(config),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.tooltip != null) {
|
||||
fab = Tooltip(
|
||||
message: widget.tooltip!,
|
||||
child: fab,
|
||||
);
|
||||
}
|
||||
|
||||
return fab;
|
||||
}
|
||||
|
||||
Widget _buildContent(_FABConfig config) {
|
||||
if (widget.size == FABSize.extended && widget.label != null) {
|
||||
return _buildExtendedContent(config);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: _buildIcon(config),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildExtendedContent(_FABConfig config) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildIcon(config),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.label!,
|
||||
style: TextStyle(
|
||||
color: config.foregroundColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon(_FABConfig config) {
|
||||
IconData iconToShow = widget.icon ?? Icons.add;
|
||||
|
||||
if (widget.morphingIcons != null && widget.morphingIcons!.isNotEmpty) {
|
||||
iconToShow = widget.morphingIcons![_currentMorphingIndex];
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation,
|
||||
child: RotationTransition(
|
||||
turns: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
iconToShow,
|
||||
key: ValueKey(iconToShow),
|
||||
color: config.foregroundColor,
|
||||
size: _getIconSize(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_FABConfig _getFABConfig() {
|
||||
switch (widget.variant) {
|
||||
case FABVariant.primary:
|
||||
return _FABConfig(
|
||||
backgroundColor: widget.backgroundColor ?? AppTheme.primaryColor,
|
||||
foregroundColor: widget.foregroundColor ?? Colors.white,
|
||||
hasElevation: true,
|
||||
);
|
||||
|
||||
case FABVariant.secondary:
|
||||
return _FABConfig(
|
||||
backgroundColor: widget.backgroundColor ?? AppTheme.secondaryColor,
|
||||
foregroundColor: widget.foregroundColor ?? Colors.white,
|
||||
hasElevation: true,
|
||||
);
|
||||
|
||||
case FABVariant.gradient:
|
||||
return _FABConfig(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: widget.foregroundColor ?? Colors.white,
|
||||
hasElevation: true,
|
||||
useGradient: true,
|
||||
);
|
||||
|
||||
case FABVariant.glass:
|
||||
return _FABConfig(
|
||||
backgroundColor: Colors.white.withOpacity(0.2),
|
||||
foregroundColor: widget.foregroundColor ?? AppTheme.textPrimary,
|
||||
borderColor: Colors.white.withOpacity(0.3),
|
||||
hasElevation: true,
|
||||
isGlass: true,
|
||||
);
|
||||
|
||||
case FABVariant.morphing:
|
||||
return _FABConfig(
|
||||
backgroundColor: widget.backgroundColor ?? AppTheme.accentColor,
|
||||
foregroundColor: widget.foregroundColor ?? Colors.white,
|
||||
hasElevation: true,
|
||||
isMorphing: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Decoration _getDecoration(_FABConfig config) {
|
||||
if (config.useGradient) {
|
||||
return BoxDecoration(
|
||||
gradient: widget.gradient ?? LinearGradient(
|
||||
colors: [
|
||||
widget.backgroundColor ?? AppTheme.primaryColor,
|
||||
(widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.7),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: config.hasElevation ? _getShadow(config) : null,
|
||||
);
|
||||
}
|
||||
|
||||
return BoxDecoration(
|
||||
color: config.backgroundColor,
|
||||
shape: BoxShape.circle,
|
||||
border: config.borderColor != null
|
||||
? Border.all(color: config.borderColor!, width: 1)
|
||||
: null,
|
||||
boxShadow: config.hasElevation ? _getShadow(config) : null,
|
||||
);
|
||||
}
|
||||
|
||||
List<BoxShadow> _getShadow(_FABConfig config) {
|
||||
final shadowColor = config.useGradient
|
||||
? (widget.backgroundColor ?? AppTheme.primaryColor)
|
||||
: config.backgroundColor;
|
||||
|
||||
return [
|
||||
BoxShadow(
|
||||
color: shadowColor.withOpacity(0.4),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
BoxShadow(
|
||||
color: shadowColor.withOpacity(0.2),
|
||||
blurRadius: 40,
|
||||
offset: const Offset(0, 16),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
double _getSize() {
|
||||
switch (widget.size) {
|
||||
case FABSize.small:
|
||||
return 40;
|
||||
case FABSize.regular:
|
||||
return 56;
|
||||
case FABSize.large:
|
||||
return 72;
|
||||
case FABSize.extended:
|
||||
return 56; // Height for extended FAB
|
||||
}
|
||||
}
|
||||
|
||||
double _getIconSize() {
|
||||
switch (widget.size) {
|
||||
case FABSize.small:
|
||||
return 20;
|
||||
case FABSize.regular:
|
||||
return 24;
|
||||
case FABSize.large:
|
||||
return 32;
|
||||
case FABSize.extended:
|
||||
return 24;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
HapticFeedback.lightImpact();
|
||||
|
||||
if (widget.animated) {
|
||||
_rotationController.forward().then((_) {
|
||||
_rotationController.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
widget.onPressed?.call();
|
||||
}
|
||||
}
|
||||
|
||||
class _FABConfig {
|
||||
final Color backgroundColor;
|
||||
final Color foregroundColor;
|
||||
final Color? borderColor;
|
||||
final bool hasElevation;
|
||||
final bool useGradient;
|
||||
final bool isGlass;
|
||||
final bool isMorphing;
|
||||
|
||||
_FABConfig({
|
||||
required this.backgroundColor,
|
||||
required this.foregroundColor,
|
||||
this.borderColor,
|
||||
this.hasElevation = false,
|
||||
this.useGradient = false,
|
||||
this.isGlass = false,
|
||||
this.isMorphing = false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../badges/count_badge.dart';
|
||||
|
||||
enum IconButtonVariant {
|
||||
standard,
|
||||
filled,
|
||||
outlined,
|
||||
ghost,
|
||||
gradient,
|
||||
glass,
|
||||
}
|
||||
|
||||
enum IconButtonShape {
|
||||
circle,
|
||||
rounded,
|
||||
square,
|
||||
}
|
||||
|
||||
class SophisticatedIconButton extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onLongPress;
|
||||
final IconButtonVariant variant;
|
||||
final IconButtonShape shape;
|
||||
final double? size;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final Color? borderColor;
|
||||
final Gradient? gradient;
|
||||
final bool animated;
|
||||
final bool disabled;
|
||||
final String? tooltip;
|
||||
final Widget? badge;
|
||||
final int? notificationCount;
|
||||
final bool showPulse;
|
||||
|
||||
const SophisticatedIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.onPressed,
|
||||
this.onLongPress,
|
||||
this.variant = IconButtonVariant.standard,
|
||||
this.shape = IconButtonShape.circle,
|
||||
this.size,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.borderColor,
|
||||
this.gradient,
|
||||
this.animated = true,
|
||||
this.disabled = false,
|
||||
this.tooltip,
|
||||
this.badge,
|
||||
this.notificationCount,
|
||||
this.showPulse = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SophisticatedIconButton> createState() => _SophisticatedIconButtonState();
|
||||
}
|
||||
|
||||
class _SophisticatedIconButtonState extends State<SophisticatedIconButton>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _pressController;
|
||||
late AnimationController _pulseController;
|
||||
late AnimationController _rotationController;
|
||||
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _pulseAnimation;
|
||||
late Animation<double> _rotationAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_pressController = AnimationController(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_pulseController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_rotationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.9,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _pressController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_pulseAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.1,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _pulseController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_rotationAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 0.25,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _rotationController,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
|
||||
if (widget.showPulse) {
|
||||
_pulseController.repeat(reverse: true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pressController.dispose();
|
||||
_pulseController.dispose();
|
||||
_rotationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = _getButtonConfig();
|
||||
final buttonSize = widget.size ?? 48.0;
|
||||
final iconSize = buttonSize * 0.5;
|
||||
|
||||
Widget button = AnimatedBuilder(
|
||||
animation: Listenable.merge([_pressController, _pulseController, _rotationController]),
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: widget.animated
|
||||
? _scaleAnimation.value * (widget.showPulse ? _pulseAnimation.value : 1.0)
|
||||
: 1.0,
|
||||
child: Transform.rotate(
|
||||
angle: widget.animated ? _rotationAnimation.value : 0.0,
|
||||
child: Container(
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
decoration: _getDecoration(config, buttonSize),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: widget.disabled ? null : _handleTap,
|
||||
onLongPress: widget.disabled ? null : widget.onLongPress,
|
||||
onTapDown: widget.animated && !widget.disabled ? (_) => _pressController.forward() : null,
|
||||
onTapUp: widget.animated && !widget.disabled ? (_) => _pressController.reverse() : null,
|
||||
onTapCancel: widget.animated && !widget.disabled ? () => _pressController.reverse() : null,
|
||||
customBorder: _getInkWellBorder(buttonSize),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
size: iconSize,
|
||||
color: widget.disabled
|
||||
? AppTheme.textHint
|
||||
: config.foregroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Add badge if provided
|
||||
if (widget.badge != null || widget.notificationCount != null) {
|
||||
button = Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
button,
|
||||
if (widget.notificationCount != null)
|
||||
Positioned(
|
||||
top: -8,
|
||||
right: -8,
|
||||
child: CountBadge(
|
||||
count: widget.notificationCount!,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
if (widget.badge != null)
|
||||
Positioned(
|
||||
top: -4,
|
||||
right: -4,
|
||||
child: widget.badge!,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.tooltip != null) {
|
||||
button = Tooltip(
|
||||
message: widget.tooltip!,
|
||||
child: button,
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
_IconButtonConfig _getButtonConfig() {
|
||||
switch (widget.variant) {
|
||||
case IconButtonVariant.standard:
|
||||
return _IconButtonConfig(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: widget.foregroundColor ?? AppTheme.textPrimary,
|
||||
hasElevation: false,
|
||||
);
|
||||
|
||||
case IconButtonVariant.filled:
|
||||
return _IconButtonConfig(
|
||||
backgroundColor: widget.backgroundColor ?? AppTheme.primaryColor,
|
||||
foregroundColor: widget.foregroundColor ?? Colors.white,
|
||||
hasElevation: true,
|
||||
);
|
||||
|
||||
case IconButtonVariant.outlined:
|
||||
return _IconButtonConfig(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: widget.foregroundColor ?? AppTheme.primaryColor,
|
||||
borderColor: widget.borderColor ?? AppTheme.primaryColor,
|
||||
hasElevation: false,
|
||||
);
|
||||
|
||||
case IconButtonVariant.ghost:
|
||||
return _IconButtonConfig(
|
||||
backgroundColor: (widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.1),
|
||||
foregroundColor: widget.foregroundColor ?? AppTheme.primaryColor,
|
||||
hasElevation: false,
|
||||
);
|
||||
|
||||
case IconButtonVariant.gradient:
|
||||
return _IconButtonConfig(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: widget.foregroundColor ?? Colors.white,
|
||||
hasElevation: true,
|
||||
useGradient: true,
|
||||
);
|
||||
|
||||
case IconButtonVariant.glass:
|
||||
return _IconButtonConfig(
|
||||
backgroundColor: Colors.white.withOpacity(0.2),
|
||||
foregroundColor: widget.foregroundColor ?? AppTheme.textPrimary,
|
||||
borderColor: Colors.white.withOpacity(0.3),
|
||||
hasElevation: true,
|
||||
isGlass: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Decoration _getDecoration(_IconButtonConfig config, double size) {
|
||||
final borderRadius = _getBorderRadius(size);
|
||||
|
||||
if (config.useGradient) {
|
||||
return BoxDecoration(
|
||||
gradient: widget.gradient ?? LinearGradient(
|
||||
colors: [
|
||||
widget.backgroundColor ?? AppTheme.primaryColor,
|
||||
(widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.7),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: borderRadius,
|
||||
boxShadow: config.hasElevation ? _getShadow(config, size) : null,
|
||||
);
|
||||
}
|
||||
|
||||
return BoxDecoration(
|
||||
color: config.backgroundColor,
|
||||
borderRadius: borderRadius,
|
||||
border: config.borderColor != null
|
||||
? Border.all(color: config.borderColor!, width: 1.5)
|
||||
: null,
|
||||
boxShadow: config.hasElevation && !widget.disabled ? _getShadow(config, size) : null,
|
||||
);
|
||||
}
|
||||
|
||||
BorderRadius _getBorderRadius(double size) {
|
||||
switch (widget.shape) {
|
||||
case IconButtonShape.circle:
|
||||
return BorderRadius.circular(size / 2);
|
||||
case IconButtonShape.rounded:
|
||||
return BorderRadius.circular(size * 0.25);
|
||||
case IconButtonShape.square:
|
||||
return BorderRadius.circular(8);
|
||||
}
|
||||
}
|
||||
|
||||
ShapeBorder _getInkWellBorder(double size) {
|
||||
switch (widget.shape) {
|
||||
case IconButtonShape.circle:
|
||||
return const CircleBorder();
|
||||
case IconButtonShape.rounded:
|
||||
return RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(size * 0.25),
|
||||
);
|
||||
case IconButtonShape.square:
|
||||
return RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<BoxShadow> _getShadow(_IconButtonConfig config, double size) {
|
||||
final shadowColor = config.useGradient
|
||||
? (widget.backgroundColor ?? AppTheme.primaryColor)
|
||||
: config.backgroundColor;
|
||||
|
||||
return [
|
||||
BoxShadow(
|
||||
color: shadowColor.withOpacity(0.3),
|
||||
blurRadius: size * 0.3,
|
||||
offset: Offset(0, size * 0.1),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
if (widget.animated) {
|
||||
_rotationController.forward().then((_) {
|
||||
_rotationController.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
widget.onPressed?.call();
|
||||
}
|
||||
}
|
||||
|
||||
class _IconButtonConfig {
|
||||
final Color backgroundColor;
|
||||
final Color foregroundColor;
|
||||
final Color? borderColor;
|
||||
final bool hasElevation;
|
||||
final bool useGradient;
|
||||
final bool isGlass;
|
||||
|
||||
_IconButtonConfig({
|
||||
required this.backgroundColor,
|
||||
required this.foregroundColor,
|
||||
this.borderColor,
|
||||
this.hasElevation = false,
|
||||
this.useGradient = false,
|
||||
this.isGlass = false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
enum ButtonVariant {
|
||||
primary,
|
||||
secondary,
|
||||
outline,
|
||||
ghost,
|
||||
gradient,
|
||||
glass,
|
||||
danger,
|
||||
success,
|
||||
}
|
||||
|
||||
enum ButtonSize {
|
||||
small,
|
||||
medium,
|
||||
large,
|
||||
extraLarge,
|
||||
}
|
||||
|
||||
enum ButtonShape {
|
||||
rounded,
|
||||
circular,
|
||||
square,
|
||||
}
|
||||
|
||||
class SophisticatedButton extends StatefulWidget {
|
||||
final String? text;
|
||||
final Widget? child;
|
||||
final IconData? icon;
|
||||
final IconData? suffixIcon;
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onLongPress;
|
||||
final ButtonVariant variant;
|
||||
final ButtonSize size;
|
||||
final ButtonShape shape;
|
||||
final Color? backgroundColor;
|
||||
final Color? foregroundColor;
|
||||
final Gradient? gradient;
|
||||
final bool loading;
|
||||
final bool disabled;
|
||||
final bool animated;
|
||||
final bool showRipple;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final EdgeInsets? padding;
|
||||
final List<BoxShadow>? customShadow;
|
||||
final String? tooltip;
|
||||
final bool hapticFeedback;
|
||||
|
||||
const SophisticatedButton({
|
||||
super.key,
|
||||
this.text,
|
||||
this.child,
|
||||
this.icon,
|
||||
this.suffixIcon,
|
||||
this.onPressed,
|
||||
this.onLongPress,
|
||||
this.variant = ButtonVariant.primary,
|
||||
this.size = ButtonSize.medium,
|
||||
this.shape = ButtonShape.rounded,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.gradient,
|
||||
this.loading = false,
|
||||
this.disabled = false,
|
||||
this.animated = true,
|
||||
this.showRipple = true,
|
||||
this.width,
|
||||
this.height,
|
||||
this.padding,
|
||||
this.customShadow,
|
||||
this.tooltip,
|
||||
this.hapticFeedback = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SophisticatedButton> createState() => _SophisticatedButtonState();
|
||||
}
|
||||
|
||||
class _SophisticatedButtonState extends State<SophisticatedButton>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _pressController;
|
||||
late AnimationController _loadingController;
|
||||
late AnimationController _shimmerController;
|
||||
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _shadowAnimation;
|
||||
late Animation<double> _loadingAnimation;
|
||||
late Animation<double> _shimmerAnimation;
|
||||
|
||||
bool _isPressed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_pressController = AnimationController(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_loadingController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_shimmerController = AnimationController(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.95,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _pressController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_shadowAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.7,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _pressController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_loadingAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _loadingController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_shimmerAnimation = Tween<double>(
|
||||
begin: -1.0,
|
||||
end: 2.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _shimmerController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
if (widget.loading) {
|
||||
_loadingController.repeat();
|
||||
}
|
||||
|
||||
// Shimmer effect for premium buttons
|
||||
if (widget.variant == ButtonVariant.gradient) {
|
||||
_shimmerController.repeat();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(SophisticatedButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget.loading != oldWidget.loading) {
|
||||
if (widget.loading) {
|
||||
_loadingController.repeat();
|
||||
} else {
|
||||
_loadingController.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pressController.dispose();
|
||||
_loadingController.dispose();
|
||||
_shimmerController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = _getButtonConfig();
|
||||
final isDisabled = widget.disabled || widget.loading;
|
||||
|
||||
Widget button = AnimatedBuilder(
|
||||
animation: Listenable.merge([_pressController, _loadingController, _shimmerController]),
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: widget.animated ? _scaleAnimation.value : 1.0,
|
||||
child: Container(
|
||||
width: widget.width,
|
||||
height: widget.height ?? _getHeight(),
|
||||
padding: widget.padding ?? _getPadding(),
|
||||
decoration: _getDecoration(config),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: isDisabled ? null : _handleTap,
|
||||
onLongPress: isDisabled ? null : widget.onLongPress,
|
||||
onTapDown: widget.animated && !isDisabled ? (_) => _pressController.forward() : null,
|
||||
onTapUp: widget.animated && !isDisabled ? (_) => _pressController.reverse() : null,
|
||||
onTapCancel: widget.animated && !isDisabled ? () => _pressController.reverse() : null,
|
||||
borderRadius: _getBorderRadius(),
|
||||
splashColor: widget.showRipple ? config.foregroundColor.withOpacity(0.2) : Colors.transparent,
|
||||
highlightColor: widget.showRipple ? config.foregroundColor.withOpacity(0.1) : Colors.transparent,
|
||||
child: _buildContent(config),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.tooltip != null) {
|
||||
button = Tooltip(
|
||||
message: widget.tooltip!,
|
||||
child: button,
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
Widget _buildContent(_ButtonConfig config) {
|
||||
final hasIcon = widget.icon != null;
|
||||
final hasSuffixIcon = widget.suffixIcon != null;
|
||||
final hasText = widget.text != null || widget.child != null;
|
||||
|
||||
if (widget.loading) {
|
||||
return _buildLoadingContent(config);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (hasIcon) ...[
|
||||
_buildIcon(widget.icon!, config),
|
||||
if (hasText) SizedBox(width: _getIconSpacing()),
|
||||
],
|
||||
if (hasText) ...[
|
||||
Flexible(child: _buildText(config)),
|
||||
],
|
||||
if (hasSuffixIcon) ...[
|
||||
if (hasText || hasIcon) SizedBox(width: _getIconSpacing()),
|
||||
_buildIcon(widget.suffixIcon!, config),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingContent(_ButtonConfig config) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: _getIconSize(),
|
||||
height: _getIconSize(),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(config.foregroundColor),
|
||||
),
|
||||
),
|
||||
if (widget.text != null) ...[
|
||||
SizedBox(width: _getIconSpacing()),
|
||||
Text(
|
||||
'Chargement...',
|
||||
style: _getTextStyle(config),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIcon(IconData icon, _ButtonConfig config) {
|
||||
return Icon(
|
||||
icon,
|
||||
size: _getIconSize(),
|
||||
color: config.foregroundColor,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildText(_ButtonConfig config) {
|
||||
if (widget.child != null) {
|
||||
return DefaultTextStyle(
|
||||
style: _getTextStyle(config),
|
||||
child: widget.child!,
|
||||
);
|
||||
}
|
||||
|
||||
return Text(
|
||||
widget.text!,
|
||||
style: _getTextStyle(config),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
||||
_ButtonConfig _getButtonConfig() {
|
||||
final isDisabled = widget.disabled || widget.loading;
|
||||
|
||||
switch (widget.variant) {
|
||||
case ButtonVariant.primary:
|
||||
return _ButtonConfig(
|
||||
backgroundColor: isDisabled
|
||||
? AppTheme.textHint
|
||||
: (widget.backgroundColor ?? AppTheme.primaryColor),
|
||||
foregroundColor: isDisabled
|
||||
? AppTheme.textSecondary
|
||||
: (widget.foregroundColor ?? Colors.white),
|
||||
hasElevation: true,
|
||||
);
|
||||
|
||||
case ButtonVariant.secondary:
|
||||
return _ButtonConfig(
|
||||
backgroundColor: isDisabled
|
||||
? AppTheme.backgroundLight
|
||||
: (widget.backgroundColor ?? AppTheme.secondaryColor),
|
||||
foregroundColor: isDisabled
|
||||
? AppTheme.textHint
|
||||
: (widget.foregroundColor ?? Colors.white),
|
||||
hasElevation: true,
|
||||
);
|
||||
|
||||
case ButtonVariant.outline:
|
||||
return _ButtonConfig(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: isDisabled
|
||||
? AppTheme.textHint
|
||||
: (widget.foregroundColor ?? AppTheme.primaryColor),
|
||||
borderColor: isDisabled
|
||||
? AppTheme.textHint
|
||||
: (widget.backgroundColor ?? AppTheme.primaryColor),
|
||||
hasElevation: false,
|
||||
);
|
||||
|
||||
case ButtonVariant.ghost:
|
||||
return _ButtonConfig(
|
||||
backgroundColor: isDisabled
|
||||
? Colors.transparent
|
||||
: (widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.1),
|
||||
foregroundColor: isDisabled
|
||||
? AppTheme.textHint
|
||||
: (widget.foregroundColor ?? AppTheme.primaryColor),
|
||||
hasElevation: false,
|
||||
);
|
||||
|
||||
case ButtonVariant.gradient:
|
||||
return _ButtonConfig(
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: isDisabled
|
||||
? AppTheme.textHint
|
||||
: (widget.foregroundColor ?? Colors.white),
|
||||
hasElevation: true,
|
||||
useGradient: true,
|
||||
);
|
||||
|
||||
case ButtonVariant.glass:
|
||||
return _ButtonConfig(
|
||||
backgroundColor: isDisabled
|
||||
? Colors.grey.withOpacity(0.1)
|
||||
: Colors.white.withOpacity(0.2),
|
||||
foregroundColor: isDisabled
|
||||
? AppTheme.textHint
|
||||
: (widget.foregroundColor ?? AppTheme.textPrimary),
|
||||
borderColor: Colors.white.withOpacity(0.3),
|
||||
hasElevation: true,
|
||||
isGlass: true,
|
||||
);
|
||||
|
||||
case ButtonVariant.danger:
|
||||
return _ButtonConfig(
|
||||
backgroundColor: isDisabled
|
||||
? AppTheme.textHint
|
||||
: AppTheme.errorColor,
|
||||
foregroundColor: isDisabled
|
||||
? AppTheme.textSecondary
|
||||
: Colors.white,
|
||||
hasElevation: true,
|
||||
);
|
||||
|
||||
case ButtonVariant.success:
|
||||
return _ButtonConfig(
|
||||
backgroundColor: isDisabled
|
||||
? AppTheme.textHint
|
||||
: AppTheme.successColor,
|
||||
foregroundColor: isDisabled
|
||||
? AppTheme.textSecondary
|
||||
: Colors.white,
|
||||
hasElevation: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Decoration _getDecoration(_ButtonConfig config) {
|
||||
final borderRadius = _getBorderRadius();
|
||||
final isDisabled = widget.disabled || widget.loading;
|
||||
|
||||
if (config.useGradient && !isDisabled) {
|
||||
return BoxDecoration(
|
||||
gradient: widget.gradient ?? LinearGradient(
|
||||
colors: [
|
||||
widget.backgroundColor ?? AppTheme.primaryColor,
|
||||
(widget.backgroundColor ?? AppTheme.primaryColor).withOpacity(0.7),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: borderRadius,
|
||||
boxShadow: config.hasElevation ? _getShadow(config) : null,
|
||||
);
|
||||
}
|
||||
|
||||
return BoxDecoration(
|
||||
color: config.backgroundColor,
|
||||
borderRadius: borderRadius,
|
||||
border: config.borderColor != null
|
||||
? Border.all(color: config.borderColor!, width: 1.5)
|
||||
: null,
|
||||
boxShadow: config.hasElevation && !isDisabled ? _getShadow(config) : null,
|
||||
);
|
||||
}
|
||||
|
||||
List<BoxShadow> _getShadow(_ButtonConfig config) {
|
||||
if (widget.customShadow != null) {
|
||||
return widget.customShadow!.map((shadow) => BoxShadow(
|
||||
color: shadow.color.withOpacity(shadow.color.opacity * _shadowAnimation.value),
|
||||
blurRadius: shadow.blurRadius * _shadowAnimation.value,
|
||||
offset: shadow.offset * _shadowAnimation.value,
|
||||
spreadRadius: shadow.spreadRadius,
|
||||
)).toList();
|
||||
}
|
||||
|
||||
final shadowColor = config.useGradient
|
||||
? (widget.backgroundColor ?? AppTheme.primaryColor)
|
||||
: config.backgroundColor;
|
||||
|
||||
return [
|
||||
BoxShadow(
|
||||
color: shadowColor.withOpacity(0.3 * _shadowAnimation.value),
|
||||
blurRadius: 15 * _shadowAnimation.value,
|
||||
offset: Offset(0, 8 * _shadowAnimation.value),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
BorderRadius _getBorderRadius() {
|
||||
switch (widget.shape) {
|
||||
case ButtonShape.rounded:
|
||||
return BorderRadius.circular(_getHeight() / 2);
|
||||
case ButtonShape.circular:
|
||||
return BorderRadius.circular(_getHeight());
|
||||
case ButtonShape.square:
|
||||
return BorderRadius.circular(8);
|
||||
}
|
||||
}
|
||||
|
||||
double _getHeight() {
|
||||
switch (widget.size) {
|
||||
case ButtonSize.small:
|
||||
return 32;
|
||||
case ButtonSize.medium:
|
||||
return 44;
|
||||
case ButtonSize.large:
|
||||
return 56;
|
||||
case ButtonSize.extraLarge:
|
||||
return 72;
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsets _getPadding() {
|
||||
switch (widget.size) {
|
||||
case ButtonSize.small:
|
||||
return const EdgeInsets.symmetric(horizontal: 16, vertical: 6);
|
||||
case ButtonSize.medium:
|
||||
return const EdgeInsets.symmetric(horizontal: 24, vertical: 12);
|
||||
case ButtonSize.large:
|
||||
return const EdgeInsets.symmetric(horizontal: 32, vertical: 16);
|
||||
case ButtonSize.extraLarge:
|
||||
return const EdgeInsets.symmetric(horizontal: 40, vertical: 20);
|
||||
}
|
||||
}
|
||||
|
||||
double _getFontSize() {
|
||||
switch (widget.size) {
|
||||
case ButtonSize.small:
|
||||
return 14;
|
||||
case ButtonSize.medium:
|
||||
return 16;
|
||||
case ButtonSize.large:
|
||||
return 18;
|
||||
case ButtonSize.extraLarge:
|
||||
return 20;
|
||||
}
|
||||
}
|
||||
|
||||
double _getIconSize() {
|
||||
switch (widget.size) {
|
||||
case ButtonSize.small:
|
||||
return 16;
|
||||
case ButtonSize.medium:
|
||||
return 20;
|
||||
case ButtonSize.large:
|
||||
return 24;
|
||||
case ButtonSize.extraLarge:
|
||||
return 28;
|
||||
}
|
||||
}
|
||||
|
||||
double _getIconSpacing() {
|
||||
switch (widget.size) {
|
||||
case ButtonSize.small:
|
||||
return 6;
|
||||
case ButtonSize.medium:
|
||||
return 8;
|
||||
case ButtonSize.large:
|
||||
return 10;
|
||||
case ButtonSize.extraLarge:
|
||||
return 12;
|
||||
}
|
||||
}
|
||||
|
||||
TextStyle _getTextStyle(_ButtonConfig config) {
|
||||
return TextStyle(
|
||||
fontSize: _getFontSize(),
|
||||
fontWeight: FontWeight.w600,
|
||||
color: config.foregroundColor,
|
||||
letterSpacing: 0.5,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
if (widget.hapticFeedback) {
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
widget.onPressed?.call();
|
||||
}
|
||||
}
|
||||
|
||||
class _ButtonConfig {
|
||||
final Color backgroundColor;
|
||||
final Color foregroundColor;
|
||||
final Color? borderColor;
|
||||
final bool hasElevation;
|
||||
final bool useGradient;
|
||||
final bool isGlass;
|
||||
|
||||
_ButtonConfig({
|
||||
required this.backgroundColor,
|
||||
required this.foregroundColor,
|
||||
this.borderColor,
|
||||
this.hasElevation = false,
|
||||
this.useGradient = false,
|
||||
this.isGlass = false,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user