attachment_preview_page.dart 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. import 'dart:typed_data';
  2. import 'package:flutter/material.dart';
  3. /// 图片预览弹窗,支持捏合缩放。
  4. ///
  5. /// 以半透明蒙版覆盖在当前页面上方,内部自动加载图片,
  6. /// 加载期间显示 loading 动画,加载失败显示错误提示。
  7. ///
  8. /// 使用方式:
  9. /// ```dart
  10. /// AttachmentPreview.show(context,
  11. /// loader: api.downloadAttachment(id),
  12. /// fileName: 'photo.jpg',
  13. /// loadingText: '加载中…',
  14. /// );
  15. /// ```
  16. class AttachmentPreview {
  17. AttachmentPreview._();
  18. /// 显示图片预览弹窗。
  19. static Future<void> show(
  20. BuildContext context, {
  21. required Future<Uint8List?> loader,
  22. required String fileName,
  23. required String loadingText,
  24. }) {
  25. return showGeneralDialog(
  26. context: context,
  27. barrierDismissible: true,
  28. barrierLabel: 'Close',
  29. barrierColor: Colors.black87,
  30. transitionDuration: const Duration(milliseconds: 250),
  31. pageBuilder: (context, animation, secondaryAnimation) {
  32. return FadeTransition(
  33. opacity: animation,
  34. child: Material(
  35. type: MaterialType.transparency,
  36. child: _PreviewContent(
  37. loader: loader,
  38. fileName: fileName,
  39. loadingText: loadingText,
  40. ),
  41. ),
  42. );
  43. },
  44. );
  45. }
  46. }
  47. class _PreviewContent extends StatefulWidget {
  48. final Future<Uint8List?> loader;
  49. final String fileName;
  50. final String loadingText;
  51. const _PreviewContent({
  52. required this.loader,
  53. required this.fileName,
  54. required this.loadingText,
  55. });
  56. @override
  57. State<_PreviewContent> createState() => _PreviewContentState();
  58. }
  59. class _PreviewContentState extends State<_PreviewContent> {
  60. Uint8List? _bytes;
  61. bool _loading = true;
  62. String? _error;
  63. @override
  64. void initState() {
  65. super.initState();
  66. _load();
  67. }
  68. Future<void> _load() async {
  69. try {
  70. final bytes = await widget.loader;
  71. if (!mounted) return;
  72. if (bytes == null) {
  73. setState(() { _error = 'Download failed'; _loading = false; });
  74. return;
  75. }
  76. setState(() { _bytes = bytes; _loading = false; });
  77. } catch (_) {
  78. if (!mounted) return;
  79. setState(() { _error = 'Open failed'; _loading = false; });
  80. }
  81. }
  82. @override
  83. Widget build(BuildContext context) {
  84. return SafeArea(
  85. child: Stack(
  86. children: [
  87. Center(child: _buildBody()),
  88. Positioned(
  89. top: 8,
  90. right: 8,
  91. child: IconButton(
  92. icon: const Icon(Icons.close, color: Colors.white70, size: 28),
  93. onPressed: () => Navigator.of(context).pop(),
  94. ),
  95. ),
  96. Positioned(
  97. top: 16,
  98. left: 16,
  99. right: 56,
  100. child: Text(
  101. widget.fileName,
  102. style: const TextStyle(color: Colors.white, fontSize: 15),
  103. overflow: TextOverflow.ellipsis,
  104. ),
  105. ),
  106. ],
  107. ),
  108. );
  109. }
  110. Widget _buildBody() {
  111. if (_loading) {
  112. return Column(
  113. mainAxisSize: MainAxisSize.min,
  114. children: [
  115. const CircularProgressIndicator(color: Colors.white),
  116. const SizedBox(height: 16),
  117. Text(widget.loadingText,
  118. style: const TextStyle(color: Colors.white70, fontSize: 14)),
  119. ],
  120. );
  121. }
  122. if (_error != null || _bytes == null) {
  123. return Column(
  124. mainAxisSize: MainAxisSize.min,
  125. children: [
  126. const Icon(Icons.broken_image_outlined, color: Colors.white54, size: 48),
  127. const SizedBox(height: 12),
  128. Text(_error ?? 'Unknown error',
  129. style: const TextStyle(color: Colors.white70, fontSize: 14)),
  130. ],
  131. );
  132. }
  133. return InteractiveViewer(
  134. minScale: 0.5,
  135. maxScale: 4.0,
  136. child: Image.memory(_bytes!),
  137. );
  138. }
  139. }