import 'package:flutter/material.dart'; import '../constants/app_colors.dart'; import '../constants/app_dimensions.dart'; /// Shimmer 효과를 가진 스켈톤 로더 class SkeletonLoader extends StatefulWidget { final double width; final double height; final double borderRadius; const SkeletonLoader({ super.key, this.width = double.infinity, required this.height, this.borderRadius = AppDimensions.radiusSm, }); @override State createState() => _SkeletonLoaderState(); } class _SkeletonLoaderState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, )..repeat(); _animation = Tween(begin: -1.0, end: 2.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine), ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final baseColor = isDark ? AppColors.darkSurface.withValues(alpha: 0.6) : Colors.grey.shade300; final highlightColor = isDark ? AppColors.darkSurface.withValues(alpha: 0.9) : Colors.grey.shade100; return AnimatedBuilder( animation: _animation, builder: (context, child) { return Container( width: widget.width, height: widget.height, decoration: BoxDecoration( borderRadius: BorderRadius.circular(widget.borderRadius), gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [baseColor, highlightColor, baseColor], stops: [ (_animation.value - 1).clamp(0.0, 1.0), _animation.value.clamp(0.0, 1.0), (_animation.value + 1).clamp(0.0, 1.0), ], ), ), ); }, ); } } /// 맛집 카드 스켈톤 class RestaurantCardSkeleton extends StatelessWidget { const RestaurantCardSkeleton({super.key}); @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return Card( margin: const EdgeInsets.symmetric( horizontal: AppDimensions.paddingDefault, vertical: AppDimensions.paddingSm, ), color: isDark ? AppColors.darkSurface : AppColors.lightSurface, child: Padding( padding: const EdgeInsets.all(AppDimensions.paddingDefault), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ // 카테고리 아이콘 영역 const SkeletonLoader( width: AppDimensions.cardIconSize, height: AppDimensions.cardIconSize, ), const SizedBox(width: AppDimensions.paddingMd), // 가게 정보 영역 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SkeletonLoader(height: 20, width: 150), const SizedBox(height: AppDimensions.paddingXs), const SkeletonLoader(height: 14, width: 100), ], ), ), // 거리 배지 const SkeletonLoader(width: 60, height: 28, borderRadius: 14), ], ), const SizedBox(height: AppDimensions.paddingMd), // 주소 const SkeletonLoader(height: 14), ], ), ), ); } } /// 맛집 리스트 스켈톤 class RestaurantListSkeleton extends StatelessWidget { final int itemCount; const RestaurantListSkeleton({super.key, this.itemCount = 5}); @override Widget build(BuildContext context) { return ListView.builder( physics: const NeverScrollableScrollPhysics(), itemCount: itemCount, itemBuilder: (context, index) => const RestaurantCardSkeleton(), ); } }