flutter 图片加载类 图片的安全使用

flutter 图片加载类 图片的安全使用 建议使用方法2

版本1

1.1使用

dart 复制代码
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 关键:初始化全局配置
  KimiImageConfig.init(
    stalePeriod: const Duration(days: 3), // 磁盘缓存3天
    maxCacheObjects: 100, // 最多100个文件
  );
  
  runApp(MyApp());
}

1.2 依赖

dart 复制代码
dependencies:
  flutter:
    sdk: flutter
  # 缓存
  cached_network_image: ^3.3.0
  flutter_cache_manager: ^3.3.1
    #加载状态
  shimmer: ^3.0.0
  visibility_detector: ^0.4.0+2  # 懒加载

  crypto: ^3.0.3                  # 缓存Key生成

1.3工具类

dart 复制代码
import 'dart:io';
import 'dart:typed_data';
import 'dart:collection';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:shimmer/shimmer.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert';

/** 
 * KimiImage - 生产级图片组件(修复版)
 * 修复问题:
 * 1. 磁盘缓存大小限制(自定义CacheManager)
 * 2. 移除无效的maxAge参数
 * 3. 缓存Key生成优化(避免冲突)
 * 4. 异步文件检查(避免UI阻塞)
 * 5. 自动重试机制(指数退避)
 * 6. 支持暗黑模式
 * 7. 加载回调
 */

// ==================== 全局配置 ====================

class KimiImageConfig {
  static bool _initialized = false;
  static late final CacheManager cacheManager;
  
  static void init({
    Duration stalePeriod = const Duration(days: 3),
    int maxCacheObjects = 100,
  }) {
    if (_initialized) return;
    
    cacheManager = CacheManager(
      Config(
        'kimi_image_cache',
        stalePeriod: stalePeriod,
        maxNrOfCacheObjects: maxCacheObjects,
        fileService: HttpFileService(),
      ),
    );
    
    PaintingBinding.instance.imageCache
      ..maximumSize = 100
      ..maximumSizeBytes = 100 * 1024 * 1024;
      
    _initialized = true;
    debugPrint('✅ KimiImage初始化完成');
  }
  
  static Future<void> clearAll() async {
    await cacheManager.emptyCache();
    _CompressionCache().clear();
    PaintingBinding.instance.imageCache.clear();
  }
}

// ==================== 内存缓存(LRU + 并发控制)====================

class _CompressionCache {
  static final _CompressionCache _instance = _CompressionCache._internal();
  factory _CompressionCache() => _instance;
  _CompressionCache._internal();

  final LinkedHashMap<String, _CacheEntry> _cache = LinkedHashMap();
  final Map<String, Future<Uint8List?>> _loading = {};
  
  static const int _maxSize = 50;
  static const Duration _maxAge = Duration(minutes: 10);

  Uint8List? get(String key) {
    final entry = _cache.remove(key);
    if (entry == null) return null;
    if (DateTime.now().difference(entry.time) > _maxAge) return null;
    _cache[key] = entry;
    return entry.data;
  }

  void set(String key, Uint8List data) {
    while (_cache.length >= _maxSize) {
      _cache.remove(_cache.keys.first);
    }
    _cache[key] = _CacheEntry(data: data, time: DateTime.now());
  }

  Future<Uint8List?> load(String key, Future<Uint8List?> Function() loader) async {
    final cached = get(key);
    if (cached != null) return cached;
    
    if (_loading.containsKey(key)) return _loading[key]!;
    
    final future = loader().then((data) {
      if (data != null) set(key, data);
      _loading.remove(key);
      return data;
    }).catchError((e) {
      _loading.remove(key);
      throw e;
    });
    
    _loading[key] = future;
    return future;
  }

  void clear() {
    _cache.clear();
    _loading.clear();
  }
}

class _CacheEntry {
  final Uint8List data;
  final DateTime time;
  _CacheEntry({required this.data, required this.time});
}

// ==================== 核心组件 ====================

class KimiImage extends StatefulWidget {
  final String image;
  final double width;
  final double height;
  final BoxFit fit;
  final BorderRadius? borderRadius;
  final bool circle;
  final bool lazy;
  final Duration fadeDuration;
  final VoidCallback? onTap;
  final VoidCallback? onLoad; // 新增:加载成功回调
  final VoidCallback? onError; // 新增:加载失败回调
  final Map<String, String>? headers;
  
