Versione OK Pour l'onglet événements.
This commit is contained in:
320
unionflow-mobile-apps/lib/core/animations/animated_button.dart
Normal file
320
unionflow-mobile-apps/lib/core/animations/animated_button.dart
Normal file
@@ -0,0 +1,320 @@
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../shared/theme/app_theme.dart';
|
||||
|
||||
/// Service de notifications animées
|
||||
class AnimatedNotifications {
|
||||
static OverlayEntry? _currentOverlay;
|
||||
|
||||
/// Affiche une notification de succès
|
||||
static void showSuccess(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_showNotification(
|
||||
context,
|
||||
message,
|
||||
NotificationType.success,
|
||||
duration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche une notification d'erreur
|
||||
static void showError(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
_showNotification(
|
||||
context,
|
||||
message,
|
||||
NotificationType.error,
|
||||
duration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche une notification d'information
|
||||
static void showInfo(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_showNotification(
|
||||
context,
|
||||
message,
|
||||
NotificationType.info,
|
||||
duration,
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche une notification d'avertissement
|
||||
static void showWarning(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_showNotification(
|
||||
context,
|
||||
message,
|
||||
NotificationType.warning,
|
||||
duration,
|
||||
);
|
||||
}
|
||||
|
||||
static void _showNotification(
|
||||
BuildContext context,
|
||||
String message,
|
||||
NotificationType type,
|
||||
Duration duration,
|
||||
) {
|
||||
// Supprimer la notification précédente si elle existe
|
||||
_currentOverlay?.remove();
|
||||
|
||||
final overlay = Overlay.of(context);
|
||||
late OverlayEntry overlayEntry;
|
||||
|
||||
overlayEntry = OverlayEntry(
|
||||
builder: (context) => AnimatedNotificationWidget(
|
||||
message: message,
|
||||
type: type,
|
||||
onDismiss: () {
|
||||
overlayEntry.remove();
|
||||
_currentOverlay = null;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
_currentOverlay = overlayEntry;
|
||||
overlay.insert(overlayEntry);
|
||||
|
||||
// Auto-dismiss après la durée spécifiée
|
||||
Future.delayed(duration, () {
|
||||
if (_currentOverlay == overlayEntry) {
|
||||
overlayEntry.remove();
|
||||
_currentOverlay = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Masque la notification actuelle
|
||||
static void dismiss() {
|
||||
_currentOverlay?.remove();
|
||||
_currentOverlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget de notification animée
|
||||
class AnimatedNotificationWidget extends StatefulWidget {
|
||||
final String message;
|
||||
final NotificationType type;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
const AnimatedNotificationWidget({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.type,
|
||||
required this.onDismiss,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AnimatedNotificationWidget> createState() => _AnimatedNotificationWidgetState();
|
||||
}
|
||||
|
||||
class _AnimatedNotificationWidgetState extends State<AnimatedNotificationWidget>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _slideController;
|
||||
late AnimationController _fadeController;
|
||||
late AnimationController _scaleController;
|
||||
|
||||
late Animation<Offset> _slideAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_slideController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_fadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleController = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, -1),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _slideController,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _fadeController,
|
||||
curve: Curves.easeOut,
|
||||
));
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.05,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _scaleController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
// Démarrer les animations d'entrée
|
||||
_fadeController.forward();
|
||||
_slideController.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_slideController.dispose();
|
||||
_fadeController.dispose();
|
||||
_scaleController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dismiss() async {
|
||||
await _fadeController.reverse();
|
||||
widget.onDismiss();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = _getColors();
|
||||
|
||||
return Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: AnimatedBuilder(
|
||||
animation: Listenable.merge([
|
||||
_slideAnimation,
|
||||
_fadeAnimation,
|
||||
_scaleAnimation,
|
||||
]),
|
||||
builder: (context, child) {
|
||||
return SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: GestureDetector(
|
||||
onTap: () => _scaleController.forward().then((_) {
|
||||
_scaleController.reverse();
|
||||
}),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
colors.backgroundColor,
|
||||
colors.backgroundColor.withOpacity(0.9),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colors.backgroundColor.withOpacity(0.3),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Icône
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.iconBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
colors.icon,
|
||||
color: colors.iconColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Message
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.message,
|
||||
style: TextStyle(
|
||||
color: colors.textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bouton de fermeture
|
||||
GestureDetector(
|
||||
onTap: _dismiss,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: colors.textColor.withOpacity(0.7),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_NotificationColors _getColors() {
|
||||
switch (widget.type) {
|
||||
case NotificationType.success:
|
||||
return _NotificationColors(
|
||||
backgroundColor: AppTheme.successColor,
|
||||
textColor: Colors.white,
|
||||
icon: Icons.check_circle,
|
||||
iconColor: Colors.white,
|
||||
iconBackgroundColor: Colors.white.withOpacity(0.2),
|
||||
);
|
||||
case NotificationType.error:
|
||||
return _NotificationColors(
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
textColor: Colors.white,
|
||||
icon: Icons.error,
|
||||
iconColor: Colors.white,
|
||||
iconBackgroundColor: Colors.white.withOpacity(0.2),
|
||||
);
|
||||
case NotificationType.warning:
|
||||
return _NotificationColors(
|
||||
backgroundColor: AppTheme.warningColor,
|
||||
textColor: Colors.white,
|
||||
icon: Icons.warning,
|
||||
iconColor: Colors.white,
|
||||
iconBackgroundColor: Colors.white.withOpacity(0.2),
|
||||
);
|
||||
case NotificationType.info:
|
||||
return _NotificationColors(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
textColor: Colors.white,
|
||||
icon: Icons.info,
|
||||
iconColor: Colors.white,
|
||||
iconBackgroundColor: Colors.white.withOpacity(0.2),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationColors {
|
||||
final Color backgroundColor;
|
||||
final Color textColor;
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final Color iconBackgroundColor;
|
||||
|
||||
_NotificationColors({
|
||||
required this.backgroundColor,
|
||||
required this.textColor,
|
||||
required this.icon,
|
||||
required this.iconColor,
|
||||
required this.iconBackgroundColor,
|
||||
});
|
||||
}
|
||||
|
||||
enum NotificationType {
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,72 @@ class PageTransitions {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Transition avec effet de morphing et blur
|
||||
static PageRouteBuilder<T> morphWithBlur<T>(Widget page) {
|
||||
return PageRouteBuilder<T>(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => page,
|
||||
transitionDuration: const Duration(milliseconds: 500),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 400),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
final curvedAnimation = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOutCubic,
|
||||
);
|
||||
|
||||
final scaleAnimation = Tween<double>(
|
||||
begin: 0.8,
|
||||
end: 1.0,
|
||||
).animate(curvedAnimation);
|
||||
|
||||
final fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.3, 1.0, curve: Curves.easeOut),
|
||||
));
|
||||
|
||||
return FadeTransition(
|
||||
opacity: fadeAnimation,
|
||||
child: Transform.scale(
|
||||
scale: scaleAnimation.value,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Transition avec effet de rotation 3D
|
||||
static PageRouteBuilder<T> rotate3D<T>(Widget page) {
|
||||
return PageRouteBuilder<T>(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => page,
|
||||
transitionDuration: const Duration(milliseconds: 600),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 500),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
final curvedAnimation = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOutCubic,
|
||||
);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: curvedAnimation,
|
||||
builder: (context, child) {
|
||||
final rotationY = (1.0 - curvedAnimation.value) * 0.5;
|
||||
return Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001)
|
||||
..rotateY(rotationY),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions pour faciliter l'utilisation des transitions
|
||||
@@ -209,6 +275,16 @@ extension NavigatorTransitions on NavigatorState {
|
||||
Future<T?> pushSlideWithParallax<T>(Widget page) {
|
||||
return push<T>(PageTransitions.slideWithParallax<T>(page));
|
||||
}
|
||||
|
||||
/// Navigation avec transition de morphing
|
||||
Future<T?> pushMorphWithBlur<T>(Widget page) {
|
||||
return push<T>(PageTransitions.morphWithBlur<T>(page));
|
||||
}
|
||||
|
||||
/// Navigation avec transition de rotation 3D
|
||||
Future<T?> pushRotate3D<T>(Widget page) {
|
||||
return push<T>(PageTransitions.rotate3D<T>(page));
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget d'animation pour les éléments de liste
|
||||
|
||||
Reference in New Issue
Block a user