import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../core/constants/design_system.dart'; /// Widget bouton avec animation de scale au tap /// /// Ajoute un effet de réduction de taille lors du tap, /// avec feedback haptique optionnel. /// /// **Usage:** /// ```dart /// AnimatedScaleButton( /// onTap: () => print('Tapped'), /// child: Container( /// padding: EdgeInsets.all(16), /// child: Text('Tap me'), /// ), /// ) /// ``` class AnimatedScaleButton extends StatefulWidget { const AnimatedScaleButton({ required this.onTap, required this.child, this.scaleFactor = 0.95, this.duration, this.hapticFeedback = true, super.key, }); final VoidCallback? onTap; final Widget child; final double scaleFactor; final Duration? duration; final bool hapticFeedback; @override State createState() => _AnimatedScaleButtonState(); } class _AnimatedScaleButtonState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnimation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: widget.duration ?? DesignSystem.durationFast, ); _scaleAnimation = Tween( begin: 1.0, end: widget.scaleFactor, ).animate( CurvedAnimation( parent: _controller, curve: DesignSystem.curveDecelerate, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } void _handleTapDown(TapDownDetails details) { if (widget.onTap != null) { _controller.forward(); if (widget.hapticFeedback) { HapticFeedback.lightImpact(); } } } void _handleTapUp(TapUpDetails details) { if (widget.onTap != null) { _controller.reverse(); } } void _handleTapCancel() { _controller.reverse(); } @override Widget build(BuildContext context) { return GestureDetector( onTapDown: _handleTapDown, onTapUp: _handleTapUp, onTapCancel: _handleTapCancel, onTap: widget.onTap, child: ScaleTransition( scale: _scaleAnimation, child: widget.child, ), ); } } /// Widget avec animation de bounce au tap /// /// Ajoute un effet de rebond élastique lors du tap. class AnimatedBounceButton extends StatefulWidget { const AnimatedBounceButton({ required this.onTap, required this.child, this.hapticFeedback = true, super.key, }); final VoidCallback? onTap; final Widget child; final bool hapticFeedback; @override State createState() => _AnimatedBounceButtonState(); } class _AnimatedBounceButtonState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnimation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: DesignSystem.durationMedium, ); _scaleAnimation = TweenSequence([ TweenSequenceItem( tween: Tween(begin: 1.0, end: 0.9), weight: 1, ), TweenSequenceItem( tween: Tween(begin: 0.9, end: 1.1), weight: 1, ), TweenSequenceItem( tween: Tween(begin: 1.1, end: 1.0), weight: 1, ), ]).animate( CurvedAnimation( parent: _controller, curve: DesignSystem.curveBounce, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } void _handleTap() { if (widget.onTap != null) { if (widget.hapticFeedback) { HapticFeedback.mediumImpact(); } _controller.forward(from: 0); widget.onTap!(); } } @override Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, child: ScaleTransition( scale: _scaleAnimation, child: widget.child, ), ); } } /// Widget avec fade in animation /// /// Apparait en fondu avec translation optionnelle. class FadeInWidget extends StatefulWidget { const FadeInWidget({ required this.child, this.duration, this.delay = Duration.zero, this.offset = const Offset(0, 20), this.curve, super.key, }); final Widget child; final Duration? duration; final Duration delay; final Offset offset; final Curve? curve; @override State createState() => _FadeInWidgetState(); } class _FadeInWidgetState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _opacityAnimation; late Animation _slideAnimation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: widget.duration ?? DesignSystem.durationMedium, ); _opacityAnimation = Tween( begin: 0.0, end: 1.0, ).animate( CurvedAnimation( parent: _controller, curve: widget.curve ?? DesignSystem.curveDecelerate, ), ); _slideAnimation = Tween( begin: widget.offset, end: Offset.zero, ).animate( CurvedAnimation( parent: _controller, curve: widget.curve ?? DesignSystem.curveDecelerate, ), ); // Démarrer l'animation après le délai Future.delayed(widget.delay, () { if (mounted) { _controller.forward(); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return FadeTransition( opacity: _opacityAnimation, child: Transform.translate( offset: _slideAnimation.value, child: widget.child, ), ); } } /// Widget avec animation staggered pour listes /// /// Anime chaque enfant avec un délai progressif. class StaggeredListAnimation extends StatelessWidget { const StaggeredListAnimation({ required this.children, this.staggerDelay = const Duration(milliseconds: 50), this.initialDelay = Duration.zero, super.key, }); final List children; final Duration staggerDelay; final Duration initialDelay; @override Widget build(BuildContext context) { return Column( children: List.generate( children.length, (index) => FadeInWidget( delay: initialDelay + (staggerDelay * index), child: children[index], ), ), ); } } /// Card animée avec effet de élévation au hover /// /// Augmente l'élévation et la taille au survol/tap. class AnimatedCard extends StatefulWidget { const AnimatedCard({ required this.child, this.onTap, this.elevation = 2.0, this.hoverElevation = 8.0, this.borderRadius, this.padding, this.margin, super.key, }); final Widget child; final VoidCallback? onTap; final double elevation; final double hoverElevation; final BorderRadius? borderRadius; final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? margin; @override State createState() => _AnimatedCardState(); } class _AnimatedCardState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _elevationAnimation; late Animation _scaleAnimation; bool _isHovered = false; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: DesignSystem.durationFast, ); _elevationAnimation = Tween( begin: widget.elevation, end: widget.hoverElevation, ).animate( CurvedAnimation( parent: _controller, curve: DesignSystem.curveDecelerate, ), ); _scaleAnimation = Tween( begin: 1.0, end: 1.02, ).animate( CurvedAnimation( parent: _controller, curve: DesignSystem.curveDecelerate, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } void _handleHoverEnter() { setState(() => _isHovered = true); _controller.forward(); } void _handleHoverExit() { setState(() => _isHovered = false); _controller.reverse(); } void _handleTapDown(TapDownDetails details) { _controller.forward(); HapticFeedback.lightImpact(); } void _handleTapUp(TapUpDetails details) { if (!_isHovered) { _controller.reverse(); } } @override Widget build(BuildContext context) { return MouseRegion( onEnter: (_) => _handleHoverEnter(), onExit: (_) => _handleHoverExit(), child: GestureDetector( onTapDown: widget.onTap != null ? _handleTapDown : null, onTapUp: widget.onTap != null ? _handleTapUp : null, onTap: widget.onTap, child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Container( margin: widget.margin, child: Material( elevation: _elevationAnimation.value, borderRadius: widget.borderRadius ?? DesignSystem.borderRadiusLg, color: Theme.of(context).cardColor, child: Container( padding: widget.padding ?? DesignSystem.paddingAll(DesignSystem.spacingLg), child: widget.child, ), ), ), ); }, ), ), ); } } /// Loading button avec animation /// /// Bouton qui affiche un loader pendant l'exécution d'une action async. class AnimatedLoadingButton extends StatefulWidget { const AnimatedLoadingButton({ required this.onPressed, required this.child, this.backgroundColor, this.foregroundColor, this.borderRadius, this.padding, this.height = 48, super.key, }); final Future Function() onPressed; final Widget child; final Color? backgroundColor; final Color? foregroundColor; final BorderRadius? borderRadius; final EdgeInsetsGeometry? padding; final double height; @override State createState() => _AnimatedLoadingButtonState(); } class _AnimatedLoadingButtonState extends State { bool _isLoading = false; Future _handlePress() async { if (_isLoading) return; setState(() => _isLoading = true); HapticFeedback.mediumImpact(); try { await widget.onPressed(); } finally { if (mounted) { setState(() => _isLoading = false); } } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return AnimatedScaleButton( onTap: _isLoading ? null : _handlePress, child: AnimatedContainer( duration: DesignSystem.durationMedium, curve: DesignSystem.curveDecelerate, height: widget.height, width: _isLoading ? widget.height : null, padding: _isLoading ? EdgeInsets.zero : (widget.padding ?? DesignSystem.paddingHorizontal(DesignSystem.spacing2xl)), decoration: BoxDecoration( color: widget.backgroundColor ?? theme.colorScheme.primary, borderRadius: widget.borderRadius ?? DesignSystem.borderRadiusLg, ), child: Center( child: _isLoading ? SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( widget.foregroundColor ?? theme.colorScheme.onPrimary, ), ), ) : DefaultTextStyle( style: TextStyle( color: widget.foregroundColor ?? theme.colorScheme.onPrimary, fontSize: 16, fontWeight: FontWeight.w600, ), child: widget.child, ), ), ), ); } } /// Pulse animation pour attirer l'attention class PulseAnimation extends StatefulWidget { const PulseAnimation({ required this.child, this.duration = const Duration(milliseconds: 1000), this.minScale = 0.95, this.maxScale = 1.05, super.key, }); final Widget child; final Duration duration; final double minScale; final double maxScale; @override State createState() => _PulseAnimationState(); } class _PulseAnimationState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: widget.duration, )..repeat(reverse: true); _animation = Tween( begin: widget.minScale, end: widget.maxScale, ).animate( CurvedAnimation( parent: _controller, curve: Curves.easeInOut, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ScaleTransition( scale: _animation, child: widget.child, ); } }