import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // import '../theme/app_colors.dart'; import '../l10n/app_localizations.dart'; import '../utils/platform_helper.dart'; import '../utils/reduce_motion.dart'; class FloatingNavigationBar extends StatefulWidget { final int selectedIndex; final Function(int) onItemTapped; final bool isVisible; const FloatingNavigationBar({ super.key, required this.selectedIndex, required this.onItemTapped, this.isVisible = true, }); @override State createState() => _FloatingNavigationBarState(); } class _FloatingNavigationBarState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: ReduceMotion.platform() ? const Duration(milliseconds: 0) : const Duration(milliseconds: 300), vsync: this, ); _animation = CurvedAnimation( parent: _controller, curve: Curves.easeInOut, ); if (widget.isVisible) { _controller.forward(); } } @override void didUpdateWidget(FloatingNavigationBar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isVisible != oldWidget.isVisible) { if (widget.isVisible) { _controller.forward(); } else { _controller.reverse(); } } } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, child) { final bottomInset = MediaQuery.of(context).padding.bottom; return Positioned( bottom: 0, left: 16, right: 16, height: 88 + bottomInset, child: Transform.translate( offset: Offset( 0, ReduceMotion.isEnabled(context) ? 0 : 100 * (1 - _animation.value)), child: Opacity( opacity: ReduceMotion.isEnabled(context) ? 1 : _animation.value, child: Container( margin: EdgeInsets.zero, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(24), boxShadow: const [], ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _NavigationItem( icon: Icons.home_rounded, label: AppLocalizations.of(context).home, isSelected: widget.selectedIndex == 0, onTap: () => _onItemTapped(0), ), _NavigationItem( icon: Icons.analytics_rounded, label: AppLocalizations.of(context).analysis, isSelected: widget.selectedIndex == 1, onTap: () => _onItemTapped(1), ), _AddButton( onTap: () => _onItemTapped(2), ), if (!PlatformHelper.isIOS) _NavigationItem( icon: Icons.qr_code_scanner_rounded, label: AppLocalizations.of(context).smsScanLabel, isSelected: widget.selectedIndex == 3, onTap: () => _onItemTapped(3), ), _NavigationItem( icon: Icons.settings_rounded, label: AppLocalizations.of(context).settings, isSelected: PlatformHelper.isIOS ? widget.selectedIndex == 3 : widget.selectedIndex == 4, onTap: () => _onItemTapped(PlatformHelper.isIOS ? 3 : 4), ), ], ), ), ), ), ), ); }, ); } void _onItemTapped(int index) { HapticFeedback.lightImpact(); widget.onItemTapped(index); } } class _NavigationItem extends StatelessWidget { final IconData icon; final String label; final bool isSelected; final VoidCallback onTap; const _NavigationItem({ required this.icon, required this.label, required this.isSelected, required this.onTap, }); @override Widget build(BuildContext context) { return Material( color: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: isSelected ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.1) : Colors.transparent, borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ AnimatedContainer( duration: const Duration(milliseconds: 200), child: Icon( icon, color: isSelected ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurfaceVariant, size: isSelected ? 24 : 22, ), ), const SizedBox(height: 2), AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 200), style: TextStyle( fontSize: 10, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, color: isSelected ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.onSurfaceVariant, ), child: Text(label), ), ], ), ), ), ); } } class _AddButton extends StatefulWidget { final VoidCallback onTap; const _AddButton({required this.onTap}); @override State<_AddButton> createState() => _AddButtonState(); } class _AddButtonState extends State<_AddButton> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _scaleAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: ReduceMotion.platform() ? const Duration(milliseconds: 0) : const Duration(milliseconds: 150), vsync: this, ); _scaleAnimation = Tween( begin: 1.0, end: ReduceMotion.platform() ? 1.0 : 0.9, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeInOut, )); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTapDown: (_) => _controller.forward(), onTapUp: (_) { _controller.reverse(); widget.onTap(); }, onTapCancel: () => _controller.reverse(), child: AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Container( width: 56, height: 56, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary, borderRadius: BorderRadius.circular(16), ), child: Icon( Icons.add_rounded, color: Theme.of(context).colorScheme.onPrimary, size: 28, ), ), ); }, ), ); } } // 스크롤 감지를 위한 유틸리티 클래스 class FloatingNavBarScrollController { final ScrollController scrollController; final VoidCallback onHide; final VoidCallback onShow; double _lastScrollPosition = 0; bool _isVisible = true; FloatingNavBarScrollController({ required this.scrollController, required this.onHide, required this.onShow, }) { scrollController.addListener(_handleScroll); } void _handleScroll() { final currentScroll = scrollController.position.pixels; if (currentScroll > _lastScrollPosition && currentScroll > 50) { // 스크롤 다운 if (_isVisible) { _isVisible = false; onHide(); } } else if (currentScroll < _lastScrollPosition - 5) { // 스크롤 업 if (!_isVisible) { _isVisible = true; onShow(); } } _lastScrollPosition = currentScroll; } void dispose() { scrollController.removeListener(_handleScroll); } }