ix_image.dart 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import 'dart:convert';
  2. import 'dart:typed_data';
  3. import 'package:cached_network_image/cached_network_image.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:nomo/app/extensions/img_extension.dart';
  6. import 'package:shimmer/shimmer.dart';
  7. import 'dart:io';
  8. import '../constants/configs.dart';
  9. enum ImageSourceType { network, asset, file, memory }
  10. class IXImage extends StatelessWidget {
  11. // 静态缓存,避免重复解码 base64 数据
  12. static final Map<String, Uint8List> _memoryImageCache = {};
  13. /// 清理内存图片缓存
  14. static void clearMemoryCache() {
  15. _memoryImageCache.clear();
  16. }
  17. /// 获取缓存大小
  18. static int get cacheSize => _memoryImageCache.length;
  19. const IXImage({
  20. super.key,
  21. required this.source,
  22. required this.width,
  23. required this.height,
  24. this.sourceType = ImageSourceType.network,
  25. this.fadeOutDuration,
  26. this.fadeInDuration,
  27. this.origAspectRatio,
  28. this.borderRadius,
  29. this.fit,
  30. this.placeholderDuration = const Duration(milliseconds: 400),
  31. });
  32. final String source;
  33. final ImageSourceType sourceType;
  34. final double width;
  35. final double height;
  36. final Duration? fadeOutDuration;
  37. final Duration? fadeInDuration;
  38. final double? origAspectRatio;
  39. final double? borderRadius;
  40. final BoxFit? fit;
  41. final Duration placeholderDuration;
  42. @override
  43. Widget build(BuildContext context) {
  44. int? memCacheWidth, memCacheHeight;
  45. double aspectRatio = (width / height).toDouble();
  46. void setMemCacheSizes() {
  47. if (aspectRatio > 1) {
  48. memCacheHeight = height.cacheSize(context);
  49. } else if (aspectRatio < 1) {
  50. memCacheWidth = width.cacheSize(context);
  51. } else {
  52. if (origAspectRatio != null && origAspectRatio! > 1) {
  53. memCacheWidth = width.cacheSize(context);
  54. } else if (origAspectRatio != null && origAspectRatio! < 1) {
  55. memCacheHeight = height.cacheSize(context);
  56. } else {
  57. memCacheWidth = width.cacheSize(context);
  58. memCacheHeight = height.cacheSize(context);
  59. }
  60. }
  61. }
  62. setMemCacheSizes();
  63. if (memCacheWidth == null && memCacheHeight == null) {
  64. memCacheWidth = width.toInt();
  65. }
  66. return ClipRRect(
  67. key: ValueKey('ix_image_${source.hashCode}_${width}_$height'),
  68. clipBehavior: Clip.antiAlias,
  69. borderRadius: BorderRadius.circular(borderRadius ?? 0),
  70. child: _buildImage(context, memCacheWidth, memCacheHeight),
  71. );
  72. }
  73. Widget _buildImage(
  74. BuildContext context,
  75. int? memCacheWidth,
  76. int? memCacheHeight,
  77. ) {
  78. switch (sourceType) {
  79. case ImageSourceType.network:
  80. String url = source;
  81. if (source.indexOf("http") != 0) {
  82. url = "${Configs.assetUrl}/$source";
  83. }
  84. return CachedNetworkImage(
  85. key: ValueKey(url),
  86. imageUrl: url,
  87. width: width,
  88. height: height,
  89. memCacheWidth: memCacheWidth,
  90. memCacheHeight: memCacheHeight,
  91. fit: fit ?? BoxFit.cover,
  92. fadeOutDuration: fadeOutDuration ?? const Duration(milliseconds: 250),
  93. fadeInDuration: fadeInDuration ?? const Duration(milliseconds: 350),
  94. filterQuality: FilterQuality.medium,
  95. errorWidget: (context, url, error) =>
  96. _buildErrorWidget(context, url, error),
  97. placeholder: (context, url) => _buildPlaceholder(context, url),
  98. );
  99. case ImageSourceType.asset:
  100. return Image.asset(
  101. source,
  102. width: width,
  103. height: height,
  104. fit: fit ?? BoxFit.cover,
  105. alignment: fit == BoxFit.fitWidth
  106. ? Alignment.topCenter
  107. : Alignment.center,
  108. errorBuilder: (context, error, stackTrace) =>
  109. _buildErrorWidget(context, source, error),
  110. );
  111. case ImageSourceType.file:
  112. return Image.file(
  113. File(source),
  114. width: width,
  115. height: height,
  116. fit: fit ?? BoxFit.cover,
  117. errorBuilder: (context, error, stackTrace) =>
  118. _buildErrorWidget(context, source, error),
  119. );
  120. case ImageSourceType.memory:
  121. // 使用缓存避免重复解码
  122. Uint8List imageBytes;
  123. if (_memoryImageCache.containsKey(source)) {
  124. imageBytes = _memoryImageCache[source]!;
  125. } else {
  126. imageBytes = base64Decode(source);
  127. _memoryImageCache[source] = imageBytes;
  128. }
  129. return Image.memory(
  130. imageBytes,
  131. key: ValueKey('memory_${source.hashCode}'),
  132. width: width,
  133. height: height,
  134. fit: fit ?? BoxFit.cover,
  135. errorBuilder: (context, error, stackTrace) =>
  136. _buildErrorWidget(context, source, error),
  137. );
  138. }
  139. }
  140. Widget _buildErrorWidget(BuildContext context, String url, dynamic error) {
  141. return _buildSmoothPlaceholder(context, isError: false);
  142. }
  143. Widget _buildPlaceholder(BuildContext context, String url) {
  144. return _buildSmoothPlaceholder(context, isError: false);
  145. }
  146. Widget _buildSmoothPlaceholder(
  147. BuildContext context, {
  148. required bool isError,
  149. }) {
  150. return AnimatedContainer(
  151. duration: placeholderDuration,
  152. curve: Curves.easeInOut,
  153. width: width,
  154. height: height,
  155. decoration: BoxDecoration(
  156. gradient: LinearGradient(
  157. begin: Alignment.topLeft,
  158. end: Alignment.bottomRight,
  159. colors: isError
  160. ? [
  161. Colors.red.withValues(alpha: 0.15),
  162. Colors.red.withValues(alpha: 0.08),
  163. Colors.red.withValues(alpha: 0.15),
  164. ]
  165. : [
  166. Colors.grey.withValues(alpha: 0.25),
  167. Colors.grey.withValues(alpha: 0.08),
  168. Colors.grey.withValues(alpha: 0.25),
  169. ],
  170. stops: const [0.0, 0.5, 1.0],
  171. ),
  172. borderRadius: BorderRadius.circular(borderRadius ?? 0),
  173. ),
  174. child: Shimmer.fromColors(
  175. baseColor: isError
  176. ? Colors.red.withValues(alpha: 0.3)
  177. : Colors.grey.withValues(alpha: 0.4),
  178. highlightColor: isError
  179. ? Colors.red.withValues(alpha: 0.1)
  180. : Colors.grey.withValues(alpha: 0.1),
  181. period: const Duration(milliseconds: 1800),
  182. child: Container(
  183. width: width,
  184. height: height,
  185. decoration: BoxDecoration(
  186. color: Colors.white.withValues(alpha: 0.15),
  187. borderRadius: BorderRadius.circular(borderRadius ?? 0),
  188. ),
  189. ),
  190. ),
  191. );
  192. }
  193. }