import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../theme/app_colors.dart'; import 'skeleton_loading.dart'; /// 최적화된 캐시 네트워크 이미지 위젯 class OptimizedCachedNetworkImage extends StatelessWidget { final String imageUrl; final double? width; final double? height; final BoxFit fit; final BorderRadius? borderRadius; final Duration fadeInDuration; final Duration fadeOutDuration; final Widget? placeholder; final Widget? errorWidget; final Map? httpHeaders; final bool enableMemoryCache; final bool enableDiskCache; final int? maxWidth; final int? maxHeight; const OptimizedCachedNetworkImage({ super.key, required this.imageUrl, this.width, this.height, this.fit = BoxFit.cover, this.borderRadius, this.fadeInDuration = const Duration(milliseconds: 300), this.fadeOutDuration = const Duration(milliseconds: 300), this.placeholder, this.errorWidget, this.httpHeaders, this.enableMemoryCache = true, this.enableDiskCache = true, this.maxWidth, this.maxHeight, }); @override Widget build(BuildContext context) { // 성능 최적화를 위한 이미지 크기 계산 final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final optimalWidth = maxWidth ?? (width != null ? (width! * devicePixelRatio).round() : null); final optimalHeight = maxHeight ?? (height != null ? (height! * devicePixelRatio).round() : null); Widget image = CachedNetworkImage( imageUrl: imageUrl, width: width, height: height, fit: fit, fadeInDuration: fadeInDuration, fadeOutDuration: fadeOutDuration, httpHeaders: httpHeaders, memCacheWidth: optimalWidth, memCacheHeight: optimalHeight, maxWidthDiskCache: optimalWidth, maxHeightDiskCache: optimalHeight, placeholder: (context, url) => placeholder ?? _buildDefaultPlaceholder(), errorWidget: (context, url, error) => errorWidget ?? _buildDefaultErrorWidget(), imageBuilder: (context, imageProvider) { return Container( width: width, height: height, decoration: BoxDecoration( borderRadius: borderRadius, image: DecorationImage( image: imageProvider, fit: fit, ), ), ); }, ); if (borderRadius != null) { return ClipRRect( borderRadius: borderRadius!, child: image, ); } return image; } Widget _buildDefaultPlaceholder() { return SkeletonLoading( width: width, height: height, borderRadius: borderRadius?.topLeft.x ?? 0, ); } Widget _buildDefaultErrorWidget() { return Container( width: width, height: height, decoration: BoxDecoration( color: AppColors.surfaceColorAlt, borderRadius: borderRadius, ), child: const Icon( Icons.broken_image_outlined, color: AppColors.textMuted, size: 24, ), ); } } /// 프로그레시브 이미지 로더 (저화질 → 고화질) class ProgressiveNetworkImage extends StatelessWidget { final String thumbnailUrl; final String imageUrl; final double? width; final double? height; final BoxFit fit; final BorderRadius? borderRadius; const ProgressiveNetworkImage({ super.key, required this.thumbnailUrl, required this.imageUrl, this.width, this.height, this.fit = BoxFit.cover, this.borderRadius, }); @override Widget build(BuildContext context) { return Stack( fit: StackFit.passthrough, children: [ // 썸네일 (저화질) OptimizedCachedNetworkImage( imageUrl: thumbnailUrl, width: width, height: height, fit: fit, borderRadius: borderRadius, fadeInDuration: Duration.zero, ), // 원본 이미지 (고화질) OptimizedCachedNetworkImage( imageUrl: imageUrl, width: width, height: height, fit: fit, borderRadius: borderRadius, ), ], ); } } /// 이미지 갤러리 위젯 (메모리 효율적) class OptimizedImageGallery extends StatefulWidget { final List imageUrls; final double itemHeight; final double spacing; final int crossAxisCount; final void Function(int)? onImageTap; const OptimizedImageGallery({ super.key, required this.imageUrls, this.itemHeight = 120, this.spacing = 8, this.crossAxisCount = 3, this.onImageTap, }); @override State createState() => _OptimizedImageGalleryState(); } class _OptimizedImageGalleryState extends State { final ScrollController _scrollController = ScrollController(); final Set _visibleIndices = {}; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); // 초기 보이는 아이템 계산 WidgetsBinding.instance.addPostFrameCallback((_) { _calculateVisibleIndices(); }); } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onScroll() { _calculateVisibleIndices(); } void _calculateVisibleIndices() { if (!mounted) return; final viewportHeight = context.size?.height ?? 0; final scrollOffset = _scrollController.offset; final itemHeight = widget.itemHeight + widget.spacing; final itemsPerRow = widget.crossAxisCount; final firstVisibleRow = (scrollOffset / itemHeight).floor(); final lastVisibleRow = ((scrollOffset + viewportHeight) / itemHeight).ceil(); final newVisibleIndices = {}; for (int row = firstVisibleRow; row <= lastVisibleRow; row++) { for (int col = 0; col < itemsPerRow; col++) { final index = row * itemsPerRow + col; if (index < widget.imageUrls.length) { newVisibleIndices.add(index); } } } if (!setEquals(_visibleIndices, newVisibleIndices)) { setState(() { _visibleIndices.clear(); _visibleIndices.addAll(newVisibleIndices); }); } } @override Widget build(BuildContext context) { return GridView.builder( controller: _scrollController, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: widget.crossAxisCount, childAspectRatio: 1.0, crossAxisSpacing: widget.spacing, mainAxisSpacing: widget.spacing, ), itemCount: widget.imageUrls.length, itemBuilder: (context, index) { // 보이는 영역의 이미지만 로드 if (_visibleIndices.contains(index) || (index >= _visibleIndices.first - widget.crossAxisCount && index <= _visibleIndices.last + widget.crossAxisCount)) { return GestureDetector( onTap: () => widget.onImageTap?.call(index), child: OptimizedCachedNetworkImage( imageUrl: widget.imageUrls[index], fit: BoxFit.cover, borderRadius: BorderRadius.circular(8), ), ); } // 보이지 않는 영역은 플레이스홀더 return Container( decoration: BoxDecoration( color: AppColors.surfaceColorAlt, borderRadius: BorderRadius.circular(8), ), ); }, ); } bool setEquals(Set a, Set b) { if (a.length != b.length) return false; for (final item in a) { if (!b.contains(item)) return false; } return true; } } /// 히어로 애니메이션이 적용된 이미지 class HeroNetworkImage extends StatelessWidget { final String imageUrl; final String heroTag; final double? width; final double? height; final BoxFit fit; final VoidCallback? onTap; const HeroNetworkImage({ super.key, required this.imageUrl, required this.heroTag, this.width, this.height, this.fit = BoxFit.cover, this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Hero( tag: heroTag, child: OptimizedCachedNetworkImage( imageUrl: imageUrl, width: width, height: height, fit: fit, ), ), ); } }