369 lines
9.4 KiB
Dart
369 lines
9.4 KiB
Dart
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,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|