import 'dart:typed_data'; import 'package:flutter/material.dart'; /// 图片预览弹窗,支持捏合缩放。 /// /// 以半透明蒙版覆盖在当前页面上方,内部自动加载图片, /// 加载期间显示 loading 动画,加载失败显示错误提示。 /// /// 使用方式: /// ```dart /// AttachmentPreview.show(context, /// loader: api.downloadAttachment(id), /// fileName: 'photo.jpg', /// loadingText: '加载中…', /// ); /// ``` class AttachmentPreview { AttachmentPreview._(); /// 显示图片预览弹窗。 static Future show( BuildContext context, { required Future loader, required String fileName, required String loadingText, }) { return showGeneralDialog( context: context, barrierDismissible: true, barrierLabel: 'Close', barrierColor: Colors.black87, transitionDuration: const Duration(milliseconds: 250), pageBuilder: (context, animation, secondaryAnimation) { return FadeTransition( opacity: animation, child: Material( type: MaterialType.transparency, child: _PreviewContent( loader: loader, fileName: fileName, loadingText: loadingText, ), ), ); }, ); } } class _PreviewContent extends StatefulWidget { final Future loader; final String fileName; final String loadingText; const _PreviewContent({ required this.loader, required this.fileName, required this.loadingText, }); @override State<_PreviewContent> createState() => _PreviewContentState(); } class _PreviewContentState extends State<_PreviewContent> { Uint8List? _bytes; bool _loading = true; String? _error; @override void initState() { super.initState(); _load(); } Future _load() async { try { final bytes = await widget.loader; if (!mounted) return; if (bytes == null) { setState(() { _error = 'Download failed'; _loading = false; }); return; } setState(() { _bytes = bytes; _loading = false; }); } catch (_) { if (!mounted) return; setState(() { _error = 'Open failed'; _loading = false; }); } } @override Widget build(BuildContext context) { return SafeArea( child: Stack( children: [ Center(child: _buildBody()), Positioned( top: 8, right: 8, child: IconButton( icon: const Icon(Icons.close, color: Colors.white70, size: 28), onPressed: () => Navigator.of(context).pop(), ), ), Positioned( top: 16, left: 16, right: 56, child: Text( widget.fileName, style: const TextStyle(color: Colors.white, fontSize: 15), overflow: TextOverflow.ellipsis, ), ), ], ), ); } Widget _buildBody() { if (_loading) { return Column( mainAxisSize: MainAxisSize.min, children: [ const CircularProgressIndicator(color: Colors.white), const SizedBox(height: 16), Text(widget.loadingText, style: const TextStyle(color: Colors.white70, fontSize: 14)), ], ); } if (_error != null || _bytes == null) { return Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.broken_image_outlined, color: Colors.white54, size: 48), const SizedBox(height: 12), Text(_error ?? 'Unknown error', style: const TextStyle(color: Colors.white70, fontSize: 14)), ], ); } return InteractiveViewer( minScale: 0.5, maxScale: 4.0, child: Image.memory(_bytes!), ); } }