import 'package:flutter/material.dart'; import 'dart:math' as math; import '../theme/app_colors.dart'; import '../utils/haptic_feedback_helper.dart'; import 'glassmorphism_card.dart'; class ExpandableFab extends StatefulWidget { final List actions; final double distance; const ExpandableFab({ super.key, required this.actions, this.distance = 100.0, }); @override State createState() => _ExpandableFabState(); } class _ExpandableFabState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _expandAnimation; late Animation _rotateAnimation; bool _isExpanded = false; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); _expandAnimation = CurvedAnimation( parent: _controller, curve: Curves.easeOutBack, reverseCurve: Curves.easeInBack, ); _rotateAnimation = Tween( begin: 0.0, end: math.pi / 4, ).animate(CurvedAnimation( parent: _controller, curve: Curves.easeInOut, )); } @override void dispose() { _controller.dispose(); super.dispose(); } void _toggle() { setState(() { _isExpanded = !_isExpanded; }); if (_isExpanded) { HapticFeedbackHelper.mediumImpact(); _controller.forward(); } else { HapticFeedbackHelper.lightImpact(); _controller.reverse(); } } @override Widget build(BuildContext context) { return Stack( alignment: Alignment.bottomRight, children: [ // 배경 오버레이 (확장 시) if (_isExpanded) GestureDetector( onTap: _toggle, child: AnimatedBuilder( animation: _expandAnimation, builder: (context, child) { return Container( color: AppColors.shadowBlack .withValues(alpha: 3.75 * _expandAnimation.value), ); }, ), ), // 액션 버튼들 ...widget.actions.asMap().entries.map((entry) { final index = entry.key; final action = entry.value; final angle = (index + 1) * (math.pi / 2 / widget.actions.length); return AnimatedBuilder( animation: _expandAnimation, builder: (context, child) { final distance = widget.distance * _expandAnimation.value; final x = distance * math.cos(angle); final y = distance * math.sin(angle); return Transform.translate( offset: Offset(-x, -y), child: ScaleTransition( scale: _expandAnimation, child: FloatingActionButton.small( heroTag: 'fab_action_$index', onPressed: _isExpanded ? () { HapticFeedbackHelper.lightImpact(); _toggle(); action.onPressed(); } : null, backgroundColor: action.color ?? AppColors.primaryColor, child: Icon( action.icon, size: 20, color: AppColors.pureWhite, ), ), ), ); }, ); }), // 메인 FAB AnimatedBuilder( animation: _rotateAnimation, builder: (context, child) { return Transform.rotate( angle: _rotateAnimation.value, child: FloatingActionButton( onPressed: _toggle, backgroundColor: AppColors.primaryColor, child: Icon( _isExpanded ? Icons.close : Icons.add, size: 28, color: Colors.white, ), ), ); }, ), // 라벨 표시 if (_isExpanded) ...widget.actions.asMap().entries.map((entry) { final index = entry.key; final action = entry.value; final angle = (index + 1) * (math.pi / 2 / widget.actions.length); return AnimatedBuilder( animation: _expandAnimation, builder: (context, child) { final distance = widget.distance * _expandAnimation.value; final x = distance * math.cos(angle); final y = distance * math.sin(angle); return Transform.translate( offset: Offset(-x - 80, -y), child: FadeTransition( opacity: _expandAnimation, child: GlassmorphismCard( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), borderRadius: 8, blur: 10, child: Text( action.label, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.darkNavy, ), ), ), ), ); }, ); }), ], ); } } class FabAction { final IconData icon; final String label; final VoidCallback onPressed; final Color? color; const FabAction({ required this.icon, required this.label, required this.onPressed, this.color, }); } // 드래그 가능한 FAB class DraggableFab extends StatefulWidget { final Widget child; final EdgeInsets? padding; const DraggableFab({ super.key, required this.child, this.padding, }); @override State createState() => _DraggableFabState(); } class _DraggableFabState extends State { Offset _position = const Offset(20, 20); bool _isDragging = false; @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; final padding = widget.padding ?? const EdgeInsets.all(20); return Stack( children: [ Positioned( right: _position.dx, bottom: _position.dy, child: GestureDetector( onPanStart: (_) { setState(() => _isDragging = true); HapticFeedbackHelper.lightImpact(); }, onPanUpdate: (details) { setState(() { _position = Offset( (_position.dx - details.delta.dx).clamp( padding.right, screenSize.width - 100 - padding.left, ), (_position.dy - details.delta.dy).clamp( padding.bottom, screenSize.height - 200 - padding.top, ), ); }); }, onPanEnd: (_) { setState(() => _isDragging = false); HapticFeedbackHelper.lightImpact(); }, child: AnimatedScale( duration: const Duration(milliseconds: 150), scale: _isDragging ? 0.9 : 1.0, child: widget.child, ), ), ), ], ); } }