QinGang_interested/lib/customWidget/remote_file_page.dart

263 lines
9.4 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<RemoteFilePage> {
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<double> _pageTopOffsets = []; // 每页顶部偏移(相对于内容起点)
double _totalContentHeight = 0; // 计算得到的滚动内容总高度
double _pageSpacing = 0.0; // 如果你在 PdfViewPinch 布局里有页间距,可设置
// ======================================
@override
void initState() {
super.initState();
_secondsRemaining = widget.countdownSeconds;
_startCountdown();
_downloadAndLoad();
LoadingDialogHelper.hide();
}
Future<void> _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<List<int>>(
url,
options: Options(responseType: ResponseType.bytes),
);
final file = File(filePath);
await file.writeAsBytes(response.data!);
// 直接传 Future<PdfDocument> 给 controller不要 await
final futureDoc = PdfDocument.openFile(filePath); // Future<PdfDocument>
_pdfController = PdfControllerPinch(document: futureDoc);
// 注册 pageListenable listenerpageListenable 是 ValueListenable<int>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<ScrollNotification>(
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,
),
),
],
),
),
);
}
}