import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; /// Widget avec micro-interactions pour les boutons class InteractiveButton extends StatefulWidget { final Widget child; final VoidCallback? onPressed; final Color? backgroundColor; final Color? foregroundColor; final EdgeInsetsGeometry? padding; final BorderRadius? borderRadius; final bool enableHapticFeedback; final bool enableSoundFeedback; final Duration animationDuration; const InteractiveButton({ super.key, required this.child, this.onPressed, this.backgroundColor, this.foregroundColor, this.padding, this.borderRadius, this.enableHapticFeedback = true, this.enableSoundFeedback = false, this.animationDuration = const Duration(milliseconds: 150), }); @override State createState() => _InteractiveButtonState(); } class _InteractiveButtonState extends State with TickerProviderStateMixin { late AnimationController _scaleController; late AnimationController _rippleController; late Animation _scaleAnimation; late Animation _rippleAnimation; bool _isPressed = false; @override void initState() { super.initState(); _scaleController = AnimationController( duration: widget.animationDuration, vsync: this, ); _rippleController = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _scaleAnimation = Tween( begin: 1.0, end: 0.95, ).animate(CurvedAnimation( parent: _scaleController, curve: Curves.easeInOut, )); _rippleAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _rippleController, curve: Curves.easeOut, )); } @override void dispose() { _scaleController.dispose(); _rippleController.dispose(); super.dispose(); } void _handleTapDown(TapDownDetails details) { if (widget.onPressed != null) { setState(() => _isPressed = true); _scaleController.forward(); _rippleController.forward(); if (widget.enableHapticFeedback) { HapticFeedback.lightImpact(); } } } void _handleTapUp(TapUpDetails details) { _handleTapEnd(); } void _handleTapCancel() { _handleTapEnd(); } void _handleTapEnd() { if (_isPressed) { setState(() => _isPressed = false); _scaleController.reverse(); Future.delayed(const Duration(milliseconds: 100), () { _rippleController.reverse(); }); } } void _handleTap() { if (widget.onPressed != null) { if (widget.enableSoundFeedback) { SystemSound.play(SystemSoundType.click); } widget.onPressed!(); } } @override Widget build(BuildContext context) { return GestureDetector( onTapDown: _handleTapDown, onTapUp: _handleTapUp, onTapCancel: _handleTapCancel, onTap: _handleTap, child: AnimatedBuilder( animation: Listenable.merge([_scaleAnimation, _rippleAnimation]), builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Container( padding: widget.padding ?? const EdgeInsets.symmetric( horizontal: 24, vertical: 12, ), decoration: BoxDecoration( color: widget.backgroundColor ?? Theme.of(context).primaryColor, borderRadius: widget.borderRadius ?? BorderRadius.circular(8), boxShadow: [ BoxShadow( color: (widget.backgroundColor ?? Theme.of(context).primaryColor) .withOpacity(0.3), blurRadius: _isPressed ? 8 : 12, offset: Offset(0, _isPressed ? 2 : 4), spreadRadius: _isPressed ? 0 : 2, ), ], ), child: Stack( alignment: Alignment.center, children: [ // Effet de ripple if (_rippleAnimation.value > 0) Positioned.fill( child: Container( decoration: BoxDecoration( borderRadius: widget.borderRadius ?? BorderRadius.circular(8), color: Colors.white.withOpacity( 0.2 * _rippleAnimation.value, ), ), ), ), // Contenu du bouton DefaultTextStyle( style: TextStyle( color: widget.foregroundColor ?? Colors.white, fontWeight: FontWeight.w600, ), child: widget.child, ), ], ), ), ); }, ), ); } } /// Widget avec effet de parallax pour les cartes class ParallaxCard extends StatefulWidget { final Widget child; final double parallaxOffset; final Duration animationDuration; const ParallaxCard({ super.key, required this.child, this.parallaxOffset = 20.0, this.animationDuration = const Duration(milliseconds: 200), }); @override State createState() => _ParallaxCardState(); } class _ParallaxCardState extends State with TickerProviderStateMixin { late AnimationController _controller; late Animation _offsetAnimation; late Animation _elevationAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: widget.animationDuration, vsync: this, ); _offsetAnimation = Tween( begin: Offset.zero, end: Offset(0, -widget.parallaxOffset), ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeOut, )); _elevationAnimation = Tween( begin: 4.0, end: 12.0, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeOut, )); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return MouseRegion( onEnter: (_) => _controller.forward(), onExit: (_) => _controller.reverse(), child: GestureDetector( onTapDown: (_) => _controller.forward(), onTapUp: (_) => _controller.reverse(), onTapCancel: () => _controller.reverse(), child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.translate( offset: _offsetAnimation.value, child: Card( elevation: _elevationAnimation.value, child: widget.child, ), ); }, ), ), ); } } /// Widget avec effet de morphing pour les icĂ´nes class MorphingIcon extends StatefulWidget { final IconData icon; final IconData? alternateIcon; final double size; final Color? color; final Duration animationDuration; final VoidCallback? onPressed; const MorphingIcon({ super.key, required this.icon, this.alternateIcon, this.size = 24.0, this.color, this.animationDuration = const Duration(milliseconds: 300), this.onPressed, }); @override State createState() => _MorphingIconState(); } class _MorphingIconState extends State with TickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnimation; late Animation _rotationAnimation; bool _isAlternate = false; @override void initState() { super.initState(); _controller = AnimationController( duration: widget.animationDuration, vsync: this, ); _scaleAnimation = Tween( begin: 1.0, end: 0.0, ).animate(CurvedAnimation( parent: _controller, curve: const Interval(0.0, 0.5, curve: Curves.easeIn), )); _rotationAnimation = Tween( begin: 0.0, end: 0.5, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeInOut, )); _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { setState(() { _isAlternate = !_isAlternate; }); _controller.reverse(); } }); } @override void dispose() { _controller.dispose(); super.dispose(); } void _handleTap() { if (widget.alternateIcon != null) { _controller.forward(); } widget.onPressed?.call(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: _handleTap, child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value == 0.0 ? 1.0 : _scaleAnimation.value, child: Transform.rotate( angle: _rotationAnimation.value * 3.14159, child: Icon( _isAlternate && widget.alternateIcon != null ? widget.alternateIcon! : widget.icon, size: widget.size, color: widget.color, ), ), ); }, ), ); } }