Versione OK Pour l'onglet événements.
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
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<InteractiveButton> createState() => _InteractiveButtonState();
|
||||
}
|
||||
|
||||
class _InteractiveButtonState extends State<InteractiveButton>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _scaleController;
|
||||
late AnimationController _rippleController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _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<double>(
|
||||
begin: 1.0,
|
||||
end: 0.95,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _scaleController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_rippleAnimation = Tween<double>(
|
||||
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<ParallaxCard> createState() => _ParallaxCardState();
|
||||
}
|
||||
|
||||
class _ParallaxCardState extends State<ParallaxCard>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _offsetAnimation;
|
||||
late Animation<double> _elevationAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: widget.animationDuration,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_offsetAnimation = Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: Offset(0, -widget.parallaxOffset),
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
));
|
||||
|
||||
_elevationAnimation = Tween<double>(
|
||||
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<MorphingIcon> createState() => _MorphingIconState();
|
||||
}
|
||||
|
||||
class _MorphingIconState extends State<MorphingIcon>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _rotationAnimation;
|
||||
|
||||
bool _isAlternate = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: widget.animationDuration,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
|
||||
));
|
||||
|
||||
_rotationAnimation = Tween<double>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user