Skip to content
xiaotuantuanaPublic

About

Offline Android PDF merger built with Kotlin, Jetpack Compose, and PDFBox.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

PdfMerger - 安卓端本地 PDF 合并工具

定位:轻量、简洁、纯本地离线处理、无任何网络上传依赖的 Android 原生 PDF 合并工具。
核心目标:帮助用户快速选取手机中的多个 PDF 文件,自由调整排序,在本地合并为一个全新的 PDF 并保存到手机。


目录

  1. 项目简介与设计哲学
  2. 技术架构选型
  3. 完整代码结构
  4. 核心功能特性
  5. 内存保护与大文件处理机制
  6. 异常与边界处理策略
  7. 如何在 Android Studio 中编译与运行
  8. 打包独立 APK 指南

项目简介与设计哲学

在日常学术研究、职场办公和合同签署中,合并 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 // 历史日期格式化、重复文件过滤、库排序单元测试

核心功能特性

1. 三栏底部导航架构

  • 「合并」:核心合并流程,支持 SAF 文件选取、拖拽重排、标准 TopAppBar(返回防误触确认弹窗与清空弹窗)、底部固定合并栏;
  • 「PDF 文件」:全自动扫描 Download / 下载目录及所有子文件夹,通过 Android 11+ MANAGE_EXTERNAL_STORAGE(及旧版存储权限)智能读取,返回 App 自动重检权限并触发扫描,支持实时文件名搜索、按时间/名称/大小多维度排序、长按多选批量「加入合并」;
  • 「历史」:按日期倒序归档合并历史,支持一键打开、系统分享、再次合并(一键将源文件重新拉回合并工作区)和单独删除记录;
  • 跨 Tab 状态保持:底部三栏随意切换,第一页已选文件和排序 100% 完好保留,杜绝误丢失。

2. 交互重构:大热区拖拽手势与 48dp 按钮

  • 左侧大范围拖拽区:宽度提升至 52dp 且纵向覆盖整个条目卡片,长按整个左侧区域即可顺畅拖拽调整顺序,伴随卡片 Elevation 与微缩放视觉反馈;
  • 48dp × 48dp 触控热区:上移 [ ↑ ]、下移 [ ↓ ] 与删除 [ × ] 按钮均达到官方 Material Design 规定的 48dp 无障碍点击标准,首尾项按键自动调暗禁用;
  • 超长文件名安全截断:中间文本自动省略(Ellipsis),右侧操作按钮永不被挤压变形或折行。

3. 默认直接保存到 Download 与智能防重名

  • 当选择至少 2 个有效 PDF 后,底部 「合并 PDF」 激活;
  • 点击后弹出简洁命名确认对话框,展示默认文件名 合并文件_yyyyMMdd_HHmm.pdf(如 合并文件_20260926_1830.pdf)并固定保存位置为 「下载 / Download」,无需每次手动选文件夹;
  • 文件名冲突自动避让:若 Download 目录下已存在同名文件,系统自动递增生成 合并文件 (1).pdf、合并文件 (2).pdf,绝不直接覆盖用户已有文件;
  • Android 10+ MediaStore 流式写入:采用 MediaStore.Downloads 标准接口写入,合并完成自动触发「PDF 文件」页刷新,新文件即刻可见。

4. 实时多步进度展示

  • 合并过程展示半透明进度对话框:
    • 显示“正在处理第 X 个,共 Y 个文件”;
    • 显示当前正在解析和拼合的源文件名;
    • 动态圆环与进度条同步反映完成百分比;
  • 合并中禁止重复点击,杜绝并发冲突。

5. 合并完成页与一键分享

  • 合并完成后无缝切至成功页面,自动写入本地持久化历史记录(保存 MediaStore Uri,绝不报 FileUriExposedException);
  • 显示“已保存到:Download / 下载”与最终大小;
  • 「打开 PDF」:直接调用系统默认阅读器(如 WPS、Adobe Reader、Google PDF Viewer 等)查看合并效果;
  • 「分享 PDF」:调起系统分享菜单,可通过微信、QQ、邮件或蓝牙一键发送给他人;
  • 「完成」:清空当前任务,随时开启下一次合并。

内存保护与大文件处理机制

很多简陋的合并 App 容易发生内存溢出(OutOfMemoryError),原因在于直接使用 inputStream.readBytes() 将整个 PDF 加载到了 JVM 堆中。

PdfMerger 采取了全链路低内存策略:

  1. 禁止一次性全部读入内存:严禁调用任何全量字节数组操作;
  2. 混合临时文件缓冲策略:
    val memorySetting = MemoryUsageSetting.setupMixed(20 * 1024 * 1024)
        .setTempDir(File(context.cacheDir, "pdfbox_temp").apply { mkdirs() })
    超过阈值(20MB)的数据由 PDFBox 自动暂存至 App 沙盒临时目录并在合并结束后自动回收,轻松应对 100MB、200MB 甚至更大的超大文档合并;
  3. 逐文档流水线合并:通过 PDFMergerUtility.appendDocument(destinationDoc, srcDoc),每追加完一个源文档立即调用 srcDoc.close() 释放其句柄与资源,内存占用始终处于恒定低位。

异常与边界处理策略

App 对各类边界情况做了严密保护,绝不产生无提示的静默闪退:

异常场景 防护措施与用户反馈
PDF 已加密(含密码) 选入时自动探测 document.isEncrypted,卡片显示红字提醒「已加密」,并在合并前拦截,提示用户移除受密码保护的文件
文件损坏或格式不兼容 捕获 InvalidPasswordException / IOException,标记为损坏文件,提示重新选择
源文件被外部删除 打开输入流失败时拦截并提示「无法读取文件,请检查文件是否存在」
权限失效 选择文件后立即调用 takePersistableUriPermission 申请持久只读授权
用户取消选择 / 取消保存 SAF 接口返回空 Uri 时平滑忽略,保持原界面状态不变
设备存储不足 捕获 I/O 异常并给出友好提示,避免破坏损坏目标文件

如何在 Android Studio 中编译与运行

  1. 打开 Android Studio(推荐 2024.1+,配 JDK 17);
  2. 选择 Open,打开 E:\Claude-project\PdfMerger;
  3. 等待 Gradle 自动完成依赖索引同步;
  4. 用数据线连接 Android 手机并开启 USB 调试;
  5. 点击工具栏绿色的 Run 'app' 按钮即可安装体验。

打包独立 APK 指南

若需要生成可独立分发的安装包(APK):

  1. 在 Android Studio 顶部菜单点击 Build -> Build Bundle(s) / APK(s) -> Build APK(s);
  2. 编译成功后点击右下角提示中的 locate;
  3. 将生成的 app-debug.apk 发送给手机安装即可。

About

Offline Android PDF merger built with Kotlin, Jetpack Compose, and PDFBox.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages