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:
314
lib/widgets/glassmorphic_scaffold.dart
Normal file
314
lib/widgets/glassmorphic_scaffold.dart
Normal file
@@ -0,0 +1,314 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'dart:math' as math;
|
||||
import '../theme/app_colors.dart';
|
||||
import 'glassmorphic_app_bar.dart';
|
||||
import 'floating_navigation_bar.dart';
|
||||
|
||||
/// 글래스모피즘 디자인이 적용된 통일된 스캐폴드
|
||||
class GlassmorphicScaffold extends StatefulWidget {
|
||||
final PreferredSizeWidget? appBar;
|
||||
final Widget body;
|
||||
final Widget? floatingActionButton;
|
||||
final FloatingActionButtonLocation? floatingActionButtonLocation;
|
||||
final List<Color>? backgroundGradient;
|
||||
final bool extendBodyBehindAppBar;
|
||||
final bool extendBody;
|
||||
final Widget? bottomNavigationBar;
|
||||
final bool useFloatingNavBar;
|
||||
final int? floatingNavBarIndex;
|
||||
final Function(int)? onFloatingNavBarTapped;
|
||||
final bool resizeToAvoidBottomInset;
|
||||
final Widget? drawer;
|
||||
final Widget? endDrawer;
|
||||
final Color? backgroundColor;
|
||||
final bool enableParticles;
|
||||
final bool enableWaveAnimation;
|
||||
|
||||
const GlassmorphicScaffold({
|
||||
super.key,
|
||||
this.appBar,
|
||||
required this.body,
|
||||
this.floatingActionButton,
|
||||
this.floatingActionButtonLocation,
|
||||
this.backgroundGradient,
|
||||
this.extendBodyBehindAppBar = true,
|
||||
this.extendBody = true,
|
||||
this.bottomNavigationBar,
|
||||
this.useFloatingNavBar = false,
|
||||
this.floatingNavBarIndex,
|
||||
this.onFloatingNavBarTapped,
|
||||
this.resizeToAvoidBottomInset = true,
|
||||
this.drawer,
|
||||
this.endDrawer,
|
||||
this.backgroundColor,
|
||||
this.enableParticles = false,
|
||||
this.enableWaveAnimation = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<GlassmorphicScaffold> createState() => _GlassmorphicScaffoldState();
|
||||
}
|
||||
|
||||
class _GlassmorphicScaffoldState extends State<GlassmorphicScaffold>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _particleController;
|
||||
late AnimationController _waveController;
|
||||
ScrollController? _scrollController;
|
||||
bool _isFloatingNavBarVisible = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_particleController = AnimationController(
|
||||
duration: const Duration(seconds: 20),
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
|
||||
_waveController = AnimationController(
|
||||
duration: const Duration(seconds: 10),
|
||||
vsync: this,
|
||||
)..repeat();
|
||||
|
||||
if (widget.useFloatingNavBar) {
|
||||
_scrollController = ScrollController();
|
||||
_setupScrollListener();
|
||||
}
|
||||
}
|
||||
|
||||
void _setupScrollListener() {
|
||||
_scrollController?.addListener(() {
|
||||
final currentScroll = _scrollController!.position.pixels;
|
||||
final maxScroll = _scrollController!.position.maxScrollExtent;
|
||||
|
||||
// 스크롤 방향에 따라 플로팅 네비게이션 바 표시/숨김
|
||||
if (currentScroll > 50 && _scrollController!.position.userScrollDirection == ScrollDirection.reverse) {
|
||||
if (_isFloatingNavBarVisible) {
|
||||
setState(() => _isFloatingNavBarVisible = false);
|
||||
}
|
||||
} else if (_scrollController!.position.userScrollDirection == ScrollDirection.forward) {
|
||||
if (!_isFloatingNavBarVisible) {
|
||||
setState(() => _isFloatingNavBarVisible = true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_particleController.dispose();
|
||||
_waveController.dispose();
|
||||
_scrollController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<Color> _getBackgroundGradient() {
|
||||
if (widget.backgroundGradient != null) {
|
||||
return widget.backgroundGradient!;
|
||||
}
|
||||
|
||||
// 시간대별 기본 그라디언트
|
||||
final hour = DateTime.now().hour;
|
||||
if (hour >= 6 && hour < 10) {
|
||||
return AppColors.morningGradient;
|
||||
} else if (hour >= 10 && hour < 17) {
|
||||
return AppColors.dayGradient;
|
||||
} else if (hour >= 17 && hour < 20) {
|
||||
return AppColors.eveningGradient;
|
||||
} else {
|
||||
return AppColors.nightGradient;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backgroundGradient = _getBackgroundGradient();
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// 배경 그라디언트
|
||||
_buildBackground(backgroundGradient),
|
||||
|
||||
// 파티클 효과 (선택적)
|
||||
if (widget.enableParticles) _buildParticles(),
|
||||
|
||||
// 웨이브 애니메이션 (선택적)
|
||||
if (widget.enableWaveAnimation) _buildWaveAnimation(),
|
||||
|
||||
// 메인 스캐폴드
|
||||
Scaffold(
|
||||
backgroundColor: widget.backgroundColor ?? Colors.transparent,
|
||||
appBar: widget.appBar,
|
||||
body: widget.body,
|
||||
floatingActionButton: widget.floatingActionButton,
|
||||
floatingActionButtonLocation: widget.floatingActionButtonLocation,
|
||||
bottomNavigationBar: widget.bottomNavigationBar,
|
||||
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
|
||||
extendBody: widget.extendBody,
|
||||
resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset,
|
||||
drawer: widget.drawer,
|
||||
endDrawer: widget.endDrawer,
|
||||
),
|
||||
|
||||
// 플로팅 네비게이션 바 (선택적)
|
||||
if (widget.useFloatingNavBar && widget.floatingNavBarIndex != null)
|
||||
FloatingNavigationBar(
|
||||
selectedIndex: widget.floatingNavBarIndex!,
|
||||
isVisible: _isFloatingNavBarVisible,
|
||||
onItemTapped: widget.onFloatingNavBarTapped ?? (_) {},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBackground(List<Color> gradientColors) {
|
||||
return Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: gradientColors.map((color) => color.withValues(alpha: 0.1)).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildParticles() {
|
||||
return Positioned.fill(
|
||||
child: AnimatedBuilder(
|
||||
animation: _particleController,
|
||||
builder: (context, child) {
|
||||
return CustomPaint(
|
||||
painter: ParticlePainter(
|
||||
animation: _particleController,
|
||||
particleCount: 30,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaveAnimation() {
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 200,
|
||||
child: AnimatedBuilder(
|
||||
animation: _waveController,
|
||||
builder: (context, child) {
|
||||
return CustomPaint(
|
||||
painter: WavePainter(
|
||||
animation: _waveController,
|
||||
waveColor: AppColors.primaryColor.withValues(alpha: 0.1),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 파티클 페인터
|
||||
class ParticlePainter extends CustomPainter {
|
||||
final Animation<double> animation;
|
||||
final int particleCount;
|
||||
final List<Particle> particles = [];
|
||||
|
||||
ParticlePainter({
|
||||
required this.animation,
|
||||
this.particleCount = 50,
|
||||
}) : super(repaint: animation) {
|
||||
_initParticles();
|
||||
}
|
||||
|
||||
void _initParticles() {
|
||||
final random = math.Random();
|
||||
for (int i = 0; i < particleCount; i++) {
|
||||
particles.add(Particle(
|
||||
x: random.nextDouble(),
|
||||
y: random.nextDouble(),
|
||||
size: random.nextDouble() * 3 + 1,
|
||||
speed: random.nextDouble() * 0.5 + 0.1,
|
||||
opacity: random.nextDouble() * 0.5 + 0.1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()..style = PaintingStyle.fill;
|
||||
|
||||
for (final particle in particles) {
|
||||
final progress = animation.value;
|
||||
final y = (particle.y + progress * particle.speed) % 1.0;
|
||||
|
||||
paint.color = Colors.white.withValues(alpha: particle.opacity);
|
||||
canvas.drawCircle(
|
||||
Offset(particle.x * size.width, y * size.height),
|
||||
particle.size,
|
||||
paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
/// 웨이브 페인터
|
||||
class WavePainter extends CustomPainter {
|
||||
final Animation<double> animation;
|
||||
final Color waveColor;
|
||||
|
||||
WavePainter({
|
||||
required this.animation,
|
||||
required this.waveColor,
|
||||
}) : super(repaint: animation);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = waveColor
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final path = Path();
|
||||
final progress = animation.value;
|
||||
|
||||
path.moveTo(0, size.height);
|
||||
|
||||
for (double x = 0; x <= size.width; x++) {
|
||||
final y = math.sin((x / size.width * 2 * math.pi) + (progress * 2 * math.pi)) * 20 +
|
||||
size.height * 0.5;
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
|
||||
path.lineTo(size.width, size.height);
|
||||
path.close();
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
/// 파티클 데이터 클래스
|
||||
class Particle {
|
||||
final double x;
|
||||
final double y;
|
||||
final double size;
|
||||
final double speed;
|
||||
final double opacity;
|
||||
|
||||
Particle({
|
||||
required this.x,
|
||||
required this.y,
|
||||
required this.size,
|
||||
required this.speed,
|
||||
required this.opacity,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user