  final int quality;
  final int maxDimension;

  const KimiImage(
    this.image, {
    super.key,
    this.width = 100,
    this.height = 100,
    this.fit = BoxFit.cover,
    this.borderRadius,
    this.circle = false,
    this.lazy = true,
    this.fadeDuration = const Duration(milliseconds: 300),
    this.onTap,
    this.onLoad,
    this.onError,
    this.headers,
    this.quality = 85,
    this.maxDimension = 1080,
  });

  @override
  State<KimiImage> createState() => _KimiImageState();
}

class _KimiImageState extends State<KimiImage> {
  bool _shouldLoad = false;
  bool _disposed = false;
  int _retryCount = 0; // 新增:重试计数
  late final String _cacheKey;
  late final String _uniqueId;

  @override
  void initState() {
    super.initState();
    _uniqueId = '${widget.image.hashCode}_${hashCode}_${DateTime.now().microsecond}';
    _cacheKey = _generateCacheKey();
    if (!widget.lazy) _shouldLoad = true;
  }

  // 修复:明确的字符串拼接,避免类型隐式转换问题
  String _generateCacheKey() {
    final data = '${widget.image}_${widget.maxDimension}_${widget.quality}';
    return md5.convert(utf8.encode(data)).toString();
  }

  // 修复:异步检查文件,避免UI线程阻塞
  Future<ImageSourceType> _getSourceType() async {
    if (widget.image.startsWith('http')) return ImageSourceType.network;
    if (widget.image.startsWith('assets')) return ImageSourceType.asset;
    try {
      if (await File(widget.image).exists()) return ImageSourceType.file;
    } catch (e) {
      debugPrint('文件检查失败: $e');
    }
    return ImageSourceType.unknown;
  }

  void _safeSetState(VoidCallback fn) {
    if (mounted && !_disposed) setState(fn);
  }

  @override
  void dispose() {
    _disposed = true;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Widget content = widget.lazy && !_shouldLoad
        ? _buildLazyWrapper()
        : _buildContent();

    if (widget.circle) {
      content = ClipOval(child: content);
    } else if (widget.borderRadius != null) {
      content = ClipRRect(borderRadius: widget.borderRadius!, child: content);
    }

    if (widget.onTap != null) {
      content = GestureDetector(
        onTap: widget.onTap, 
        behavior: HitTestBehavior.opaque, 
        child: content,
      );
    }

    return content;
  }

  Widget _buildLazyWrapper() {
    return VisibilityDetector(
      key: Key('lazy_$_uniqueId'),
      onVisibilityChanged: (info) {
        if (info.visibleFraction > 0.05 && !_shouldLoad) {
          _safeSetState(() => _shouldLoad = true);
        }
      },
      child: _buildPlaceholder(),
    );
  }

  // 修复:使用FutureBuilder处理异步sourceType检查
  Widget _buildContent() {
    return FutureBuilder<ImageSourceType>(
      future: _getSourceType(),
      builder: (context, snapshot) {
        if (!snapshot.hasData) return _buildPlaceholder();
        
        switch (snapshot.data!) {
          case ImageSourceType.network:
            return _buildNetworkImage();
          case ImageSourceType.asset:
            return _buildAssetImage();
          case ImageSourceType.file:
            return _buildFileImage();
          case ImageSourceType.unknown:
            return _buildError('无效路径');
        }
      },
    );
  }

  /// 网络图片
  Widget _buildNetworkImage() {
    final pixelRatio = MediaQuery.of(context).devicePixelRatio;
    final memWidth = (widget.width * pixelRatio * 1.5).toInt();
    final memHeight = (widget.height * pixelRatio * 1.5).toInt();

    return CachedNetworkImage(
      imageUrl: widget.image,
      cacheManager: KimiImageConfig.cacheManager,
      width: widget.width,
      height: widget.height,
      fit: widget.fit,
      cacheKey: widget.image,
      memCacheWidth: memWidth,
      memCacheHeight: memHeight,
      // 修复:移除无效的maxAge参数
      httpHeaders: widget.headers,
      fadeInDuration: widget.fadeDuration,
      placeholder: (_, __) => _buildPlaceholder(),
      errorWidget: (_, __, ___) => _buildRetryWidget(), // 修复:使用重试组件
    );
  }

