You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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中添加权限声明
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
    
    Android 13及以上版本使用分区存储,无需额外申请存储权限即可写入公共下载目录。
  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 08:06:22