import 'dart:async'; import 'dart:io'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:pdfx/pdfx.dart'; import 'package:path_provider/path_provider.dart'; import 'package:qhd_prevention/customWidget/toast_util.dart'; import 'package:qhd_prevention/pages/my_appbar.dart'; import 'package:qhd_prevention/customWidget/custom_button.dart'; import 'package:dio/dio.dart'; import 'package:qhd_prevention/tools/tools.dart'; class RemoteFilePage extends StatefulWidget { final String fileUrl; final int countdownSeconds; const RemoteFilePage({ Key? key, required this.fileUrl, this.countdownSeconds = 3, }) : super(key: key); @override _RemoteFilePageState createState() => _RemoteFilePageState(); } class _RemoteFilePageState extends State { String? _localPath; bool _isLoading = true; bool _hasScrolledToBottom = false; bool _timerFinished = false; late int _secondsRemaining; Timer? _countdownTimer; PdfControllerPinch? _pdfController; int _totalPages = 0; // ========== 新增用于方案1的字段 ========== Size? _viewportSize; // 当前 viewport 大小,用于计算 scale final List _pageTopOffsets = []; // 每页顶部偏移(相对于内容起点) double _totalContentHeight = 0; // 计算得到的滚动内容总高度 double _pageSpacing = 0.0; // 如果你在 PdfViewPinch 布局里有页间距,可设置 // ====================================== @override void initState() { super.initState(); _secondsRemaining = widget.countdownSeconds; _startCountdown(); _downloadAndLoad(); LoadingDialogHelper.hide(); } Future _downloadAndLoad() async { try { final url = widget.fileUrl; final filename = url.split('/').last; final dir = await getTemporaryDirectory(); final filePath = '${dir.path}/$filename'; final dio = Dio(); final response = await dio.get>( url, options: Options(responseType: ResponseType.bytes), ); final file = File(filePath); await file.writeAsBytes(response.data!); // 直接传 Future 给 controller(不要 await) final futureDoc = PdfDocument.openFile(filePath); // Future _pdfController = PdfControllerPinch(document: futureDoc); // 注册 pageListenable listener(pageListenable 是 ValueListenable,1-based) _pdfController!.pageListenable.addListener(_onPageListenableChanged); if (!mounted) return; setState(() { _localPath = filePath; _isLoading = false; }); } catch (e) { if (mounted) { setState(() { _isLoading = false; }); ToastUtil.showNormal(context, '文件加载失败: $e'); } } } void _onPageListenableChanged() { // pageListenable.value 是当前页(1-based) final controller = _pdfController; if (controller == null) return; final currentPage = controller.pageListenable.value; // 如果 totalPages 已知且到达最后一页,则标记为已看完 if (_totalPages > 0 && currentPage >= _totalPages - 3) { if (!_hasScrolledToBottom && mounted) { setState(() { _hasScrolledToBottom = true; }); } } } void _startCountdown() { _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (!mounted) return; setState(() { if (_secondsRemaining > 1) { _secondsRemaining--; } else { _secondsRemaining = 0; _timerFinished = true; _countdownTimer?.cancel(); } }); }); } @override void dispose() { _countdownTimer?.cancel(); // 移除 listener 再释放 controller(注意 controller 和其 listenable 可能为 null) try { if (_pdfController != null) { _pdfController!.pageListenable.removeListener(_onPageListenableChanged); _pdfController!.dispose(); } } catch (e) { // ignore dispose errors } super.dispose(); } // 当滚动通知到达底部(或接近底部)时调用 — 作为备选方案/调试用 bool _handleScrollNotification(ScrollNotification notification) { final metrics = notification.metrics; // 优先使用我们计算的 totalContentHeight + viewport 判断(更准确) if (_totalContentHeight > 0 && _viewportSize != null) { final viewportH = _viewportSize!.height; final threshold = math.min(50.0, viewportH * 0.1); // 灵活阈值 final scrolledToBottom = (metrics.pixels + viewportH) >= (_totalContentHeight - threshold); if (scrolledToBottom) { if (!_hasScrolledToBottom && mounted) { setState(() { _hasScrolledToBottom = true; }); } } } else { // 兜底:使用 maxScrollExtent 判断(保留你原来的逻辑) if ((metrics.maxScrollExtent - metrics.pixels) <= 5.0 || (metrics.atEdge && metrics.pixels == metrics.maxScrollExtent)) { if (!_hasScrolledToBottom && mounted) { setState(() { _hasScrolledToBottom = true; }); } } } return false; // 让事件继续传递 } @override Widget build(BuildContext context) { final isButtonEnabled = _timerFinished && _hasScrolledToBottom; return Scaffold( appBar: MyAppbar(title: '资料学习'), backgroundColor: Colors.white, body: SafeArea( child: Column( children: [ Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : LayoutBuilder(builder: (context, constraints) { // 保存 viewport size(用于计算 scale) _viewportSize = Size(constraints.maxWidth, constraints.maxHeight); return NotificationListener( onNotification: _handleScrollNotification, child: PdfViewPinch( controller: _pdfController!, scrollDirection: Axis.vertical, onDocumentLoaded: (document) async { // 原有行为:保存页数 _totalPages = document.pagesCount; // ======= 新增:计算每页渲染高度并累加 totalContentHeight ======= // 注意:document.getPage(i) 后要 close() try { final viewportW = _viewportSize?.width ?? MediaQuery.of(context).size.width; final viewportH = _viewportSize?.height ?? MediaQuery.of(context).size.height; _pageTopOffsets.clear(); double acc = 0; for (int i = 1; i <= _totalPages; i++) { final page = await document.getPage(i); // page.width / page.height 可能是 num/double final pw = (page.width is num) ? (page.width as num).toDouble() : page.width.toDouble(); final ph = (page.height is num) ? (page.height as num).toDouble() : page.height.toDouble(); // 模拟 contained 缩放行为:scale = min(viewportW / pw, viewportH / ph) final scale = math.min(viewportW / pw, viewportH / ph); final renderedH = ph * scale; _pageTopOffsets.add(acc); acc += renderedH + _pageSpacing; await page.close(); } _totalContentHeight = acc; } catch (e) { // 计算失败则保留 _totalContentHeight 为 0,使用兜底判断 _totalContentHeight = 0; } // ============================================================== // 保留你原来:如果页少,直接视为已看完 if (_totalPages <= 3) { if (!_hasScrolledToBottom && mounted) { setState(() => _hasScrolledToBottom = true); } } if (mounted) setState(() {}); }, // 作为兜底:当 page 到最后一页时也标记为已看完(注意 page 是 1-based) onPageChanged: (page) { if (_totalPages > 0 && page == _totalPages) { if (!_hasScrolledToBottom && mounted) { setState(() => _hasScrolledToBottom = true); } } }, ), ); }), ), Padding( padding: const EdgeInsets.all(16), child: CustomButton( backgroundColor: isButtonEnabled ? Colors.blue : Colors.grey, text: isButtonEnabled ? '我已学习完毕' : _secondsRemaining == 0 ? '我已学习完毕' : '($_secondsRemaining s)我已学习完毕', onPressed: isButtonEnabled ? () { Navigator.pop(context, true); } : null, ), ), ], ), ), ); } }