  /// 修复:自动重试组件(指数退避)
  Widget _buildRetryWidget() {
    // 自动重试3次
    if (_retryCount < 3) {
      Future.delayed(Duration(seconds: 1 << _retryCount), () {
        if (mounted) {
          _safeSetState(() => _retryCount++);
        }
      });
      return _buildPlaceholder();
    }
    
    // 超过3次显示错误
    widget.onError?.call();
    return GestureDetector(
      onTap: () => _safeSetState(() => _retryCount = 0),
      child: Container(
        width: widget.width,
        height: widget.height,
        color: Colors.grey[100],
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.wifi_off, color: Colors.grey[400], size: 32),
            Text('加载失败', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
            Text('点击重试', style: TextStyle(color: Colors.grey[400], fontSize: 10)),
          ],
        ),
      ),
    );
  }

  Widget _buildAssetImage() {
    return FutureBuilder<Uint8List?>(
      future: _CompressionCache().load(_cacheKey, _loadAsset),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return _buildPlaceholder();
        }
        if (snapshot.hasError || snapshot.data == null) {
          return _buildError('加载失败');
        }
        widget.onLoad?.call(); // 回调
        return _buildMemoryImage(snapshot.data!);
      },
    );
  }

  Future<Uint8List?> _loadAsset() async {
    final data = await rootBundle.load(widget.image);
    return _compress(data.buffer.asUint8List());
  }

  Widget _buildFileImage() {
    return FutureBuilder<Uint8List?>(
      future: _CompressionCache().load(_cacheKey, _loadFile),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return _buildPlaceholder();
        }
        if (snapshot.hasError || snapshot.data == null) {
          return _buildError('文件不存在');
        }
        widget.onLoad?.call();
        return _buildMemoryImage(snapshot.data!);
      },
    );
  }

  Future<Uint8List?> _loadFile() async {
    final file = File(widget.image);
    if (!await file.exists()) return null;
    return _compress(await file.readAsBytes());
  }

  Future<Uint8List?> _compress(Uint8List bytes) async {
    if (bytes.length < 50 * 1024) return bytes;
    try {
      return await FlutterImageCompress.compressWithList(
        bytes,
        quality: widget.quality,
        minWidth: widget.maxDimension,
        minHeight: widget.maxDimension,
        format: CompressFormat.webp,
        autoCorrectionAngle: true,
      );
    } catch (e) {
      debugPrint('压缩失败: $e');
      return bytes;
    }
  }

  Widget _buildMemoryImage(Uint8List bytes) {
    return TweenAnimationBuilder<double>(
      tween: Tween(begin: 0.0, end: 1.0),
      duration: widget.fadeDuration,
      builder: (context, value, child) => Opacity(opacity: value, child: child),
      child: Image.memory(
        bytes,
        width: widget.width,
        height: widget.height,
        fit: widget.fit,
        gaplessPlayback: true,
      ),
    );
  }

  /// 修复:支持暗黑模式的Shimmer
  Widget _buildPlaceholder() {
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return Shimmer.fromColors(
      baseColor: isDark ? Colors.grey[800]! : Colors.grey[300]!,
      highlightColor: isDark ? Colors.grey[700]! : Colors.grey[100]!,
      child: Container(
        width: widget.width,
        height: widget.height,
        color: isDark ? Colors.black : Colors.white,
      ),
    );
  }

  Widget _buildError(String msg) {
    widget.onError?.call();
    return GestureDetector(
      onTap: () => _safeSetState(() {}),
      child: Container(
        width: widget.width,
        height: widget.height,
        color: Colors.grey[100],
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.broken_image, color: Colors.grey[400], size: 32),
            Text(msg, style: TextStyle(color: Colors.grey[500], fontSize: 12)),
          ],
        ),
      ),
    );
  }
}

enum ImageSourceType { network, asset, file, unknown }

// ==================== 便捷组件 ====================

class KimiImageList extends StatelessWidget {
  final List<String> images;
  final Axis direction;
  final double itemWidth;
  final double itemHeight;
  final double spacing;
  final EdgeInsets padding;
  final Function(int index, String url)? onTap;

  const KimiImageList({
    super.key,
    required this.images,
    this.direction = Axis.horizontal,
    this.itemWidth = 100,
    this.itemHeight = 100,
    this.spacing = 8,
    this.padding = const EdgeInsets.all(16),
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: direction == Axis.horizontal ? itemHeight : null,
      child: ListView.separated(
        scrollDirection: direction,
        padding: padding,
        itemCount: images.length,
        cacheExtent: 200,
        addAutomaticKeepAlives: false,
        separatorBuilder: (_, __) => SizedBox(
          width: direction == Axis.horizontal ? spacing : 0,
          height: direction == Axis.vertical ? spacing : 0,
        ),
        itemBuilder: (context, index) => KimiImage(
          images[index],
          width: itemWidth,
          height: itemHeight,
          maxDimension: 400,
          quality: 75,
          lazy: true,
          onTap: () => onTap?.call(index, images[index]),
        ),
      ),
    );
  }
}

class KimiAvatar extends StatelessWidget {
  final String url;
  final double size;
  final VoidCallback? onTap;

  const KimiAvatar(this.url, {super.key, this.size = 48, this.onTap});

  @override
  Widget build(BuildContext context) {
    return KimiImage(
      url,
      width: size,
      height: size,
      circle: true,
      maxDimension: 128,
      quality: 80,
      lazy: false,
      onTap: onTap,
    );
  }
}

2 优化版

2.1 pubspec.yaml 必须添加的依赖

dart 复制代码
dependencies:
  flutter:
    sdk: flutter
  cached_network_image: ^3.3.0
  flutter_image_compress: ^2.1.0
  visibility_detector: ^0.4.0+2
  shimmer: ^3.0.0
  crypto: ^3.0.3
  flutter_cache_manager: ^3.3.1

2.2 使用方式

dart 复制代码
// 1. main.dart 初始化
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  ImageConfig.init(); // 必须调用
  runApp(MyApp());
}

// 2. 基础使用
OptimizedImage(
  'https://xxx.jpg',
  width: 100,
  height: 100,
  circle: true,
  onLoad: () {},
  onError: () {},
)

// 3. 清理缓存
ImageConfig.clearCache();

2.3 代码

dart 复制代码
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'package:shimmer/shimmer.dart';
import 'package:crypto/crypto.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';

// ==================== 配置 ====================
class ImageConfig {
  static bool _initialized = false;
  static late final CacheManager _cacheManager;

  static CacheManager get cacheManager {
    if (!_initialized) {
      // 自动初始化而非抛异常
      init();
    }
    return _cacheManager;
  }

  static void init({
    Duration stalePeriod = const Duration(days: 7),
    int maxCacheObjects = 100,
  }) {
    if (_initialized) return;
    _cacheManager = CacheManager(
      Config(
        'app_image_cache',
        stalePeriod: stalePeriod,
        maxNrOfCacheObjects: maxCacheObjects,
        fileService: HttpFileService(),
      ),
    );
    PaintingBinding.instance.imageCache
      ..maximumSize = 100
      ..maximumSizeBytes = 100 * 1024 * 1024;
    _initialized = true;
  }

  static Future<void> clearCache() async {
    if (!_initialized) return;
    await _cacheManager.emptyCache();
    _ImageCache().clear();
    PaintingBinding.instance.imageCache.clear();
  }
}

// ==================== 内存缓存(豆包方案优化)====================
class _ImageCache {
  static final _ImageCache _instance = _ImageCache._internal();
  factory _ImageCache() => _instance;
  _ImageCache._internal();

  final LinkedHashMap<String, _Entry> _cache = LinkedHashMap();
  final Map<String, Future<Uint8List?>> _loading = {};

  static const int _maxCount = 50;
  static const Duration _maxAge = Duration(minutes: 10);

  int _currentMemoryBytes = 0;
  static const int _maxMemoryBytes = 50 * 1024 * 1024; // 提高到50MB

