- Extract business logic from screens into dedicated controllers - Split large screen files into smaller, reusable widget components - Add controllers for AddSubscriptionScreen and DetailScreen - Create modular widgets for subscription and detail features - Improve code organization and maintainability - Remove duplicated code and improve reusability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
341 lines
7.8 KiB
Dart
341 lines
7.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/physics.dart';
|
|
|
|
/// 물리 기반 스프링 애니메이션을 적용하는 위젯
|
|
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),
|
|
);
|
|
|
|
// 오프셋 애니메이션
|
|
_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,
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |