import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; import '../theme/app_colors.dart'; import '../services/sms_service.dart'; import '../utils/platform_helper.dart'; import '../routes/app_routes.dart'; import '../l10n/app_localizations.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @override State createState() => _SplashScreenState(); } class _SplashScreenState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _fadeAnimation; late Animation _scaleAnimation; late Animation _slideAnimation; late Animation _rotateAnimation; // 파티클 애니메이션을 위한 변수 final List> _particles = []; @override void initState() { super.initState(); // 애니메이션 컨트롤러 초기화 _animationController = AnimationController( duration: const Duration(milliseconds: 2500), vsync: this, ); _fadeAnimation = CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.7, curve: Curves.easeInOut), ); _scaleAnimation = Tween( begin: 0.6, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: Curves.elasticOut, )); _slideAnimation = Tween( begin: 50.0, end: 0.0, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.3, 0.8, curve: Curves.easeOutCubic), )); _rotateAnimation = Tween( begin: 0.0, end: 0.1, ).animate(CurvedAnimation( parent: _animationController, curve: const Interval(0.0, 0.5, curve: Curves.easeOutBack), )); // 랜덤 파티클 생성 _generateParticles(); _animationController.forward(); // 애니메이션 완료 후 메인화면으로 이동 Timer(const Duration(seconds: 2), () { navigateToNextScreen(); }); } void _generateParticles() { final random = DateTime.now().millisecondsSinceEpoch; for (int i = 0; i < 20; i++) { final size = (random % 10) / 10 * 8 + 2; // 2-10 사이의 크기 final x = (random % 100) / 100 * 300; // 랜덤 X 위치 final y = (random % 100) / 100 * 500; // 랜덤 Y 위치 final opacity = (random % 10) / 10 * 0.4 + 0.1; // 0.1-0.5 사이의 투명도 final duration = (random % 10) / 10 * 3000 + 2000; // 2-5초 사이의 지속시간 final delay = (random % 10) / 10 * 2000; // 0-2초 사이의 지연시간 int colorIndex = (random + i) % AppColors.blueGradient.length; _particles.add({ 'size': size, 'x': x, 'y': y, 'opacity': opacity, 'duration': duration, 'delay': delay, 'color': AppColors.blueGradient[colorIndex], }); } } Future navigateToNextScreen() async { // Android에서 SMS 권한이 없으면 권한 안내 화면으로 이동 if (PlatformHelper.isAndroid) { final hasPermission = await SMSService.hasSMSPermission(); if (!hasPermission && mounted) { Navigator.of(context).pushNamedAndRemoveUntil( AppRoutes.smsPermission, (route) => false, ); return; } } if (!mounted) return; Navigator.of(context).pushNamedAndRemoveUntil( AppRoutes.main, (route) => false, ); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; return Scaffold( body: Stack( children: [ // 배경 그라디언트 Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ AppColors.dayGradient[0], AppColors.dayGradient[1], ], ), ), ), // 글래스모피즘 오버레이 Container( decoration: BoxDecoration( color: AppColors.pureWhite.withValues(alpha: 0.05), ), ), Stack( children: [ // 배경 파티클 ..._particles.map((particle) { return AnimatedPositioned( duration: Duration(milliseconds: particle['duration'].toInt()), curve: Curves.easeInOut, left: particle['x'] - 50 + (size.width * 0.1), top: particle['y'] - 50 + (size.height * 0.1), child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: particle['opacity']), duration: Duration(milliseconds: particle['duration'].toInt()), builder: (context, value, child) { return Opacity( opacity: value, child: child, ); }, child: Container( width: particle['size'], height: particle['size'], decoration: BoxDecoration( color: particle['color'], shape: BoxShape.circle, boxShadow: [ BoxShadow( color: particle['color'].withValues(alpha: 0.3), blurRadius: 10, spreadRadius: 1, ), ], ), ), ), ); }).toList(), // 상단 원형 그라데이션 Positioned( top: -size.height * 0.2, right: -size.width * 0.2, child: Container( width: size.width * 0.8, height: size.width * 0.8, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ AppColors.pureWhite.withValues(alpha: 0.1), AppColors.pureWhite.withValues(alpha: 0.0), ], stops: const [0.2, 1.0], ), ), ), ), // 하단 원형 그라데이션 Positioned( bottom: -size.height * 0.1, left: -size.width * 0.3, child: Container( width: size.width * 0.9, height: size.width * 0.9, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ AppColors.pureWhite.withValues(alpha: 0.07), AppColors.pureWhite.withValues(alpha: 0.0), ], stops: const [0.4, 1.0], ), ), ), ), // 메인 콘텐츠 Column( children: [ Expanded( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 로고 애니메이션 AnimatedBuilder( animation: _animationController, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Transform.rotate( angle: _rotateAnimation.value, child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: 120, height: 120, child: ClipRRect( borderRadius: BorderRadius.circular(30), child: BackdropFilter( filter: ImageFilter.blur( sigmaX: 20, sigmaY: 20), child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ AppColors.pureWhite .withValues(alpha: 0.2), AppColors.pureWhite .withValues(alpha: 0.1), ], ), borderRadius: BorderRadius.circular(30), border: Border.all( color: AppColors.pureWhite .withValues(alpha: 0.3), width: 1.5, ), boxShadow: const [ BoxShadow( color: AppColors.shadowBlack, spreadRadius: 0, blurRadius: 30, offset: Offset(0, 10), ), ], ), child: Center( child: AnimatedBuilder( animation: _animationController, builder: (context, _) { return ShaderMask( blendMode: BlendMode.srcIn, shaderCallback: (bounds) => const LinearGradient( colors: AppColors .blueGradient, begin: Alignment.topLeft, end: Alignment .bottomRight, ).createShader(bounds), child: Icon( Icons .subscriptions_outlined, size: 64, color: Theme.of(context) .primaryColor, ), ); }), ), ), ), )), )); }, ), const SizedBox(height: 40), // 앱 이름 텍스트 AnimatedBuilder( animation: _animationController, builder: (context, child) { return Opacity( opacity: _fadeAnimation.value, child: Transform.translate( offset: Offset(0, _slideAnimation.value), child: child, ), ); }, child: Text( AppLocalizations.of(context).appTitle, style: TextStyle( fontSize: 36, fontWeight: FontWeight.bold, color: AppColors.primaryColor .withValues(alpha: 0.9), letterSpacing: 1.2, ), ), ), const SizedBox(height: 16), // 부제목 텍스트 AnimatedBuilder( animation: _animationController, builder: (context, child) { return Opacity( opacity: _fadeAnimation.value, child: Transform.translate( offset: Offset(0, _slideAnimation.value * 1.2), child: child, ), ); }, child: Text( AppLocalizations.of(context).appSubtitle, style: TextStyle( fontSize: 16, color: AppColors.primaryColor .withValues(alpha: 0.7), letterSpacing: 0.5, ), ), ), const SizedBox(height: 60), // 로딩 인디케이터 FadeTransition( opacity: _fadeAnimation, child: ClipRRect( borderRadius: BorderRadius.circular(50), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container( width: 60, height: 60, padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: AppColors.pureWhite .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(50), border: Border.all( color: AppColors.pureWhite .withValues(alpha: 0.2), width: 1, ), ), child: const CircularProgressIndicator( valueColor: AlwaysStoppedAnimation( AppColors.pureWhite), strokeWidth: 3, ), ), ), ), ), ], ), ), ), // 카피라이트 텍스트 Padding( padding: const EdgeInsets.only(bottom: 24.0), child: FadeTransition( opacity: _fadeAnimation, child: Text( '© 2025 NatureBridgeAI. All rights reserved.', style: TextStyle( fontSize: 12, color: AppColors.pureWhite.withValues(alpha: 0.6), letterSpacing: 0.5, ), ), ), ), ], ), ], ), ], ), ); } }