  Uint8List? get(String key) {
    final entry = _cache.remove(key);
    if (entry == null) return null;
    if (DateTime.now().difference(entry.time) > _maxAge) {
      _currentMemoryBytes -= entry.data.length;
      return null;
    }
    _cache[key] = entry;
    return entry.data;
  }

  void set(String key, Uint8List data) {
    final old = _cache.remove(key);
    if (old != null) _currentMemoryBytes -= old.data.length;

    // 清理直到满足限制
    while (_cache.length >= _maxCount ||
        (_currentMemoryBytes + data.length > _maxMemoryBytes && _cache.isNotEmpty)) {
      final oldest = _cache.remove(_cache.keys.first)!;
      _currentMemoryBytes -= oldest.data.length;
    }

    _cache[key] = _Entry(data: data, time: DateTime.now());
    _currentMemoryBytes += data.length;
  }

  Future<Uint8List?> load(
      String key,
      Future<Uint8List?> Function() loader, {
        CancelToken? cancelToken,
      }) async {
    final cached = get(key);
    if (cached != null) return cached;
    if (_loading.containsKey(key)) return _loading[key]!;
    if (cancelToken?.isCancelled ?? false) return null;

    final future = loader().then((data) {
      if (data != null && !(cancelToken?.isCancelled ?? false)) set(key, data);
      _loading.remove(key);
      return data;
    }).catchError((e) {
      _loading.remove(key);
      throw e;
    });

    _loading[key] = future;
    return future;
  }

  void clear() {
    _cache.clear();
    _loading.clear();
    _currentMemoryBytes = 0;
  }
}

class _Entry {
  final Uint8List data;
  final DateTime time;
  _Entry({required this.data, required this.time});
}

class CancelToken {
  bool isCancelled = false;
  void cancel() => isCancelled = true;
}

// ==================== 核心组件(关键修复)====================
class OptimizedImage extends StatefulWidget {
  final String image;
  final double? width;
  final double? height;
  final BoxFit fit;
  final BorderRadius? borderRadius;
  final bool circle;
  final bool lazy;
  final Duration fadeDuration;
  final VoidCallback? onTap;
  final VoidCallback? onLoad;
  final VoidCallback? onError;
  final Map<String, String>? headers;
  final int quality;
  final int maxDimension;
  final Widget? placeholder;
  final Widget? errorWidget;

  const OptimizedImage(
      this.image, {
        super.key,
        this.width,
        this.height,
        this.fit = BoxFit.cover,
        this.borderRadius,
        this.circle = false,
        this.lazy = true,
        this.fadeDuration = const Duration(milliseconds: 300),
        this.onTap,
        this.onLoad,
        this.onError,
        this.headers,
        this.quality = 85,
        this.maxDimension = 1080,
        this.placeholder,
        this.errorWidget,
      });

  @override
  State<OptimizedImage> createState() => _OptimizedImageState();
}

class _OptimizedImageState extends State<OptimizedImage> {
  bool _shouldLoad = false;
  late final String _cacheKey;
  late final CancelToken _cancelToken;
  int _retryCount = 0;
  static const int _maxRetry = 3;

  @override
  void initState() {
    super.initState();
    _cancelToken = CancelToken();
    _cacheKey = _generateCacheKey();
    if (!widget.lazy) _shouldLoad = true;
  }

  @override
  void dispose() {
    _cancelToken.cancel();
    super.dispose();
  }

  String _generateCacheKey() {
    String url = widget.image;
    if (url.contains('?')) url = url.split('?').first;
    return md5.convert(utf8.encode('${url}_${widget.maxDimension}_${widget.quality}')).toString();
  }

  void _safeSetState(VoidCallback fn) {
    if (mounted) setState(fn);
  }

  @override
  Widget build(BuildContext context) {
    Widget content = widget.lazy && !_shouldLoad ? _buildLazyWrapper() : _buildContent();

    if (widget.circle) {
      content = ClipOval(child: content);
    } else if (widget.borderRadius != null) {
      content = ClipRRect(borderRadius: widget.borderRadius!, child: content);
    }

    if (widget.onTap != null) {
      content = GestureDetector(onTap: widget.onTap, behavior: HitTestBehavior.opaque, child: content);
    }

    // 关键修复:正确处理null尺寸
    if (widget.width != null && widget.height != null) {
      return SizedBox(width: widget.width, height: widget.height, child: content);
    }
    return content;
  }

