321 lines
9.0 KiB
Dart
321 lines
9.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../shared/theme/app_theme.dart';
|
|
|
|
/// Bouton animé avec effets visuels sophistiqués
|
|
class AnimatedButton extends StatefulWidget {
|
|
final String text;
|
|
final IconData? icon;
|
|
final VoidCallback? onPressed;
|
|
final Color? backgroundColor;
|
|
final Color? foregroundColor;
|
|
final double? width;
|
|
final double? height;
|
|
final bool isLoading;
|
|
final AnimatedButtonStyle style;
|
|
|
|
const AnimatedButton({
|
|
super.key,
|
|
required this.text,
|
|
this.icon,
|
|
this.onPressed,
|
|
this.backgroundColor,
|
|
this.foregroundColor,
|
|
this.width,
|
|
this.height,
|
|
this.isLoading = false,
|
|
this.style = AnimatedButtonStyle.primary,
|
|
});
|
|
|
|
@override
|
|
State<AnimatedButton> createState() => _AnimatedButtonState();
|
|
}
|
|
|
|
class _AnimatedButtonState extends State<AnimatedButton>
|
|
with TickerProviderStateMixin {
|
|
late AnimationController _scaleController;
|
|
late AnimationController _shimmerController;
|
|
late AnimationController _loadingController;
|
|
|
|
late Animation<double> _scaleAnimation;
|
|
late Animation<double> _shimmerAnimation;
|
|
late Animation<double> _loadingAnimation;
|
|
|
|
bool _isPressed = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_scaleController = AnimationController(
|
|
duration: const Duration(milliseconds: 150),
|
|
vsync: this,
|
|
);
|
|
|
|
_shimmerController = AnimationController(
|
|
duration: const Duration(milliseconds: 1500),
|
|
vsync: this,
|
|
);
|
|
|
|
_loadingController = AnimationController(
|
|
duration: const Duration(milliseconds: 1000),
|
|
vsync: this,
|
|
);
|
|
|
|
_scaleAnimation = Tween<double>(
|
|
begin: 1.0,
|
|
end: 0.95,
|
|
).animate(CurvedAnimation(
|
|
parent: _scaleController,
|
|
curve: Curves.easeInOut,
|
|
));
|
|
|
|
_shimmerAnimation = Tween<double>(
|
|
begin: -1.0,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(
|
|
parent: _shimmerController,
|
|
curve: Curves.easeInOut,
|
|
));
|
|
|
|
_loadingAnimation = Tween<double>(
|
|
begin: 0.0,
|
|
end: 1.0,
|
|
).animate(CurvedAnimation(
|
|
parent: _loadingController,
|
|
curve: Curves.easeInOut,
|
|
));
|
|
|
|
if (widget.isLoading) {
|
|
_loadingController.repeat();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(AnimatedButton oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (widget.isLoading != oldWidget.isLoading) {
|
|
if (widget.isLoading) {
|
|
_loadingController.repeat();
|
|
} else {
|
|
_loadingController.stop();
|
|
_loadingController.reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scaleController.dispose();
|
|
_shimmerController.dispose();
|
|
_loadingController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onTapDown(TapDownDetails details) {
|
|
if (widget.onPressed != null && !widget.isLoading) {
|
|
setState(() => _isPressed = true);
|
|
_scaleController.forward();
|
|
}
|
|
}
|
|
|
|
void _onTapUp(TapUpDetails details) {
|
|
if (widget.onPressed != null && !widget.isLoading) {
|
|
setState(() => _isPressed = false);
|
|
_scaleController.reverse();
|
|
_shimmerController.forward().then((_) {
|
|
_shimmerController.reset();
|
|
});
|
|
}
|
|
}
|
|
|
|
void _onTapCancel() {
|
|
if (widget.onPressed != null && !widget.isLoading) {
|
|
setState(() => _isPressed = false);
|
|
_scaleController.reverse();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colors = _getColors();
|
|
|
|
return AnimatedBuilder(
|
|
animation: Listenable.merge([_scaleAnimation, _shimmerAnimation, _loadingAnimation]),
|
|
builder: (context, child) {
|
|
return Transform.scale(
|
|
scale: _scaleAnimation.value,
|
|
child: GestureDetector(
|
|
onTapDown: _onTapDown,
|
|
onTapUp: _onTapUp,
|
|
onTapCancel: _onTapCancel,
|
|
onTap: widget.onPressed != null && !widget.isLoading ? widget.onPressed : null,
|
|
child: Container(
|
|
width: widget.width,
|
|
height: widget.height ?? 56,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
colors.backgroundColor,
|
|
colors.backgroundColor.withOpacity(0.8),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: colors.backgroundColor.withOpacity(0.3),
|
|
blurRadius: _isPressed ? 4 : 8,
|
|
offset: Offset(0, _isPressed ? 2 : 4),
|
|
),
|
|
],
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
// Effet shimmer
|
|
if (!widget.isLoading)
|
|
Positioned.fill(
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: AnimatedBuilder(
|
|
animation: _shimmerAnimation,
|
|
builder: (context, child) {
|
|
return Transform.translate(
|
|
offset: Offset(_shimmerAnimation.value * 200, 0),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Colors.transparent,
|
|
Colors.white.withOpacity(0.2),
|
|
Colors.transparent,
|
|
],
|
|
stops: const [0.0, 0.5, 1.0],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
// Contenu du bouton
|
|
Center(
|
|
child: widget.isLoading
|
|
? _buildLoadingContent(colors)
|
|
: _buildNormalContent(colors),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildLoadingContent(_ButtonColors colors) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(colors.foregroundColor),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Text(
|
|
'Chargement...',
|
|
style: TextStyle(
|
|
color: colors.foregroundColor,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildNormalContent(_ButtonColors colors) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (widget.icon != null) ...[
|
|
Icon(
|
|
widget.icon,
|
|
color: colors.foregroundColor,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
Text(
|
|
widget.text,
|
|
style: TextStyle(
|
|
color: colors.foregroundColor,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
_ButtonColors _getColors() {
|
|
switch (widget.style) {
|
|
case AnimatedButtonStyle.primary:
|
|
return _ButtonColors(
|
|
backgroundColor: widget.backgroundColor ?? AppTheme.primaryColor,
|
|
foregroundColor: widget.foregroundColor ?? Colors.white,
|
|
);
|
|
case AnimatedButtonStyle.secondary:
|
|
return _ButtonColors(
|
|
backgroundColor: widget.backgroundColor ?? AppTheme.secondaryColor,
|
|
foregroundColor: widget.foregroundColor ?? Colors.white,
|
|
);
|
|
case AnimatedButtonStyle.success:
|
|
return _ButtonColors(
|
|
backgroundColor: widget.backgroundColor ?? AppTheme.successColor,
|
|
foregroundColor: widget.foregroundColor ?? Colors.white,
|
|
);
|
|
case AnimatedButtonStyle.warning:
|
|
return _ButtonColors(
|
|
backgroundColor: widget.backgroundColor ?? AppTheme.warningColor,
|
|
foregroundColor: widget.foregroundColor ?? Colors.white,
|
|
);
|
|
case AnimatedButtonStyle.error:
|
|
return _ButtonColors(
|
|
backgroundColor: widget.backgroundColor ?? AppTheme.errorColor,
|
|
foregroundColor: widget.foregroundColor ?? Colors.white,
|
|
);
|
|
case AnimatedButtonStyle.outline:
|
|
return _ButtonColors(
|
|
backgroundColor: widget.backgroundColor ?? Colors.transparent,
|
|
foregroundColor: widget.foregroundColor ?? AppTheme.primaryColor,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class _ButtonColors {
|
|
final Color backgroundColor;
|
|
final Color foregroundColor;
|
|
|
|
_ButtonColors({
|
|
required this.backgroundColor,
|
|
required this.foregroundColor,
|
|
});
|
|
}
|
|
|
|
enum AnimatedButtonStyle {
|
|
primary,
|
|
secondary,
|
|
success,
|
|
warning,
|
|
error,
|
|
outline,
|
|
}
|