Flutter中PDF保存到下载文件夹前如何设置标题并实现指定页面
Flutter 两项功能实现方案
功能1:保存PDF到下载文件夹前设置自定义标题
前置依赖
在pubspec.yaml中添加以下依赖:
dependencies: path_provider: ^2.1.1 permission_handler: ^11.0.1 device_info_plus: ^9.1.1
平台权限配置
- Android:在
android/app/src/main/AndroidManifest.xml中添加权限声明
Android 13及以上版本使用分区存储,无需额外申请存储权限即可写入公共下载目录。<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> - iOS:在
ios/Runner/Info.plist中添加配置<key>LSSupportsOpeningDocumentsInPlace</key> <true/> <key>UIFileSharingEnabled</key> <true/>
核心实现代码
import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; // 获取Android SDK版本 Future<int> _getAndroidSdkInt() async { if (Platform.isAndroid) { final androidInfo = await DeviceInfoPlugin().androidInfo; return androidInfo.version.sdkInt; } return 0; } // 带自定义标题保存PDF到下载文件夹 Future<File> savePdfWithCustomTitle(List<int> pdfBytes, String customTitle) async { // 低版本Android申请存储权限 if (Platform.isAndroid) { final sdkInt = await _getAndroidSdkInt(); if (sdkInt < 33) { final status = await Permission.storage.request(); if (!status.isGranted) throw Exception('存储权限未授予'); } } // 获取公共下载目录路径 Directory? downloadDir; if (Platform.isAndroid) { downloadDir = Directory('/storage/emulated/0/Download'); } else { downloadDir = await getDownloadsDirectory(); } if (downloadDir == null || !await downloadDir.exists()) throw Exception('下载目录获取失败'); // 处理文件名,过滤非法字符、自动处理重名 final safeTitle = customTitle.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_'); String targetPath = '${downloadDir.path}/$safeTitle.pdf'; int fileIndex = 1; while (await File(targetPath).exists()) { targetPath = '${downloadDir.path}/${safeTitle}_($fileIndex).pdf'; fileIndex++; } // 写入文件 final savedFile = await File(targetPath).writeAsBytes(pdfBytes); return savedFile; }
调用时传入PDF字节流和自定义标题即可,自动处理重名、非法字符问题,保存后返回对应的文件实例。
功能2:参考截图页面实现

该页面为PDF预览操作页,整体分为顶部导航栏、中间PDF预览区、底部固定操作栏三部分。
前置依赖
在pubspec.yaml中添加以下依赖:
dependencies: flutter_pdfview: ^1.3.2 share_plus: ^7.2.1
核心实现代码
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_pdfview/flutter_pdfview.dart'; import 'package:share_plus/share_plus.dart'; class PdfPreviewPage extends StatefulWidget { final String localPdfPath; const PdfPreviewPage({super.key, required this.localPdfPath}); @override State<PdfPreviewPage> createState() => _PdfPreviewPageState(); } class _PdfPreviewPageState extends State<PdfPreviewPage> { late PDFViewController _pdfController; int _currentPage = 1; int _totalPages = 0; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey.shade100, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black, size: 22), onPressed: () => Navigator.pop(context), ), title: const Text('文档预览', style: TextStyle(color: Colors.black, fontSize: 18, fontWeight: FontWeight.w500)), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.share, color: Colors.black, size: 22), onPressed: () { Share.shareXFiles([XFile(widget.localPdfPath)]); }, ), IconButton( icon: const Icon(Icons.more_vert, color: Colors.black, size: 22), onPressed: () { // 可扩展更多操作:打印、收藏等 }, ), ], ), body: Stack( children: [ // PDF预览区域 PDFView( filePath: widget.localPdfPath, enableSwipe: true, swipeHorizontal: false, autoSpacing: true, pageFling: true, defaultPage: _currentPage - 1, onRender: (pages) { setState(() { _totalPages = pages ?? 0; }); }, onViewCreated: (controller) { _pdfController = controller; }, onPageChanged: (page, total) { setState(() { _currentPage = (page ?? 0) + 1; }); }, ), // 底部操作栏 Positioned( left: 0, right: 0, bottom: 0, child: Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 8, offset: const Offset(0, -2)) ] ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton( iconSize: 24, icon: Icon(Icons.chevron_left, color: _currentPage > 1 ? Colors.black87 : Colors.grey.shade400), onPressed: _currentPage > 1 ? () { _pdfController.setPage(_currentPage - 2); } : null, ), GestureDetector( onTap: _showJumpPageDialog, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(20) ), child: Text('$_currentPage / $_totalPages', style: const TextStyle(fontSize: 14, color: Colors.black87)), ), ), IconButton( iconSize: 24, icon: Icon(Icons.chevron_right, color: _currentPage < _totalPages ? Colors.black87 : Colors.grey.shade400), onPressed: _currentPage < _totalPages ? () { _pdfController.setPage(_currentPage); } : null, ), const SizedBox(width: 24), IconButton( iconSize: 24, icon: const Icon(Icons.zoom_out, color: Colors.black87), onPressed: () { // 可通过InteractiveViewer包裹PDFView实现缩放控制 }, ), IconButton( iconSize: 24, icon: const Icon(Icons.zoom_in, color: Colors.black87), onPressed: () { // 可通过InteractiveViewer包裹PDFView实现缩放控制 }, ), IconButton( iconSize: 24, icon: const Icon(Icons.download_outlined, color: Colors.black87), onPressed: _showSaveDialog, ), ], ), ), ) ], ), ); } // 页码跳转弹窗 void _showJumpPageDialog() { final inputController = TextEditingController(); showDialog(context: context, builder: (context) { return AlertDialog( title: const Text('跳转页码', style: TextStyle(fontSize: 16)), content: TextField( controller: inputController, keyboardType: TextInputType.number, decoration: InputDecoration( hintText: '请输入1-$_totalPages之间的页码', border: const OutlineInputBorder() ), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')), TextButton(onPressed: () { final targetPage = int.tryParse(inputController.text.trim()); if (targetPage != null && targetPage >=1 && targetPage <= _totalPages) { _pdfController.setPage(targetPage -1); Navigator.pop(context); } }, child: const Text('确定')), ], ); }); } // 保存弹窗,输入自定义标题 void _showSaveDialog() { final titleController = TextEditingController(); showDialog(context: context, builder: (context) { return AlertDialog( title: const Text('保存文档', style: TextStyle(fontSize: 16)), content: TextField( controller: titleController, decoration: const InputDecoration( hintText: '请输入文档名称', border: OutlineInputBorder() ), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('取消')), TextButton(onPressed: () async { final customTitle = titleController.text.trim(); if (customTitle.isEmpty) return; final pdfBytes = await File(widget.localPdfPath).readAsBytes(); await savePdfWithCustomTitle(pdfBytes, customTitle); if (mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('文件已保存至下载文件夹'))); } }, child: const Text('保存')), ], ); }); } }
适配说明
- 缩放功能可直接将
PDFView组件包裹在InteractiveViewer中实现,无需额外调用原生API,支持双指缩放、按钮控制缩放倍数 - 调整图标大小、颜色、边距、字号即可与参考截图达到1:1视觉匹配
- 更多操作菜单可根据业务需求扩展打印、重命名、删除等功能
内容的提问来源于stack exchange,提问作者Muhammadrizo Rafiqjonov
相关产品推荐
相关产品推荐