  Widget _buildLazyWrapper() {
    return VisibilityDetector(
      key: Key('lazy_${widget.image}_$hashCode'),
      onVisibilityChanged: (info) {
        if (info.visibleFraction > 0.05 && !_shouldLoad) {
          _safeSetState(() => _shouldLoad = true);
        }
      },
      child: _buildPlaceholder(),
    );
  }

  // ==================== 关键修复:区分网络与本地图片 ====================
  Widget _buildContent() {
    // 网络图片:用CachedNetworkImage(最优性能)
    if (widget.image.startsWith('http')) {
      return _buildNetworkImage();
    }

    // 本地图片:走压缩缓存
    return FutureBuilder<ImageSourceType>(
      future: _getLocalSourceType(),
      builder: (context, snapshot) {
        if (!snapshot.hasData) return _buildPlaceholder();
        if (snapshot.data == ImageSourceType.unknown) return _buildError('无效路径');
        return _buildLocalImage();
      },
    );
  }

  /// 关键修复:网络图片用CachedNetworkImage,渐进加载+内存限制
  Widget _buildNetworkImage() {
    final pixelRatio = MediaQuery.of(context).devicePixelRatio;
    final memWidth = widget.width != null ? (widget.width! * pixelRatio).toInt() : null;
    final memHeight = widget.height != null ? (widget.height! * pixelRatio).toInt() : null;

    return CachedNetworkImage(
      imageUrl: widget.image,
      cacheManager: ImageConfig.cacheManager,
      width: widget.width,
      height: widget.height,
      fit: widget.fit,
      cacheKey: _getBaseUrl(widget.image),
      memCacheWidth: memWidth,  // 关键:限制解码内存
      memCacheHeight: memHeight,
      httpHeaders: widget.headers,
      fadeInDuration: widget.fadeDuration,
      placeholder: (_, __) => _buildPlaceholder(),
      errorWidget: (_, __, ___) {
        // 自动重试逻辑
        if (_retryCount < _maxRetry) {
          _scheduleRetry();
          return _buildPlaceholder();
        }
        widget.onError?.call();
        return _buildErrorWidget();
      },
    );
  }

  /// 本地图片:压缩后缓存
  Widget _buildLocalImage() {
    return FutureBuilder<Uint8List?>(
      future: _ImageCache().load(_cacheKey, _loadLocalImage, cancelToken: _cancelToken),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return _buildPlaceholder();
        }
        if (snapshot.hasError || snapshot.data == null) {
          if (_retryCount < _maxRetry) {
            _scheduleRetry();
            return _buildPlaceholder();
          }
          widget.onError?.call();
          return _buildError('加载失败');
        }
        widget.onLoad?.call();
        return _buildMemoryImage(snapshot.data!);
      },
    );
  }

  Future<Uint8List?> _loadLocalImage() async {
    Uint8List? bytes;

    if (widget.image.startsWith('assets')) {
      final data = await rootBundle.load(widget.image);
      bytes = data.buffer.asUint8List();
    } else {
      final file = File(widget.image);
      if (await file.exists()) {
        bytes = await file.readAsBytes();
      }
    }

    if (bytes == null) return null;
    return _compress(bytes);
  }

  Future<ImageSourceType> _getLocalSourceType() async {
    if (widget.image.startsWith('assets')) return ImageSourceType.asset;
    try {
      if (await File(widget.image).exists()) return ImageSourceType.file;
    } catch (_) {}
    return ImageSourceType.unknown;
  }

  String _getBaseUrl(String url) {
    return url.contains('?') ? url.substring(0, url.indexOf('?')) : url;
  }

  void _scheduleRetry() {
    final delay = Duration(seconds: 1 << _retryCount);
    Future.delayed(delay, () {
      if (mounted) {
        _retryCount++;
        _safeSetState(() {});
      }
    });
  }

  Future<Uint8List?> _compress(Uint8List bytes) async {
    if (bytes.length < 50 * 1024) return bytes;
    try {
      return await FlutterImageCompress.compressWithList(
        bytes,
        quality: widget.quality,
        minWidth: widget.maxDimension,
        minHeight: widget.maxDimension,
        format: CompressFormat.webp,
      );
    } catch (e) {
      return bytes;
    }
  }

  Widget _buildMemoryImage(Uint8List bytes) {
    return TweenAnimationBuilder<double>(
      tween: Tween(begin: 0, end: 1),
      duration: widget.fadeDuration,
      builder: (_, value, child) => Opacity(opacity: value, child: child),
      child: Image.memory(
        bytes,
        fit: widget.fit,
        gaplessPlayback: true,
        width: widget.width,
        height: widget.height,
      ),
    );
  }

  Widget _buildPlaceholder() {
    if (widget.placeholder != null) return widget.placeholder!;
    final isDark = Theme.of(context).brightness == Brightness.dark;
    return Shimmer.fromColors(
      baseColor: isDark ? Colors.grey[800]! : Colors.grey[300]!,
      highlightColor: isDark ? Colors.grey[700]! : Colors.grey[100]!,
      child: Container(color: isDark ? Colors.black : Colors.white),
    );
  }

  Widget _buildError(String msg) {
    if (widget.errorWidget != null) return widget.errorWidget!;
    return GestureDetector(
      onTap: () => _safeSetState(() => _retryCount = 0),
      child: Container(
        color: Colors.grey[100],
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.broken_image, color: Colors.grey[400]),
            Text(msg, style: TextStyle(color: Colors.grey[500], fontSize: 12)),
          ],
        ),
      ),
    );
  }

  Widget _buildErrorWidget() => _buildError('加载失败');
}

