Major UI/UX and architecture improvements
- Implemented new navigation system with NavigationProvider and route management - Added adaptive theme system with ThemeProvider for better theme handling - Introduced glassmorphism design elements (app bars, scaffolds, cards) - Added advanced animations (spring animations, page transitions, staggered lists) - Implemented performance optimizations (memory manager, lazy loading) - Refactored Analysis screen into modular components - Added floating navigation bar with haptic feedback - Improved subscription cards with swipe actions - Enhanced skeleton loading with better animations - Added cached network image support - Improved overall app architecture and code organization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
268
lib/widgets/expandable_fab.dart
Normal file
268
lib/widgets/expandable_fab.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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<FabAction> actions;
|
||||
final double distance;
|
||||
|
||||
const ExpandableFab({
|
||||
super.key,
|
||||
required this.actions,
|
||||
this.distance = 100.0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExpandableFab> createState() => _ExpandableFabState();
|
||||
}
|
||||
|
||||
class _ExpandableFabState extends State<ExpandableFab>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _expandAnimation;
|
||||
late Animation<double> _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<double>(
|
||||
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: Colors.black.withValues(alpha: 0.3 * _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: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
// 메인 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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<DraggableFab> createState() => _DraggableFabState();
|
||||
}
|
||||
|
||||
class _DraggableFabState extends State<DraggableFab> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user