Files
MaliangAINovalWriter/AINoval/lib/services/image_cache_service.dart
2025-09-10 00:07:52 +08:00

366 lines
11 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ainoval/utils/logger.dart';
/// 图片缓存服务
/// 负责处理用户设置的图片缓存、自适应显示和内存管理
class ImageCacheService {
static final ImageCacheService _instance = ImageCacheService._internal();
factory ImageCacheService() => _instance;
ImageCacheService._internal();
// 内存缓存映射
final Map<String, ui.Image> _memoryCache = {};
final Map<String, ImageInfo> _imageInfoCache = {};
// 缓存限制
static const int _maxCacheSize = 50; // 最大缓存图片数量
static const int _maxMemoryUsage = 100 * 1024 * 1024; // 100MB内存限制
int _currentMemoryUsage = 0;
/// 获取自适应图片组件
Widget getAdaptiveImage({
required String imageUrl,
required double width,
required double height,
BoxFit fit = BoxFit.cover,
String? placeholder,
Color? backgroundColor,
BorderRadius? borderRadius,
double? aspectRatio,
}) {
return FutureBuilder<ui.Image?>(
future: _loadAndCacheImage(imageUrl),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return _buildAdaptiveImageWidget(
snapshot.data!,
width: width,
height: height,
fit: fit,
backgroundColor: backgroundColor,
borderRadius: borderRadius,
aspectRatio: aspectRatio,
);
}
// 显示占位符或加载指示器
return _buildPlaceholder(
width: width,
height: height,
backgroundColor: backgroundColor,
borderRadius: borderRadius,
isLoading: !snapshot.hasError,
placeholder: placeholder,
);
},
);
}
/// 构建自适应图片组件
Widget _buildAdaptiveImageWidget(
ui.Image image, {
required double width,
required double height,
BoxFit fit = BoxFit.cover,
Color? backgroundColor,
BorderRadius? borderRadius,
double? aspectRatio,
}) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: borderRadius,
),
clipBehavior: borderRadius != null ? Clip.antiAlias : Clip.none,
child: CustomPaint(
painter: _AdaptiveImagePainter(
image: image,
fit: fit,
aspectRatio: aspectRatio,
),
size: Size(width, height),
),
);
}
/// 构建占位符
Widget _buildPlaceholder({
required double width,
required double height,
Color? backgroundColor,
BorderRadius? borderRadius,
bool isLoading = false,
String? placeholder,
}) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: backgroundColor ?? Colors.grey[200],
borderRadius: borderRadius,
),
child: isLoading
? const Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: Icon(
Icons.broken_image,
color: Colors.grey[400],
size: math.min(width, height) * 0.3,
),
);
}
/// 加载并缓存图片
Future<ui.Image?> _loadAndCacheImage(String imageUrl) async {
try {
// 检查内存缓存
if (_memoryCache.containsKey(imageUrl)) {
AppLogger.d('ImageCache', '从内存缓存加载图片: $imageUrl');
return _memoryCache[imageUrl];
}
// 加载图片
ui.Image? image;
if (imageUrl.startsWith('http')) {
// 网络图片
image = await _loadNetworkImage(imageUrl);
} else if (imageUrl.startsWith('assets/')) {
// 资源图片
image = await _loadAssetImage(imageUrl);
} else {
// 本地文件图片
image = await _loadFileImage(imageUrl);
}
if (image != null) {
await _cacheImage(imageUrl, image);
}
return image;
} catch (e) {
AppLogger.e('ImageCache', '加载图片失败: $imageUrl', e);
return null;
}
}
/// 加载网络图片
Future<ui.Image?> _loadNetworkImage(String url) async {
try {
final NetworkImage provider = NetworkImage(url);
final ImageStream stream = provider.resolve(ImageConfiguration.empty);
final Completer<ui.Image> completer = Completer<ui.Image>();
late ImageStreamListener listener;
listener = ImageStreamListener(
(ImageInfo info, bool synchronousCall) {
completer.complete(info.image);
stream.removeListener(listener);
},
onError: (dynamic exception, StackTrace? stackTrace) {
completer.completeError(exception, stackTrace);
stream.removeListener(listener);
},
);
stream.addListener(listener);
return await completer.future;
} catch (e) {
AppLogger.e('ImageCache', '加载网络图片失败: $url', e);
return null;
}
}
/// 加载资源图片
Future<ui.Image?> _loadAssetImage(String assetPath) async {
try {
final ByteData data = await rootBundle.load(assetPath);
final Uint8List bytes = data.buffer.asUint8List();
final ui.Codec codec = await ui.instantiateImageCodec(bytes);
final ui.FrameInfo frame = await codec.getNextFrame();
return frame.image;
} catch (e) {
AppLogger.e('ImageCache', '加载资源图片失败: $assetPath', e);
return null;
}
}
/// 加载本地文件图片
Future<ui.Image?> _loadFileImage(String filePath) async {
try {
final File file = File(filePath);
if (!await file.exists()) {
return null;
}
final Uint8List bytes = await file.readAsBytes();
final ui.Codec codec = await ui.instantiateImageCodec(bytes);
final ui.FrameInfo frame = await codec.getNextFrame();
return frame.image;
} catch (e) {
AppLogger.e('ImageCache', '加载本地图片失败: $filePath', e);
return null;
}
}
/// 缓存图片
Future<void> _cacheImage(String key, ui.Image image) async {
// 检查缓存大小限制
if (_memoryCache.length >= _maxCacheSize) {
_evictOldestCache();
}
// 估算图片内存使用
final int imageBytes = image.width * image.height * 4; // RGBA
// 检查内存限制
if (_currentMemoryUsage + imageBytes > _maxMemoryUsage) {
await _evictCacheToFitMemory(imageBytes);
}
_memoryCache[key] = image;
_currentMemoryUsage += imageBytes;
AppLogger.d('ImageCache',
'缓存图片: $key, 尺寸: ${image.width}x${image.height}, 内存使用: ${_currentMemoryUsage ~/ 1024}KB');
}
/// 移除最旧的缓存
void _evictOldestCache() {
if (_memoryCache.isNotEmpty) {
final String firstKey = _memoryCache.keys.first;
final ui.Image? image = _memoryCache.remove(firstKey);
if (image != null) {
final int imageBytes = image.width * image.height * 4;
_currentMemoryUsage -= imageBytes;
image.dispose();
}
_imageInfoCache.remove(firstKey);
}
}
/// 移除缓存以腾出内存空间
Future<void> _evictCacheToFitMemory(int requiredBytes) async {
while (_currentMemoryUsage + requiredBytes > _maxMemoryUsage &&
_memoryCache.isNotEmpty) {
_evictOldestCache();
}
}
/// 清理所有缓存
void clearCache() {
for (final ui.Image image in _memoryCache.values) {
image.dispose();
}
_memoryCache.clear();
_imageInfoCache.clear();
_currentMemoryUsage = 0;
AppLogger.i('ImageCache', '清理所有图片缓存');
}
/// 预加载图片
Future<void> preloadImage(String imageUrl) async {
if (!_memoryCache.containsKey(imageUrl)) {
await _loadAndCacheImage(imageUrl);
}
}
/// 获取缓存统计信息
Map<String, dynamic> getCacheStats() {
return {
'cacheSize': _memoryCache.length,
'memoryUsage': _currentMemoryUsage,
'memoryUsageKB': _currentMemoryUsage ~/ 1024,
'memoryUsageMB': _currentMemoryUsage ~/ (1024 * 1024),
};
}
}
/// 自适应图片绘制器
class _AdaptiveImagePainter extends CustomPainter {
final ui.Image image;
final BoxFit fit;
final double? aspectRatio;
_AdaptiveImagePainter({
required this.image,
required this.fit,
this.aspectRatio,
});
@override
void paint(Canvas canvas, Size size) {
final double imageAspectRatio = image.width / image.height;
final double containerAspectRatio = size.width / size.height;
Rect srcRect = Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble());
Rect dstRect;
switch (fit) {
case BoxFit.cover:
if (imageAspectRatio > containerAspectRatio) {
// 图片更宽,裁剪左右
final double newWidth = image.height * containerAspectRatio;
final double offsetX = (image.width - newWidth) / 2;
srcRect = Rect.fromLTWH(offsetX, 0, newWidth, image.height.toDouble());
} else {
// 图片更高,裁剪上下
final double newHeight = image.width / containerAspectRatio;
final double offsetY = (image.height - newHeight) / 2;
srcRect = Rect.fromLTWH(0, offsetY, image.width.toDouble(), newHeight);
}
dstRect = Rect.fromLTWH(0, 0, size.width, size.height);
break;
case BoxFit.contain:
if (imageAspectRatio > containerAspectRatio) {
// 图片更宽,适应宽度
final double newHeight = size.width / imageAspectRatio;
final double offsetY = (size.height - newHeight) / 2;
dstRect = Rect.fromLTWH(0, offsetY, size.width, newHeight);
} else {
// 图片更高,适应高度
final double newWidth = size.height * imageAspectRatio;
final double offsetX = (size.width - newWidth) / 2;
dstRect = Rect.fromLTWH(offsetX, 0, newWidth, size.height);
}
break;
case BoxFit.fill:
default:
dstRect = Rect.fromLTWH(0, 0, size.width, size.height);
break;
}
// 使用高质量图片渲染
final Paint paint = Paint()
..filterQuality = FilterQuality.high
..isAntiAlias = true;
canvas.drawImageRect(image, srcRect, dstRect, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate is! _AdaptiveImagePainter ||
oldDelegate.image != image ||
oldDelegate.fit != fit ||
oldDelegate.aspectRatio != aspectRatio;
}
}