first commit
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../../../shared/theme/app_theme.dart';
|
||||
import '../pages/main_navigation.dart';
|
||||
|
||||
class CustomBottomNavBar extends StatefulWidget {
|
||||
final int currentIndex;
|
||||
final List<NavigationTab> tabs;
|
||||
final Function(int) onTap;
|
||||
|
||||
const CustomBottomNavBar({
|
||||
super.key,
|
||||
required this.currentIndex,
|
||||
required this.tabs,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomBottomNavBar> createState() => _CustomBottomNavBarState();
|
||||
}
|
||||
|
||||
class _CustomBottomNavBarState extends State<CustomBottomNavBar>
|
||||
with TickerProviderStateMixin {
|
||||
late List<AnimationController> _animationControllers;
|
||||
late List<Animation<double>> _scaleAnimations;
|
||||
late List<Animation<Color?>> _colorAnimations;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeAnimations();
|
||||
}
|
||||
|
||||
void _initializeAnimations() {
|
||||
_animationControllers = List.generate(
|
||||
widget.tabs.length,
|
||||
(index) => AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
),
|
||||
);
|
||||
|
||||
_scaleAnimations = _animationControllers
|
||||
.map((controller) => Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.2,
|
||||
).animate(CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeInOut,
|
||||
)))
|
||||
.toList();
|
||||
|
||||
_colorAnimations = _animationControllers
|
||||
.map((controller) => ColorTween(
|
||||
begin: AppTheme.textHint,
|
||||
end: AppTheme.primaryColor,
|
||||
).animate(CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeInOut,
|
||||
)))
|
||||
.toList();
|
||||
|
||||
// Animation initiale pour l'onglet sélectionné
|
||||
if (widget.currentIndex < _animationControllers.length) {
|
||||
_animationControllers[widget.currentIndex].forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomBottomNavBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.currentIndex != widget.currentIndex) {
|
||||
// Reverse animation for old tab
|
||||
if (oldWidget.currentIndex < _animationControllers.length) {
|
||||
_animationControllers[oldWidget.currentIndex].reverse();
|
||||
}
|
||||
|
||||
// Forward animation for new tab
|
||||
if (widget.currentIndex < _animationControllers.length) {
|
||||
_animationControllers[widget.currentIndex].forward();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in _animationControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Container(
|
||||
height: 70,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: List.generate(
|
||||
widget.tabs.length,
|
||||
(index) => _buildNavItem(index),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(int index) {
|
||||
final tab = widget.tabs[index];
|
||||
final isSelected = index == widget.currentIndex;
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _handleTap(index),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationControllers[index],
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Icône avec animation
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? tab.color.withOpacity(0.15)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Transform.scale(
|
||||
scale: _scaleAnimations[index].value,
|
||||
child: Icon(
|
||||
isSelected ? tab.activeIcon : tab.icon,
|
||||
size: 20,
|
||||
color: isSelected ? tab.color : AppTheme.textHint,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 2),
|
||||
|
||||
// Label avec animation
|
||||
AnimatedDefaultTextStyle(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected ? tab.color : AppTheme.textHint,
|
||||
),
|
||||
child: Text(
|
||||
tab.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Indicateur de sélection
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
width: isSelected ? 16 : 0,
|
||||
height: 2,
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: tab.color,
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTap(int index) {
|
||||
// Vibration tactile
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
// Animation de pression
|
||||
_animationControllers[index].forward().then((_) {
|
||||
if (mounted && index != widget.currentIndex) {
|
||||
_animationControllers[index].reverse();
|
||||
}
|
||||
});
|
||||
|
||||
// Callback
|
||||
widget.onTap(index);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user