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:
350
lib/widgets/spring_animation_widget.dart
Normal file
350
lib/widgets/spring_animation_widget.dart
Normal file
@@ -0,0 +1,350 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// 물리 기반 스프링 애니메이션을 적용하는 위젯
|
||||
class SpringAnimationWidget extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Duration delay;
|
||||
final SpringDescription spring;
|
||||
final Offset? initialOffset;
|
||||
final double? initialScale;
|
||||
final double? initialRotation;
|
||||
|
||||
const SpringAnimationWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.delay = Duration.zero,
|
||||
this.spring = const SpringDescription(
|
||||
mass: 1,
|
||||
stiffness: 100,
|
||||
damping: 10,
|
||||
),
|
||||
this.initialOffset,
|
||||
this.initialScale,
|
||||
this.initialRotation,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SpringAnimationWidget> createState() => _SpringAnimationWidgetState();
|
||||
}
|
||||
|
||||
class _SpringAnimationWidgetState extends State<SpringAnimationWidget>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _offsetAnimation;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _rotationAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
// 스프링 시뮬레이션
|
||||
final simulation = SpringSimulation(
|
||||
widget.spring,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
);
|
||||
|
||||
// 오프셋 애니메이션
|
||||
_offsetAnimation = Tween<Offset>(
|
||||
begin: widget.initialOffset ?? const Offset(0, 50),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
|
||||
// 스케일 애니메이션
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: widget.initialScale ?? 0.5,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
|
||||
// 회전 애니메이션
|
||||
_rotationAnimation = Tween<double>(
|
||||
begin: widget.initialRotation ?? 0.0,
|
||||
end: 0.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
|
||||
// 지연 후 애니메이션 시작
|
||||
Future.delayed(widget.delay, () {
|
||||
if (mounted) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: _offsetAnimation.value,
|
||||
child: Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Transform.rotate(
|
||||
angle: _rotationAnimation.value,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 바운스 효과가 있는 버튼
|
||||
class BouncyButton extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final BoxDecoration? decoration;
|
||||
|
||||
const BouncyButton({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onPressed,
|
||||
this.padding,
|
||||
this.decoration,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BouncyButton> createState() => _BouncyButtonState();
|
||||
}
|
||||
|
||||
class _BouncyButtonState extends State<BouncyButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.95,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTapDown(TapDownDetails details) {
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
void _handleTapUp(TapUpDetails details) {
|
||||
_controller.reverse();
|
||||
widget.onPressed?.call();
|
||||
}
|
||||
|
||||
void _handleTapCancel() {
|
||||
_controller.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: _handleTapDown,
|
||||
onTapUp: _handleTapUp,
|
||||
onTapCancel: _handleTapCancel,
|
||||
child: AnimatedBuilder(
|
||||
animation: _scaleAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Container(
|
||||
padding: widget.padding,
|
||||
decoration: widget.decoration,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 중력 효과 애니메이션
|
||||
class GravityAnimation extends StatefulWidget {
|
||||
final Widget child;
|
||||
final double gravity;
|
||||
final double bounceFactor;
|
||||
final double initialVelocity;
|
||||
|
||||
const GravityAnimation({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.gravity = 9.8,
|
||||
this.bounceFactor = 0.8,
|
||||
this.initialVelocity = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<GravityAnimation> createState() => _GravityAnimationState();
|
||||
}
|
||||
|
||||
class _GravityAnimationState extends State<GravityAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
double _position = 0;
|
||||
double _velocity = 0;
|
||||
double _floor = 300;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_velocity = widget.initialVelocity;
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 10),
|
||||
)..addListener(_updatePhysics);
|
||||
|
||||
_controller.repeat();
|
||||
}
|
||||
|
||||
void _updatePhysics() {
|
||||
setState(() {
|
||||
// 속도 업데이트 (중력 적용)
|
||||
_velocity += widget.gravity * 0.016; // 60fps 가정
|
||||
|
||||
// 위치 업데이트
|
||||
_position += _velocity;
|
||||
|
||||
// 바닥 충돌 감지
|
||||
if (_position >= _floor) {
|
||||
_position = _floor;
|
||||
_velocity = -_velocity * widget.bounceFactor;
|
||||
|
||||
// 너무 작은 바운스는 멈춤
|
||||
if (_velocity.abs() < 1) {
|
||||
_velocity = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, _position),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 물결 효과 애니메이션
|
||||
class RippleAnimation extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Color rippleColor;
|
||||
final Duration duration;
|
||||
|
||||
const RippleAnimation({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.rippleColor = Colors.blue,
|
||||
this.duration = const Duration(milliseconds: 600),
|
||||
});
|
||||
|
||||
@override
|
||||
State<RippleAnimation> createState() => _RippleAnimationState();
|
||||
}
|
||||
|
||||
class _RippleAnimationState extends State<RippleAnimation>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_animation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
_controller.forward(from: 0.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: _handleTap,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: 100 + 200 * _animation.value,
|
||||
height: 100 + 200 * _animation.value,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: widget.rippleColor.withValues(alpha:
|
||||
(1 - _animation.value) * 0.3,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
widget.child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user