356 lines
9.9 KiB
Dart
356 lines
9.9 KiB
Dart
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,
|
|
});
|
|
} |