country_icon.dart 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_svg/flutter_svg.dart';
  4. import 'ix_image.dart';
  5. /// 国旗/图标组件
  6. ///
  7. /// 智能查找逻辑:
  8. /// 1. 优先从 assets/flags/{countryCode}.svg 查找
  9. /// 2. 找不到则从 assets/images/streaming/{countryCode}.png 查找
  10. /// 3. 都找不到则显示默认图标 assets/flags/xx.svg
  11. class CountryIcon extends StatefulWidget {
  12. /// 国家/地区代码(如: us, cn, netflix 等)
  13. final String countryCode;
  14. /// 宽度
  15. final double width;
  16. /// 高度
  17. final double height;
  18. /// 圆角
  19. final double? borderRadius;
  20. /// 适配方式
  21. final BoxFit? fit;
  22. const CountryIcon({
  23. super.key,
  24. required this.countryCode,
  25. required this.width,
  26. required this.height,
  27. this.borderRadius,
  28. this.fit,
  29. });
  30. @override
  31. State<CountryIcon> createState() => _CountryIconState();
  32. }
  33. class _CountryIconState extends State<CountryIcon> {
  34. /// 静态缓存:存储已查找过的图片路径,避免重复异步检查
  35. static final Map<String, _ImageInfo> _imageCache = {};
  36. /// 图片路径
  37. String? _imagePath;
  38. /// 图片类型:svg 或 png
  39. String? _imageType;
  40. /// 是否首次加载(仅首次加载时显示占位符)
  41. bool _isFirstLoad = true;
  42. @override
  43. void initState() {
  44. super.initState();
  45. _findImage();
  46. }
  47. @override
  48. void didUpdateWidget(CountryIcon oldWidget) {
  49. super.didUpdateWidget(oldWidget);
  50. if (oldWidget.countryCode != widget.countryCode) {
  51. _findImage();
  52. }
  53. }
  54. /// 智能查找图片
  55. Future<void> _findImage() async {
  56. if (!mounted) return;
  57. final code = widget.countryCode.toLowerCase().trim();
  58. // 如果为空,直接使用默认图标
  59. if (code.isEmpty) {
  60. _updateImage('assets/flags/xx.svg', 'svg');
  61. return;
  62. }
  63. // 检查缓存,如果有缓存则直接使用,避免异步等待
  64. if (_imageCache.containsKey(code)) {
  65. final cached = _imageCache[code]!;
  66. _updateImage(cached.path, cached.type);
  67. return;
  68. }
  69. // 异步查找图片(不设置 loading 状态,保持旧图片显示)
  70. String? foundPath;
  71. String? foundType;
  72. // 1. 先检查 flags 目录的 svg
  73. final flagPath = 'assets/flags/$code.svg';
  74. if (await _assetExists(flagPath)) {
  75. foundPath = flagPath;
  76. foundType = 'svg';
  77. } else {
  78. // 2. 检查 streaming 目录的 png
  79. final streamingPath = 'assets/images/streaming/$code.png';
  80. if (await _assetExists(streamingPath)) {
  81. foundPath = streamingPath;
  82. foundType = 'png';
  83. } else {
  84. // 3. 使用默认图标
  85. foundPath = 'assets/flags/xx.svg';
  86. foundType = 'svg';
  87. }
  88. }
  89. // 缓存结果
  90. _imageCache[code] = _ImageInfo(foundPath, foundType);
  91. // 检查 widget 是否仍然需要这个 code 的图片(防止快速切换时的竞态)
  92. if (!mounted || widget.countryCode.toLowerCase().trim() != code) return;
  93. _updateImage(foundPath, foundType);
  94. }
  95. /// 更新图片状态
  96. void _updateImage(String path, String type) {
  97. if (!mounted) return;
  98. setState(() {
  99. _imagePath = path;
  100. _imageType = type;
  101. _isFirstLoad = false;
  102. });
  103. }
  104. /// 检查资源文件是否存在
  105. Future<bool> _assetExists(String path) async {
  106. try {
  107. await rootBundle.load(path);
  108. return true;
  109. } catch (e) {
  110. return false;
  111. }
  112. }
  113. @override
  114. Widget build(BuildContext context) {
  115. // 仅首次加载时显示占位符,切换时保持旧图片
  116. if (_isFirstLoad || _imagePath == null || _imageType == null) {
  117. return _buildPlaceholder();
  118. }
  119. // 根据图片类型返回不同的组件
  120. if (_imageType == 'svg') {
  121. return ClipRRect(
  122. borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
  123. child: SvgPicture.asset(
  124. _imagePath!,
  125. width: widget.width,
  126. height: widget.height,
  127. fit: widget.fit ?? BoxFit.cover,
  128. placeholderBuilder: (context) => _buildPlaceholder(),
  129. ),
  130. );
  131. } else {
  132. // png 类型使用 IXImage
  133. return IXImage(
  134. source: _imagePath!,
  135. sourceType: ImageSourceType.asset,
  136. width: widget.width,
  137. height: widget.height,
  138. borderRadius: widget.borderRadius,
  139. fit: widget.fit,
  140. );
  141. }
  142. }
  143. /// 构建占位符
  144. Widget _buildPlaceholder() {
  145. return Container(
  146. width: widget.width,
  147. height: widget.height,
  148. decoration: BoxDecoration(
  149. color: Colors.grey.withValues(alpha: 0.2),
  150. borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
  151. ),
  152. );
  153. }
  154. }
  155. /// 国旗/图标组件的简化版本(同步加载,不检查文件是否存在)
  156. ///
  157. /// 性能更好,但可能在资源不存在时显示错误
  158. class CountryIconFast extends StatelessWidget {
  159. /// 国家/地区代码
  160. final String countryCode;
  161. /// 宽度
  162. final double width;
  163. /// 高度
  164. final double height;
  165. /// 圆角
  166. final double? borderRadius;
  167. /// 适配方式
  168. final BoxFit? fit;
  169. /// 是否优先使用 streaming 目录
  170. final bool preferStreaming;
  171. const CountryIconFast({
  172. super.key,
  173. required this.countryCode,
  174. required this.width,
  175. required this.height,
  176. this.borderRadius,
  177. this.fit,
  178. this.preferStreaming = false,
  179. });
  180. @override
  181. Widget build(BuildContext context) {
  182. final code = countryCode.toLowerCase().trim();
  183. if (code.isEmpty) {
  184. return _buildSvg('assets/flags/xx.svg');
  185. }
  186. // 如果优先使用 streaming
  187. if (preferStreaming) {
  188. return _buildPng('assets/images/streaming/$code.png');
  189. }
  190. // 默认优先使用 flags
  191. return _buildSvg('assets/flags/$code.svg');
  192. }
  193. Widget _buildSvg(String path) {
  194. return ClipRRect(
  195. borderRadius: BorderRadius.circular(borderRadius ?? 0),
  196. child: SvgPicture.asset(
  197. path,
  198. width: width,
  199. height: height,
  200. fit: fit ?? BoxFit.cover,
  201. placeholderBuilder: (context) => _buildFallback(),
  202. ),
  203. );
  204. }
  205. Widget _buildPng(String path) {
  206. return IXImage(
  207. source: path,
  208. sourceType: ImageSourceType.asset,
  209. width: width,
  210. height: height,
  211. borderRadius: borderRadius,
  212. fit: fit,
  213. );
  214. }
  215. Widget _buildFallback() {
  216. // 降级到默认图标
  217. return SvgPicture.asset(
  218. 'assets/flags/xx.svg',
  219. width: width,
  220. height: height,
  221. fit: fit ?? BoxFit.cover,
  222. );
  223. }
  224. }
  225. /// 图片信息缓存类
  226. class _ImageInfo {
  227. final String path;
  228. final String type;
  229. _ImageInfo(this.path, this.type);
  230. }