import 'package:didvan/constants/app_icons.dart'; import 'package:flutter/material.dart'; import 'dart:ui'; class DidvanBNB extends StatefulWidget { final int currentTabIndex; final void Function(int index) onTabChanged; const DidvanBNB({ Key? key, required this.currentTabIndex, required this.onTabChanged, }) : super(key: key); @override State createState() => _DidvanBNBState(); } class _DidvanBNBState extends State with TickerProviderStateMixin { late int _activeIndex; late AnimationController _blobController; late Animation _blobAnim; double _dragProgress = 0.0; // 0..1 between from->to int _dragFrom = 0; int _dragTo = 0; bool _isDragging = false; final GlobalKey _rowKey = GlobalKey(); @override void initState() { super.initState(); _activeIndex = widget.currentTabIndex; _blobController = AnimationController( duration: const Duration(milliseconds: 450), vsync: this, ); _blobAnim = CurvedAnimation(parent: _blobController, curve: Curves.easeOutCubic); } @override void didUpdateWidget(covariant DidvanBNB oldWidget) { super.didUpdateWidget(oldWidget); if (widget.currentTabIndex != _activeIndex) { _animateTo(widget.currentTabIndex); } } void _animateTo(int newIndex) { setState(() { _dragFrom = _activeIndex; _dragTo = newIndex; _activeIndex = newIndex; _dragProgress = 0; _isDragging = false; }); _blobController.forward(from: 0); } void _handleTap(int index) { if (index == _activeIndex) return; widget.onTabChanged(index); } void _startDrag(DragStartDetails d) { final box = _rowKey.currentContext?.findRenderObject() as RenderBox?; if (box == null) return; final local = box.globalToLocal(d.globalPosition); final width = box.size.width; final section = width / 4; // 4 items _dragFrom = _activeIndex; _dragTo = _activeIndex; _dragProgress = 0; _isDragging = true; final pointerIndex = (local.dx / section).clamp(0, 3).floor(); if (pointerIndex != _activeIndex) { _dragTo = pointerIndex; } setState(() {}); } void _updateDrag(DragUpdateDetails d) { if (!_isDragging) return; final box = _rowKey.currentContext?.findRenderObject() as RenderBox?; if (box == null) return; final width = box.size.width; final local = box.globalToLocal(d.globalPosition); final section = width / 4; final pointerIndex = (local.dx / section).clamp(0, 3).floor(); if (pointerIndex != _dragTo) { _dragFrom = _activeIndex; _dragTo = pointerIndex; } // progress relative to dragFrom->dragTo centers final fromCenter = (_dragFrom + 0.5) * section; final toCenter = (_dragTo + 0.5) * section; final total = (toCenter - fromCenter); _dragProgress = total.abs() < 2 ? 0 : ((local.dx - fromCenter) / (total)).clamp(0.0, 1.0); setState(() {}); } void _endDrag(DragEndDetails d) { if (!_isDragging) return; _isDragging = false; // decide final index final threshold = 0.5; final target = _dragProgress > threshold ? _dragTo : _dragFrom; if (target != _activeIndex) { widget.onTabChanged(target); } else { setState(() {}); } } @override void dispose() { _blobController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Positioned( bottom: 12, left: 0, right: 0, child: GestureDetector( onHorizontalDragStart: _startDrag, onHorizontalDragUpdate: _updateDrag, onHorizontalDragEnd: _endDrag, child: SizedBox( height: 90, child: Stack( alignment: Alignment.center, children: [ // Liquid blob painter behind icons LayoutBuilder( builder: (ctx, constraints) { final w = constraints.maxWidth; final section = w / 4; final fromCenter = (_dragFrom + 0.5) * section; final toCenter = (_dragTo + 0.5) * section; final t = _isDragging ? _dragProgress : _blobAnim.value; final center = fromCenter + (toCenter - fromCenter) * t; return CustomPaint( size: Size(w, 70), painter: _LiquidBlobPainter( progress: t, from: fromCenter, to: toCenter, center: center, color: (isDark ? Colors.blueAccent : Colors.deepPurpleAccent).withOpacity(0.18), highlight: (isDark ? Colors.blueAccent : Colors.deepPurpleAccent).withOpacity(0.35), ), ); }, ), // Icons row Align( alignment: Alignment.center, child: SizedBox( height: 70, child: Row( key: _rowKey, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _IconItem( index: 0, active: _activeIndex == 0, iconActive: DidvanIcons.house_solid, iconInactive: DidvanIcons.house_light, title: 'خانه', onTap: () => _handleTap(0), dragTarget: _dragTo, dragProgress: _dragProgress, isDragging: _isDragging, ), _IconItem( index: 1, active: _activeIndex == 1, iconActive: DidvanIcons.category_solid, iconInactive: DidvanIcons.category_light, title: 'دسته‌بندی', onTap: () => _handleTap(1), dragTarget: _dragTo, dragProgress: _dragProgress, isDragging: _isDragging, ), _IconItem( index: 3, active: _activeIndex == 3, iconActive: DidvanIcons.ai_solid, iconInactive: DidvanIcons.ai_regular, title: 'هوشان', onTap: () => _handleTap(3), dragTarget: _dragTo, dragProgress: _dragProgress, isDragging: _isDragging, ), _IconItem( index: 2, active: _activeIndex == 2, iconActive: DidvanIcons.stats__solid, iconInactive: DidvanIcons.stats__light, title: 'آمار و داده', onTap: () => _handleTap(2), dragTarget: _dragTo, dragProgress: _dragProgress, isDragging: _isDragging, ), ], ), ), ), ], ), ), ), ); } } class _LiquidBlobPainter extends CustomPainter { final double progress; // 0..1 final double from; final double to; final double center; final Color color; final Color highlight; _LiquidBlobPainter({ required this.progress, required this.from, required this.to, required this.center, required this.color, required this.highlight, }); @override void paint(Canvas canvas, Size size) { // Single capsule shape that stretches between from & interpolated center. final baseY = size.height / 2; final lerp = center; final start = from < lerp ? from : lerp; final end = from < lerp ? lerp : from; final radius = 26.0; final rect = Rect.fromLTRB(start - radius, baseY - radius, end + radius, baseY + radius); final rrect = RRect.fromRectAndRadius(rect, Radius.circular(radius)); final paint = Paint() ..shader = LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ color.withOpacity(0.9), color.withOpacity(0.4), ], ).createShader(rect); // Draw blurred base using saveLayer for slight glow canvas.saveLayer(rect.inflate(20), Paint()); canvas.drawRRect(rrect, paint); final glow = Paint() ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 20) ..color = highlight.withOpacity(0.5); canvas.drawCircle(Offset(lerp, baseY), 14 + 6 * progress, glow); canvas.restore(); } @override bool shouldRepaint(covariant _LiquidBlobPainter old) => old.progress != progress || old.from != from || old.to != to || old.center != center || old.color != color; } class _IconItem extends StatelessWidget { final int index; final bool active; final IconData iconActive; final IconData iconInactive; final String title; final VoidCallback onTap; final int dragTarget; final double dragProgress; final bool isDragging; const _IconItem({ required this.index, required this.active, required this.iconActive, required this.iconInactive, required this.title, required this.onTap, required this.dragTarget, required this.dragProgress, required this.isDragging, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final isDark = scheme.brightness == Brightness.dark; final base = scheme.onSurface; // fallback since title extension not imported here final activeColor = scheme.primary; return GestureDetector( onTap: onTap, behavior: HitTestBehavior.translucent, child: AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ AnimatedScale( duration: const Duration(milliseconds: 400), curve: Curves.easeOutBack, scale: active ? 1.15 : 1.0, child: Icon( active ? iconActive : iconInactive, size: 26, color: active ? activeColor : base.withOpacity(isDark ? 0.8 : 0.65), ), ), const SizedBox(height: 2), AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 300), curve: Curves.easeOut, style: Theme.of(context).textTheme.bodySmall!.copyWith( fontSize: 11, fontWeight: active ? FontWeight.w700 : FontWeight.w500, color: active ? activeColor : base.withOpacity(0.6), ), child: Text(title, maxLines: 1, overflow: TextOverflow.fade), ), ], ), ), ); } }