定位:轻量、简洁、纯本地离线处理、无任何网络上传依赖的 Android 原生 PDF 合并工具。
核心目标:帮助用户快速选取手机中的多个 PDF 文件,自由调整排序,在本地合并为一个全新的 PDF 并保存到手机。
在日常学术研究、职场办公和合同签署中,合并 PDF 是极其高频的需求。但市面上多数工具强制要求用户将私密文档上传至远程服务器,存在严重的信息泄漏隐患与网络延迟。
PdfMerger 遵循以下设计准则:
- 纯本地极速处理:所有 PDF 拼接与页面流式解析完全在手机设备本地完成,不包含任何网络请求权限;
- 原生 SAF 文件选择与保存:使用 Android 标准 Storage Access Framework,支持多选、不锁定固定路径、用户自主决定保存位置;
- 拖拽与自由重排:直观展示每个 PDF 的文件名、页数和文件大小,支持长按或方向键快速重排顺序;
- 大文件低内存占用:通过流式临时文件缓冲(Disk-backed MemoryBuffer),杜绝几十兆或上百兆 PDF 导致 OOM(内存溢出)崩溃;
- 极简无广告体验:无登录、无广告、无云同步、无冗余花哨功能,打开即用。
| 模块 / 需求 | 选用技术方案 | 说明 |
|---|---|---|
| 编程语言 | Kotlin 2.0.0 | 现代简洁语法与协程生态 |
| UI 框架 | Jetpack Compose (BOM 2024.06.00) | 声明式响应式界面与 Material Design 3 风格 |
| 架构模式 | MVVM + StateFlow | PdfMergeViewModel 统一管理状态与任务生命周期 |
| PDF 核心引擎 | PdfBox-Android 2.0.27.0 | Apache 官方 PDFBox 的 Android 成熟移植版本,稳定支持 PDF 结构拼接 |
| 存储访问 | Android SAF (Storage Access Framework) | OpenMultipleDocuments 多选,CreateDocument 保存位置指定 |
| 异步调度 | Kotlin Coroutines + Dispatchers.IO |
将耗时 I/O 与文件读写置于后台线程,保持 UI 流畅不掉帧 |
| 系统兼容 | MinSdk 26 (Android 8.0) ~ TargetSdk 35 (Android 15) | 覆盖 95%+ 真实 Android 手机与平板 |
PdfMerger/
├── build.gradle.kts // 根工程依赖管理 (AGP 8.5.0, Kotlin 2.0.0)
├── settings.gradle.kts // 工程与仓库源配置
├── gradle.properties // JVM 编译内存与 AndroidX 配置
├── gradlew / gradlew.bat // Gradle Wrapper 跨平台脚本
├── gradle/wrapper/ // Wrapper 发行包
├── README.md // 项目详尽文档
└── app/
├── build.gradle.kts // App 模块配置与 PdfBox-Android 依赖
├── proguard-rules.pro // 混淆防劣化规则
└── src/
├── main/
│ ├── AndroidManifest.xml // 应用程序清单(无联网权限,保护隐私)
│ ├── res/
│ │ ├── values/
│ │ │ ├── strings.xml // 中文文案与国际化
│ │ │ ├── colors.xml // 调色板
│ │ │ └── themes.xml // 沉浸式全屏主题
│ │ ├── drawable/ // 图标资源
│ │ └── mipmap-anydpi-v26/ // 自适应启动图标
│ └── java/com/pdfmerger/app/
│ ├── PdfMergerApplication.kt // 全局初始化 PDFBoxResourceLoader
│ ├── MainActivity.kt // 入口 Activity,注入 Compose 主题
│ ├── model/
│ │ └── PdfItem.kt // PDF 条目实体(文件名、页数、大小、状态)
│ ├── pdf/
│ │ └── PdfMergeManager.kt // 本地 PDF 流式合并核心引擎
│ ├── utils/
│ │ ├── FileUtils.kt // SAF Uri 元数据解析、页数探测、格式化
│ │ └── IntentUtils.kt // 调用系统阅读器打开与 ACTION_SEND 分享
│ ├── model/
│ │ ├── PdfItem.kt // PDF 条目实体(文件名、页数、大小、状态)
│ │ └── MergeHistoryItem.kt // 合并历史实体与源文件关联
│ ├── pdf/
│ │ ├── PdfMergeManager.kt // 本地 PDF 流式合并核心引擎
│ │ ├── HistoryManager.kt // 历史记录本地文件持久化存储
│ │ └── FolderManager.kt // SAF 目录授权、持久权限与 PDF 递归扫描
│ ├── utils/
│ │ ├── FileUtils.kt // SAF Uri 元数据解析、页数探测、格式化
│ │ └── IntentUtils.kt // 调用系统阅读器打开与 ACTION_SEND 分享
│ ├── viewmodel/
│ │ ├── PdfMergeViewModel.kt // 合并状态机(选择、排序、合并进度、异常)
│ │ └── PdfLibraryViewModel.kt// 目录授权、文件扫描、实时搜索、排序、多选
│ └── ui/
│ ├── MainScreen.kt // 三栏底部 NavigationBar 导航框架
│ ├── MergePdfScreen.kt // 第一页:合并(空状态引导、已选列表、标准TopAppBar、合并操作栏)
│ ├── PdfLibraryScreen.kt // 第二页:PDF文件库(授权目录、搜索过滤、多选加入合并)
│ ├── HistoryScreen.kt // 第三页:合并历史(按天聚合、打开、分享、再次合并)
│ ├── MergeResultScreen.kt// 成功页(输出文件卡片、打开、分享、返回)
│ ├── components/
│ │ ├── PdfItemCard.kt // 单个文件条目卡片(左侧52dp大区域长按拖拽、48dp按钮热区、序号重排)
│ │ ├── SaveSettingsDialog.kt // 保存前修改默认文件名弹窗
│ │ └── MergeProgressDialog.kt// 实时多步骤处理进度浮层
│ └── theme/
│ ├── Color.kt // Material 3 基础配色
│ ├── Theme.kt // 亮色/深色动态主题切换
│ └── Type.kt // Typography 字体阶梯
└── test/java/com/pdfmerger/app/
├── PdfMergerLogicTest.kt // 文件名格式、大小换算、排序不变性单元测试
└── MergeHistoryAndLibraryLogicTest.kt // 历史日期格式化、重复文件过滤、库排序单元测试
- 「合并」:核心合并流程,支持 SAF 文件选取、拖拽重排、标准 TopAppBar(返回防误触确认弹窗与清空弹窗)、底部固定合并栏;
- 「PDF 文件」:全自动扫描 Download / 下载目录及所有子文件夹,通过 Android 11+
MANAGE_EXTERNAL_STORAGE(及旧版存储权限)智能读取,返回 App 自动重检权限并触发扫描,支持实时文件名搜索、按时间/名称/大小多维度排序、长按多选批量「加入合并」; - 「历史」:按日期倒序归档合并历史,支持一键打开、系统分享、再次合并(一键将源文件重新拉回合并工作区)和单独删除记录;
- 跨 Tab 状态保持:底部三栏随意切换,第一页已选文件和排序 100% 完好保留,杜绝误丢失。
- 左侧大范围拖拽区:宽度提升至 52dp 且纵向覆盖整个条目卡片,长按整个左侧区域即可顺畅拖拽调整顺序,伴随卡片 Elevation 与微缩放视觉反馈;
- 48dp × 48dp 触控热区:上移
[ ↑ ]、下移[ ↓ ]与删除[ × ]按钮均达到官方 Material Design 规定的 48dp 无障碍点击标准,首尾项按键自动调暗禁用; - 超长文件名安全截断:中间文本自动省略(Ellipsis),右侧操作按钮永不被挤压变形或折行。
- 当选择至少 2 个有效 PDF 后,底部 「合并 PDF」 激活;
- 点击后弹出简洁命名确认对话框,展示默认文件名
合并文件_yyyyMMdd_HHmm.pdf(如合并文件_20260926_1830.pdf)并固定保存位置为 「下载 / Download」,无需每次手动选文件夹; - 文件名冲突自动避让:若
Download目录下已存在同名文件,系统自动递增生成合并文件 (1).pdf、合并文件 (2).pdf,绝不直接覆盖用户已有文件; - Android 10+ MediaStore 流式写入:采用
MediaStore.Downloads标准接口写入,合并完成自动触发「PDF 文件」页刷新,新文件即刻可见。
- 合并过程展示半透明进度对话框:
- 显示“正在处理第 X 个,共 Y 个文件”;
- 显示当前正在解析和拼合的源文件名;
- 动态圆环与进度条同步反映完成百分比;
- 合并中禁止重复点击,杜绝并发冲突。
- 合并完成后无缝切至成功页面,自动写入本地持久化历史记录(保存 MediaStore Uri,绝不报
FileUriExposedException); - 显示“已保存到:Download / 下载”与最终大小;
- 「打开 PDF」:直接调用系统默认阅读器(如 WPS、Adobe Reader、Google PDF Viewer 等)查看合并效果;
- 「分享 PDF」:调起系统分享菜单,可通过微信、QQ、邮件或蓝牙一键发送给他人;
- 「完成」:清空当前任务,随时开启下一次合并。
很多简陋的合并 App 容易发生内存溢出(OutOfMemoryError),原因在于直接使用 inputStream.readBytes() 将整个 PDF 加载到了 JVM 堆中。
PdfMerger 采取了全链路低内存策略:
- 禁止一次性全部读入内存:严禁调用任何全量字节数组操作;
- 混合临时文件缓冲策略:
超过阈值(20MB)的数据由 PDFBox 自动暂存至 App 沙盒临时目录并在合并结束后自动回收,轻松应对 100MB、200MB 甚至更大的超大文档合并;
val memorySetting = MemoryUsageSetting.setupMixed(20 * 1024 * 1024) .setTempDir(File(context.cacheDir, "pdfbox_temp").apply { mkdirs() })
- 逐文档流水线合并:通过
PDFMergerUtility.appendDocument(destinationDoc, srcDoc),每追加完一个源文档立即调用srcDoc.close()释放其句柄与资源,内存占用始终处于恒定低位。
App 对各类边界情况做了严密保护,绝不产生无提示的静默闪退:
| 异常场景 | 防护措施与用户反馈 |
|---|---|
| PDF 已加密(含密码) | 选入时自动探测 document.isEncrypted,卡片显示红字提醒「已加密」,并在合并前拦截,提示用户移除受密码保护的文件 |
| 文件损坏或格式不兼容 | 捕获 InvalidPasswordException / IOException,标记为损坏文件,提示重新选择 |
| 源文件被外部删除 | 打开输入流失败时拦截并提示「无法读取文件,请检查文件是否存在」 |
| 权限失效 | 选择文件后立即调用 takePersistableUriPermission 申请持久只读授权 |
| 用户取消选择 / 取消保存 | SAF 接口返回空 Uri 时平滑忽略,保持原界面状态不变 |
| 设备存储不足 | 捕获 I/O 异常并给出友好提示,避免破坏损坏目标文件 |
- 打开 Android Studio(推荐 2024.1+,配 JDK 17);
- 选择 Open,打开
E:\Claude-project\PdfMerger; - 等待 Gradle 自动完成依赖索引同步;
- 用数据线连接 Android 手机并开启 USB 调试;
- 点击工具栏绿色的 Run 'app' 按钮即可安装体验。
若需要生成可独立分发的安装包(APK):
- 在 Android Studio 顶部菜单点击 Build -> Build Bundle(s) / APK(s) -> Build APK(s);
- 编译成功后点击右下角提示中的 locate;
- 将生成的
app-debug.apk发送给手机安装即可。