Files
submanager/lib/widgets/spring_animation_widget.dart
JiWoong Sul 2f60ef585a feat: 글래스모피즘 디자인 시스템 및 색상 가이드 전면 적용
- @doc/color.md 가이드라인에 따른 색상 시스템 전면 개편
- 딥 블루(#2563EB), 스카이 블루(#60A5FA) 메인 컬러로 변경
- 모든 화면과 위젯에 글래스모피즘 효과 일관성 있게 적용
- darkNavy, navyGray 등 새로운 텍스트 색상 체계 도입
- 공통 스낵바 및 다이얼로그 컴포넌트 추가
- Claude AI 프로젝트 컨텍스트 파일(CLAUDE.md) 추가

영향받은 컴포넌트:
- 10개 스크린 (main, settings, detail, splash 등)
- 30개 이상 위젯 (buttons, cards, forms 등)
- 테마 시스템 (AppColors, AppTheme)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-11 18:41:05 +09:00

340 lines
7.8 KiB
Dart

import 'package:flutter/material.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;
final 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,
],
),
);
}
}