400 lines
10 KiB
Dart
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,
|
|
});
|
|
} |