enum ImageSourceType { network, asset, file, unknown }

// ==================== 列表组件 ====================
class OptimizedImageList extends StatelessWidget {
  final List<String> images;
  final Axis direction;
  final double itemWidth;
  final double itemHeight;
  final double spacing;
  final EdgeInsets padding;
  final Function(int index, String url)? onTap;

  const OptimizedImageList({
    super.key,
    required this.images,
    this.direction = Axis.horizontal,
    this.itemWidth = 100,
    this.itemHeight = 100,
    this.spacing = 8,
    this.padding = const EdgeInsets.all(16),
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: direction == Axis.horizontal ? itemHeight : null,
      child: ListView.separated(
        scrollDirection: direction,
        padding: padding,
        itemCount: images.length,
        cacheExtent: 200,
        addAutomaticKeepAlives: false,
        separatorBuilder: (_, __) => SizedBox(
          width: direction == Axis.horizontal ? spacing : 0,
          height: direction == Axis.vertical ? spacing : 0,
        ),
        itemBuilder: (context, index) => OptimizedImage(
          images[index],
          width: itemWidth,
          height: itemHeight,
          maxDimension: 400,
          quality: 75,
          lazy: true,
          onTap: () => onTap?.call(index, images[index]),
        ),
      ),
    );
  }
}
相关推荐
zs宝来了1 小时前
软件供应链安全:SBOM 与签名验证
安全·devsecops·云安全
EasyGBS1 小时前
国标GB28181视频分析平台EasyGBS视频质量诊断为平安社区视频监控筑牢安全防线
人工智能·安全·音视频
哇哦9821 小时前
渗透安全(渗透防御)①
安全·防御·渗透防御
zs宝来了2 小时前
Falco 运行时安全:eBPF 与系统调用监控
安全·devsecops·云安全
m0_738120722 小时前
网络安全编程——Python编写Python编写基于UDP的主机发现工具(完结:解码ICMP头)
python·网络协议·安全·web安全·udp
胡楚昊2 小时前
openClaw CVE-2026-25253复现与简单分析
安全
哇哦9822 小时前
渗透安全(渗透防御)③
安全·https·渗透·dns·渗透防御
信创DevOps先锋2 小时前
企业级开源治理新选择:Gitee CodePecker SCA如何重塑软件供应链安全
安全·gitee·开源
csdn_aspnet2 小时前
如何保护您的 .NET Web API 免受常见安全威胁
安全·xss·csrf·.net core·cors