country_icon.dart 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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. String? _imagePath;
  36. /// 图片类型:svg 或 png
  37. String? _imageType;
  38. /// 是否正在加载
  39. bool _isLoading = true;
  40. @override
  41. void initState() {
  42. super.initState();
  43. _findImage();
  44. }
  45. @override
  46. void didUpdateWidget(CountryIcon oldWidget) {
  47. super.didUpdateWidget(oldWidget);
  48. if (oldWidget.countryCode != widget.countryCode) {
  49. _findImage();
  50. }
  51. }
  52. /// 智能查找图片
  53. Future<void> _findImage() async {
  54. if (!mounted) return;
  55. setState(() {
  56. _isLoading = true;
  57. });
  58. final code = widget.countryCode.toLowerCase().trim();
  59. // 如果为空,直接使用默认图标
  60. if (code.isEmpty) {
  61. setState(() {
  62. _imagePath = 'assets/flags/xx.svg';
  63. _imageType = 'svg';
  64. _isLoading = false;
  65. });
  66. return;
  67. }
  68. // 1. 先检查 flags 目录的 svg
  69. final flagPath = 'assets/flags/$code.svg';
  70. if (await _assetExists(flagPath)) {
  71. setState(() {
  72. _imagePath = flagPath;
  73. _imageType = 'svg';
  74. _isLoading = false;
  75. });
  76. return;
  77. }
  78. // 2. 检查 streaming 目录的 png
  79. final streamingPath = 'assets/images/streaming/$code.png';
  80. if (await _assetExists(streamingPath)) {
  81. setState(() {
  82. _imagePath = streamingPath;
  83. _imageType = 'png';
  84. _isLoading = false;
  85. });
  86. return;
  87. }
  88. // 3. 使用默认图标
  89. setState(() {
  90. _imagePath = 'assets/flags/xx.svg';
  91. _imageType = 'svg';
  92. _isLoading = false;
  93. });
  94. }
  95. /// 检查资源文件是否存在
  96. Future<bool> _assetExists(String path) async {
  97. try {
  98. await rootBundle.load(path);
  99. return true;
  100. } catch (e) {
  101. return false;
  102. }
  103. }
  104. @override
  105. Widget build(BuildContext context) {
  106. // 加载中显示占位符
  107. if (_isLoading || _imagePath == null || _imageType == null) {
  108. return _buildPlaceholder();
  109. }
  110. // 根据图片类型返回不同的组件
  111. if (_imageType == 'svg') {
  112. return ClipRRect(
  113. borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
  114. child: SvgPicture.asset(
  115. _imagePath!,
  116. width: widget.width,
  117. height: widget.height,
  118. fit: widget.fit ?? BoxFit.cover,
  119. placeholderBuilder: (context) => _buildPlaceholder(),
  120. ),
  121. );
  122. } else {
  123. // png 类型使用 IXImage
  124. return IXImage(
  125. source: _imagePath!,
  126. sourceType: ImageSourceType.asset,
  127. width: widget.width,
  128. height: widget.height,
  129. borderRadius: widget.borderRadius,
  130. fit: widget.fit,
  131. );
  132. }
  133. }
  134. /// 构建占位符
  135. Widget _buildPlaceholder() {
  136. return Container(
  137. width: widget.width,
  138. height: widget.height,
  139. decoration: BoxDecoration(
  140. color: Colors.grey.withValues(alpha: 0.2),
  141. borderRadius: BorderRadius.circular(widget.borderRadius ?? 0),
  142. ),
  143. );
  144. }
  145. }
  146. /// 国旗/图标组件的简化版本(同步加载,不检查文件是否存在)
  147. ///
  148. /// 性能更好,但可能在资源不存在时显示错误
  149. class CountryIconFast extends StatelessWidget {
  150. /// 国家/地区代码
  151. final String countryCode;
  152. /// 宽度
  153. final double width;
  154. /// 高度
  155. final double height;
  156. /// 圆角
  157. final double? borderRadius;
  158. /// 适配方式
  159. final BoxFit? fit;
  160. /// 是否优先使用 streaming 目录
  161. final bool preferStreaming;
  162. const CountryIconFast({
  163. Key? key,
  164. required this.countryCode,
  165. required this.width,
  166. required this.height,
  167. this.borderRadius,
  168. this.fit,
  169. this.preferStreaming = false,
  170. }) : super(key: key);
  171. @override
  172. Widget build(BuildContext context) {
  173. final code = countryCode.toLowerCase().trim();
  174. if (code.isEmpty) {
  175. return _buildSvg('assets/flags/xx.svg');
  176. }
  177. // 如果优先使用 streaming
  178. if (preferStreaming) {
  179. return _buildPng('assets/images/streaming/$code.png');
  180. }
  181. // 默认优先使用 flags
  182. return _buildSvg('assets/flags/$code.svg');
  183. }
  184. Widget _buildSvg(String path) {
  185. return ClipRRect(
  186. borderRadius: BorderRadius.circular(borderRadius ?? 0),
  187. child: SvgPicture.asset(
  188. path,
  189. width: width,
  190. height: height,
  191. fit: fit ?? BoxFit.cover,
  192. placeholderBuilder: (context) => _buildFallback(),
  193. ),
  194. );
  195. }
  196. Widget _buildPng(String path) {
  197. return IXImage(
  198. source: path,
  199. sourceType: ImageSourceType.asset,
  200. width: width,
  201. height: height,
  202. borderRadius: borderRadius,
  203. fit: fit,
  204. );
  205. }
  206. Widget _buildFallback() {
  207. // 降级到默认图标
  208. return SvgPicture.asset(
  209. 'assets/flags/xx.svg',
  210. width: width,
  211. height: height,
  212. fit: fit ?? BoxFit.cover,
  213. );
  214. }
  215. }