简介:面向 Android 开发者的 Word/PDF 预览示例工程,定位移动办公场景中的文档在线查看需求。项目演示了通过 WebView 加载 PDF.js 实现在线预览 PDF,以及借助 Intent 调用手机上的 Word 预览软件打开文档,并串联了运行时存储权限、FileProvider 文件共享、HTTPS 安全加载等关键环节,适合需要快速实现文档预览功能的中级 Android 开发者参考。压缩包共 2047 个文件,大小约 87.35MB。其中 java/xml 为源码与布局,class/dex/apk 为编译产物,png 为界面资源,aar/jar 为依赖库,so 为底层库,pdf/docx 为示例文档,整体目录结构清晰,便于结合源码和构建产物对照学习。已有 1881 人学习下载。这一案例展示了 Android 文档预览的完整链路:从 WebView 环境配置、PDF.js 渲染,到跨应用调用与不同系统版本的文件访问适配;同时也能了解接入 AndroidPdfViewer、POI、PDFBox 等第三方库的选型思路,为自行封装或扩展预览功能打下基础。
1. Android预览Word和PDF:早就不是“调个接口”那么简单
在Android应用里做附件预览,Word和PDF这俩格式几乎绕不开。你以为只是打开一个文件,实际一接需求才发现,PDF有渲染引擎的选型问题,Word更麻烦——原生系统压根没给一条“能用的路”,要么转码、要么解析、要么交给WebView硬扛。开发圈里常听到“Android预览PDF一把梭,预览Word全是玄学”,这话不夸张。这篇笔记从一个可复现的Demo工程出发,把两套预览方案的选型逻辑、核心代码、边界参数和踩坑记录都拆开讲,适合刚接手文件预览模块、准备自研而不是接付费SDK的开发者。读完之后,你能自己搭出一个Word和PDF的预览壳子,也知道哪里会翻车。
2. PDF预览:PdfRenderer与开源库选型对比及两个“边界”
2.1 PDF渲染的两种路径:系统内置还是第三方库
PDF预览的技术路线,业内基本分为两类:一类是Android系统内置的PdfRenderer,另一类是依赖第三方库如PdfiumCore、AndroidPdfViewer等。PdfRenderer是API 21加入的系统级API,底层走的是原生渲染,优点是不要额外引入一堆so文件,APK体积干净,缺点也明显——它只负责把PDF的某一页渲染成Bitmap,缩放、翻页、手势都要自己写,等于给了一台发动机,车壳得自己拼。
第三方库则相反,常见做法是封装了Pdfium(也就是PDFium渲染引擎的JNI层),把页面加载、缩放、滑动、双页模式都做好了,你引入后直接替换View即可。选型时的判断维度我一般看三点:目标设备的系统版本占比、APK体积容忍度、是否需要离线渲染。如果App最低版本就是Android 5.0以上,且PDF预览只是辅助功能,用系统PdfRenderer就不亏;如果要做一个独立的文档查看器,追求手势流畅和渲染速度,就上PdfiumCore这类方案。
2.2 PdfRenderer实战:从创建到翻页
先给出一段基于系统API的PDF渲染核心代码,这是在Demo里跑通的基础。
// PdfRenderer的典型用法:逐页渲染到Bitmap import android.graphics.Bitmap; import android.graphics.pdf.PdfRenderer; import android.os.ParcelFileDescriptor; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class PdfRenderHelper { private PdfRenderer pdfRenderer; private ParcelFileDescriptor descriptor; // 初始化:传入一个本地文件路径 public void init(String filePath) throws IOException { File file = new File(filePath); descriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); pdfRenderer = new PdfRenderer(descriptor); } // 渲染指定页码:返回一张宽高自适应屏幕的Bitmap public Bitmap renderPage(int pageIndex, int viewWidth, int viewHeight) { PdfRenderer.Page page = pdfRenderer.openPage(pageIndex); Bitmap bitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888); // 关键参数:渲染的目标矩形、变换矩阵、渲染模式 page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); page.close(); return bitmap; } public int getPageCount() { return pdfRenderer.getPageCount(); } public void close() { try { if (pdfRenderer != null) pdfRenderer.close(); if (descriptor != null) descriptor.close(); } catch (IOException e) { e.printStackTrace(); } } }这段代码的逻辑分三步:先把文件通过ParcelFileDescriptor打开,这是系统API接收文件的唯一方式;接着用openPage拿到页对象;最后调用render把内容画到指定Bitmap上。参数里最需要注意的是Bitmap的尺寸——直接用viewWidth和viewHeight而不是固定写死分辨率,是因为PdfRenderer渲染不会自动适配屏幕密度,你给多大Bitmap它就画多大,如果Bitmap大小和屏幕逻辑尺寸不匹配,预览时会明显模糊或发虚。RENDER_MODE_FOR_DISPLAY适合交互式浏览,裁剪模式则适合打印场景,而且模式选错会导致缩放时重绘成本倍增。
2.3 开源库方案的补充
用PdfiumCore这类库,核心代码会简化成一段“加载->绑定View->翻页”的流程。常见做法在build.gradle里引入依赖后,自定义一个View绑定文档对象。它们的内部原理是:先把PDF文件整体送入native层,由Pdfium解析页面结构,再通过JNI回调把每一帧图形指令传给Skia渲染,最后绘制到Canvas上。相比系统API,它多了一层文档缓存,所以在连续快速翻页时性能更稳。
不过第三方库的坑也藏在这层缓存里。有一类典型场景:加载一个20MB以上的扫描版PDF,内存占用直接飙到300MB以上,因为预览控件默认会把整份文档的页面索引都建好,每一页的元数据常驻内存。解决方法是初始化时控制预加载页数。例如AndroidPdfViewer可以在Builder里设置.pages(0, 1, 2)只加载前三页,剩下的等用户滑动到附近再加载。我刚接触这类库时习惯把全部页面交给它,内存直接爆掉,后来养成的习惯是只配置实际需要展示的页码范围。
3. Word预览:把难题抛给转换层或WebView渲染
3.1 Word不能直接渲染:原生层没有“Word解释器”
Android系统本身没有解析Word文件的组件,这一点和PDF天生不同。PDF有统一的页面模型,而Word是个“流式排版”的文档结构,字体、分页、样式在不同设备上结果都不一致。所以预览Word在业界有几个常见思路:第一种是把Word转成PDF再走PDF预览流程;第二种是用WebView加载并解析HTML或通过第三方库转成HTML渲染;第三种是接入在线预览服务。Demo工程里采用的路线是按顺序尝试——先看文件类型,如果是docx,优先走OpenXML解析提取文本和基础样式,用WebView加载一个拼好的HTML。这种做法的优点是离线可用,缺点也直白:排版只能还原七八成,遇到复杂分栏、页眉页脚就会变形。
3.2 本地转换:从docx到HTML的解析链路
本地解析docx文件,本质上是解包一个ZIP容器读取document.xml,再遍历XML里的段落、表格、图片引用。核心代码可以这样组织:
# 用zipfile模块解包docx,提取关键的document.xml import zipfile import re from xml.etree import ElementTree as ET DOCX_NS = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}' def extract_docx_content(file_path): """ 从docx中提取正文文本和基础样式标记。 注意:这里没有做复杂排版还原,只提取w:p段落和w:t文本。 """ paragraphs = [] with zipfile.ZipFile(file_path) as zf: with zf.open('word/document.xml') as f: tree = ET.parse(f) root = tree.getroot() body = root.find(DOCX_NS + 'body') if body is not None: for p in body.iter(DOCX_NS + 'p'): texts = [t.text or '' for t in p.iter(DOCX_NS + 't')] paragraphs.append(''.join(texts)) return '\n'.join(paragraphs) # 调用示例 content = extract_docx_content('sample.docx') print(content[:500])这段脚本在服务端或本地运行时,把Word的正文抽出来。参数说明里有个容易忽视的点:w:t是Word里“文本”元素的命名空间标签,一个段落可能有多个w:t,必须拼起来才是完整句子;如果漏掉迭代直接取第一个,遇到一行文字被拆成多段run的情况就会丢字。这套逻辑在Android上可以封装成一个基础工具类,解析完拼一段带基础样式的HTML,再丢给WebView渲染。实际做出来的效果是纯文本能满足阅读需求,但不推荐处理带复杂表格的合同类Word。
3.3 服务端转换:一张兜底且排版最稳的网
如果文档复杂度高、对排版还原度有硬要求,我一般会先建议走服务端转换。常见做法是让后端调用LibreOffice的无头模式(headless),把docx批量转成PDF,客户端再走第2章那套PDF渲染链路。这个方案有个明显收益——排版是LibreOffice渲染引擎输出的,和Office软件本身的显示效果最接近,而且客户端完全不用关心Word内部结构。代价是服务端得承担CPU开销,转一份大文档可能需要几秒钟,接口得做成异步的。
我在模拟项目X里采用过这个方案:客户端上传原始文件,服务端转完后回传PDF地址,客户端再拉到本地预览。当时踩过的一个坑是LibreOffice转出的PDF页面尺寸可能和原文档不一致,导致客户端显示时出现白边或裁切。解决办法是在转换参数里强制指定页面大小,例如转A4就加一个-o参数带上页面格式配置,或者客户端判断PDF页面实际宽高比后,以“适配宽度”的方式展示。
4. 避坑排查:预览模块翻车记录与修复思路
4.1 大文件加载直接闪退
现象:点击一个20MB以上的PDF,App直接回到桌面,Log里能看到OutOfMemoryError。
原因:预览控件默认把整份文档加载进内存,特别是扫描版PDF,每个页面都是一张高清图片,内存直接被打穿。
解决:限制预加载页数,把渲染的Bitmap采样率降低。用PdfRenderer时,先读取原始页面尺寸,再按屏幕宽度等比计算目标Bitmap宽高,避免渲染出比屏幕还大两倍的高分辨率图;用第三方库时设置页面加载范围,并开启复用Bitmap(inBitmap)特性。
4.2 Word预览排版全乱
现象:从docx解析出来的文本在WebView里显示,字号大小不一,表格全挤在一起,图片缺失。
原因:只提取了文本,没处理样式节点、图片引用和表格结构。真实的docx里,字号在w:rPr节点下定义,而图片是以二进制ZIP存储的,不解析这些只能得到一堆没有排版的字符流。
解决:要么升级解析逻辑,把w:rPr里的字号、加粗、对齐信息拼成内联CSS;要么放弃本地解析直接走服务端转换。实践中,需要排版保真的文档优先转PDF,本地解析只服务“快速预览文本内容”的场景。
4.3 混淆配置引出的JNI崩溃
现象:打包release版本后,点击预览就闪退,debug版一切正常。
原因:第三方PDF库(如PdfiumCore)包含native方法,JNI的类名、方法名被混淆规则改掉了,native层找不到对应的JVM方法签名。
解决:在proguard-rules.pro里加keep规则,保住native方法和渲染View类。一条通用规则是-keep class org.pdfium.** { *; },同时把自定义的PdfView类也一块keep住。这个坑在接入任何带so文件的库时都值得提前防一手。
4.4 高版本系统上PDF页面空白
现象:App适配到Android 12以上后,有些PDF渲染出来是白页,或者只显示一半。
原因:系统PdfRenderer在高版本上对部分PDF的加密字典、图像解码规则更严格,直接渲染非法图像流时静默失败。
解决:在打开文件之前先做一次可读性校验:解析PDF页数时如果返回异常值或渲染结果全为空白,就切换备选渲染器。我在Demo里加了降级逻辑——系统渲染失败自动切到第三方库重新加载,这比让用户“换文件试试”体面得多。
5. 进阶:预览框架的预加载和缓存,以及一个血泪教训
预览模块做到能看、能翻页只是及格线,真正的体验差异在加载速度和滚动流畅度上。我通常会在PdfRenderer方案里加两级缓存:一级是磁盘缓存,首次渲染成功的Bitmap按“页码+宽高”压缩后落到缓存目录,下次进入时直接读文件;一级是内存缓存,用LruCache管理最近滑动到的页面,容量设成最大可用内存的1/8左右比较合理,不挤占主列表页面的资源。
针对Word预览,WebView的加载有个隐藏缺点:每次重建时重新解析HTML,导致页面白屏时间偏长。解法是给WebView配一个全局复用的Application级容器,切换文件时只替换HTML内容,不销毁整个WebView。代价是内存里常驻一个WebView实例,约占用几十MB,取舍看场景——如果预览是高频操作就值得换,如果只是偶尔打开一次,每次重建反而更稳。
这里有一个我亲历过的教训。某次优化PDF预览加载速度,我把每页的解析结果缓存在磁盘上,结果用户反馈“第一次打开快了不少,但缓存之后就打不开了”。排查发现:我缓存的Bitmap文件是直接序列化的原始像素数组,而应用升级后Bitmap.Config从ARGB_8888换成了RGB_565,反序列化时字节数对不上,导致解码失败。从那次以后,我每次缓存Bitmap时都强制把Config、宽高、字节长度一起存进一个小型的JSON元数据头里,读取时先校验再解码,版本升级也不怕缓存失效。
预览模块的工作量往往被低估——PDF和Word走的是完全不同的两套链路,选型、缓存、降级策略都要提前设计。这套Demo把系统PdfRenderer、本地docx解析、LibreOffice兜底三个方案串起来,覆盖了大多数日常预览需求。碰到定位和边界问题时,回头看看渲染日志和你选用的库版本,往往比反复调试UI更有效。希望这份拆解和踩坑记录能帮你少走这段弯路。
本文还有配套的精品资源,点击获取