import 'dart:convert'; import 'dart:typed_data'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:nomo/app/extensions/img_extension.dart'; import 'package:shimmer/shimmer.dart'; import 'dart:io'; import '../constants/configs.dart'; enum ImageSourceType { network, asset, file, memory } class IXImage extends StatelessWidget { // 静态缓存,避免重复解码 base64 数据 static final Map _memoryImageCache = {}; /// 清理内存图片缓存 static void clearMemoryCache() { _memoryImageCache.clear(); } /// 获取缓存大小 static int get cacheSize => _memoryImageCache.length; const IXImage({ super.key, required this.source, required this.width, required this.height, this.sourceType = ImageSourceType.network, this.fadeOutDuration, this.fadeInDuration, this.origAspectRatio, this.borderRadius, this.fit, this.placeholderDuration = const Duration(milliseconds: 400), }); final String source; final ImageSourceType sourceType; final double width; final double height; final Duration? fadeOutDuration; final Duration? fadeInDuration; final double? origAspectRatio; final double? borderRadius; final BoxFit? fit; final Duration placeholderDuration; @override Widget build(BuildContext context) { int? memCacheWidth, memCacheHeight; double aspectRatio = (width / height).toDouble(); void setMemCacheSizes() { if (aspectRatio > 1) { memCacheHeight = height.cacheSize(context); } else if (aspectRatio < 1) { memCacheWidth = width.cacheSize(context); } else { if (origAspectRatio != null && origAspectRatio! > 1) { memCacheWidth = width.cacheSize(context); } else if (origAspectRatio != null && origAspectRatio! < 1) { memCacheHeight = height.cacheSize(context); } else { memCacheWidth = width.cacheSize(context); memCacheHeight = height.cacheSize(context); } } } setMemCacheSizes(); if (memCacheWidth == null && memCacheHeight == null) { memCacheWidth = width.toInt(); } return ClipRRect( key: ValueKey('ix_image_${source.hashCode}_${width}_$height'), clipBehavior: Clip.antiAlias, borderRadius: BorderRadius.circular(borderRadius ?? 0), child: _buildImage(context, memCacheWidth, memCacheHeight), ); } Widget _buildImage( BuildContext context, int? memCacheWidth, int? memCacheHeight, ) { switch (sourceType) { case ImageSourceType.network: String url = source; if (source.indexOf("http") != 0) { url = "${Configs.assetUrl}/$source"; } return CachedNetworkImage( key: ValueKey(url), imageUrl: url, width: width, height: height, memCacheWidth: memCacheWidth, memCacheHeight: memCacheHeight, fit: fit ?? BoxFit.cover, fadeOutDuration: fadeOutDuration ?? const Duration(milliseconds: 250), fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 350), filterQuality: FilterQuality.medium, errorWidget: (context, url, error) => _buildErrorWidget(context, url, error), placeholder: (context, url) => _buildPlaceholder(context, url), ); case ImageSourceType.asset: return Image.asset( source, width: width, height: height, fit: fit ?? BoxFit.cover, alignment: fit == BoxFit.fitWidth ? Alignment.topCenter : Alignment.center, errorBuilder: (context, error, stackTrace) => _buildErrorWidget(context, source, error), ); case ImageSourceType.file: return Image.file( File(source), width: width, height: height, fit: fit ?? BoxFit.cover, errorBuilder: (context, error, stackTrace) => _buildErrorWidget(context, source, error), ); case ImageSourceType.memory: // 使用缓存避免重复解码 Uint8List imageBytes; if (_memoryImageCache.containsKey(source)) { imageBytes = _memoryImageCache[source]!; } else { imageBytes = base64Decode(source); _memoryImageCache[source] = imageBytes; } return Image.memory( imageBytes, key: ValueKey('memory_${source.hashCode}'), width: width, height: height, fit: fit ?? BoxFit.cover, errorBuilder: (context, error, stackTrace) => _buildErrorWidget(context, source, error), ); } } Widget _buildErrorWidget(BuildContext context, String url, dynamic error) { return _buildSmoothPlaceholder(context, isError: false); } Widget _buildPlaceholder(BuildContext context, String url) { return _buildSmoothPlaceholder(context, isError: false); } Widget _buildSmoothPlaceholder( BuildContext context, { required bool isError, }) { return AnimatedContainer( duration: placeholderDuration, curve: Curves.easeInOut, width: width, height: height, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: isError ? [ Colors.red.withValues(alpha: 0.15), Colors.red.withValues(alpha: 0.08), Colors.red.withValues(alpha: 0.15), ] : [ Colors.grey.withValues(alpha: 0.25), Colors.grey.withValues(alpha: 0.08), Colors.grey.withValues(alpha: 0.25), ], stops: const [0.0, 0.5, 1.0], ), borderRadius: BorderRadius.circular(borderRadius ?? 0), ), child: Shimmer.fromColors( baseColor: isError ? Colors.red.withValues(alpha: 0.3) : Colors.grey.withValues(alpha: 0.4), highlightColor: isError ? Colors.red.withValues(alpha: 0.1) : Colors.grey.withValues(alpha: 0.1), period: const Duration(milliseconds: 1800), child: Container( width: width, height: height, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(borderRadius ?? 0), ), ), ), ); } }