Files
unionflow-server-impl-quarkus/unionflow-mobile-apps/lib/shared/widgets/buttons/floating_action_button.dart
2025-08-20 21:00:35 +00:00

400 lines
10 KiB
Dart

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