Vue + Android WebView实现大文件PDF预览完整解决方案(附详细代码)
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一、问题背景</a></li><ul class="second_class_ul"><li><a href="#_lab2_0_0">1.1 具体表现</a></li><li><a href="#_lab2_0_1">1.2 尝试过的方案</a></li></ul><li><a href="#_label1">二、最终解决方案</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_2">2.1 为什么需要 PDF.js 多版本?</a></li><li><a href="#_lab2_1_3">2.2 为什么需要 Android PDF 组件?</a></li></ul><li><a href="#_label2">三、技术架构</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_4">3.1 整体架构图</a></li><li><a href="#_lab2_2_5">3.2 技术栈</a></li></ul><li><a href="#_label3">四、前端实现</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_6">4.1 PDF.js 部署</a></li><li><a href="#_lab2_3_7">4.2 Vue PDF 预览组件</a></li><ul class="third_class_ul"><li><a href="#_label3_3_7_0">4.3 组件使用方式</a></li></ul></ul><li><a href="#_label4">五、Android 端实现</a></li><ul class="second_class_ul"><li><a href="#_lab2_4_8">5.1 添加权限</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_4_9">5.2 添加依赖</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_4_10">5.3 PDF 预览弹窗布局</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_4_11">5.4 WebActivity 核心代码</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label5">六、前端调用 Android PDF 组件方法</a></li><ul class="second_class_ul"><li><a href="#_lab2_5_12">6.1 调用方式一:通过 URL 预览(推荐)</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_5_13">6.2 调用方式二:通过 Base64 数据预览(不推荐)</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label6">七、缓存策略优化</a></li><ul class="second_class_ul"></ul><li><a href="#_label7">八、实现效果</a></li><ul class="second_class_ul"><li><a href="#_lab2_7_14">8.1 桌面端浏览器 PDF 预览(PDF.js v5)</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_7_15">8.2 Android WebView PDF 预览(PDF.js v3)</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_7_16">8.3 Android PDF 组件预览(android-pdf-viewer)</a></li><ul class="third_class_ul"></ul><li><a href="#_lab2_7_17">8.4 使用体验对比</a></li><ul class="third_class_ul"></ul></ul><li><a href="#_label8">九、总结</a></li><ul class="second_class_ul"></ul><li><a href="#_label9">十、参考资源</a></li><ul class="second_class_ul"></ul></ul></div><p class="maodian"><a name="_label0"></a></p><h2>一、问题背景</h2><p>在企业级 PAD 应用开发中,我遇到了一个棘手的问题:<strong>100MB+ 的大 PDF 文件在 Android WebView 中预览时,出现严重的性能问题</strong>。</p>
<p class="maodian"><a name="_lab2_0_0"></a></p><h3>1.1 具体表现</h3>
<table><thead><tr><th>问题</th><th>表现</th></tr></thead><tbody><tr><td>缩放卡顿</td><td>双指缩放时明显卡顿,甚至卡死</td></tr><tr><td>字体模糊</td><td>放大后字体不清晰,影响阅读</td></tr><tr><td>内存溢出</td><td>大文件加载时容易 OOM 崩溃</td></tr><tr><td>渲染异常</td><td>部分页面白屏或渲染不完整</td></tr></tbody></table>
<p><strong>关键发现</strong>:同样的 Vue2 项目,在桌面端浏览器访问完全正常,问题只出现在 Android WebView 环境。</p>
<p class="maodian"><a name="_lab2_0_1"></a></p><h3>1.2 尝试过的方案</h3>
<p>我尝试了市面上几乎所有主流的 Vue PDF 预览插件,均存在不同程度的问题:</p>
<table><thead><tr><th>插件名称</th><th>存在的问题</th></tr></thead><tbody><tr><td><code>vue-pdf</code></td><td>• 部分字体丢失或显示异常<br />• WebView 中大文件缩放严重卡顿或卡死<br />• 需要手动实现缩放、翻页等功能</td></tr><tr><td><code>vue-pdf-signature</code></td><td>• 部分字体渲染模糊<br />• WebView 中大文件缩放严重卡顿或卡死<br />• 需要手动实现缩放、翻页等功能</td></tr><tr><td><code>vue-pdf-app</code></td><td>• WebView 中大文件缩放严重卡顿或卡死<br />• 放大后字体不清晰<br />• 功能完善(缩放、翻页、搜索等)</td></tr><tr><td><code>@vue-office/pdf</code></td><td>• WebView 中大文件缩放严重卡顿或卡死<br />• 需要手动实现缩放、翻页等功能</td></tr><tr><td><code>pdfjs-dist</code></td><td>• 高版本(v4+)在 WebView/移动端浏览器不兼容,样式错乱或报错<br />• 低版本放大后字体不清晰<br />• 自带 viewer.html,功能完善(缩放、翻页、搜索等)</td></tr></tbody></table>
<blockquote><p>💡 <strong>关键发现</strong>:以上插件在桌面端浏览器中表现正常,问题主要出现在 Android WebView 环境。推测可能与 WebView 的硬件加速、Canvas 渲染性能、内存限制等因素有关。</p></blockquote>
<p class="maodian"><a name="_label1"></a></p><h2>二、最终解决方案</h2>
<p>经过大量测试,我采用了 <strong>“PDF.js 多版本 + Android PDF 组件”</strong> 的混合方案:</p>
<div class="jb51code"><pre class="brush:plain;">┌─────────────────────────────────────────────────────────────┐
│ PDF 预览策略 │
├─────────────────────────────────────────────────────────────┤
│桌面端浏览器→PDF.js v5(最新特性) │
│移动端浏览器→PDF.js v3(兼容性好) │
│Android App →android-pdf-viewer 组件(性能最优) │
└─────────────────────────────────────────────────────────────┘
</pre></div>
<p class="maodian"><a name="_lab2_1_2"></a></p><h3>2.1 为什么需要 PDF.js 多版本?</h3>
<ul><li><strong>PDF.js v5+</strong>:使用了现代 JavaScript 特性,在部分 Android WebView 和移动端浏览器中会出现样式错乱或报错</li><li><strong>PDF.js v3</strong>:兼容性更好,适合移动端环境</li></ul>
<p class="maodian"><a name="_lab2_1_3"></a></p><h3>2.2 为什么需要 Android PDF 组件?</h3>
<p>即使使用 PDF.js v3,大文件在 WebView 中仍有性能瓶颈。使用 <code>android-pdf-viewer</code> PDF 组件:</p>
<ul><li>直接调用 Android 原生渲染能力</li><li>支持流畅的缩放和翻页</li><li>字体渲染清晰</li><li>内存占用更低</li></ul>
<p class="maodian"><a name="_label2"></a></p><h2>三、技术架构</h2>
<p class="maodian"><a name="_lab2_2_4"></a></p><h3>3.1 整体架构图</h3>
<div class="jb51code"><pre class="brush:js;">┌──────────────────────────────────────────────────────────────────┐
│ Vue2 前端 │
│┌────────────────────────────────────────────────────────────┐│
││ PdfJsViewer 组件 ││
││┌─────────────┐┌─────────────┐┌──────────────────┐ ││
│││ PDF.js v5 ││ PDF.js v3 ││ Android Bridge │ ││
│││ (桌面端) ││ (移动端) ││ (原生预览) │ ││
││└─────────────┘└─────────────┘└──────────────────┘ ││
│└────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────┘
│
│ JavaScript Bridge
▼
┌──────────────────────────────────────────────────────────────────┐
│ Android WebView │
│┌────────────────────────────────────────────────────────────┐│
││ WebActivity ││
││┌───────────────────────┐┌───────────────────────────┐││
│││ @JavascriptInterface││ android-pdf-viewer 插件 │││
│││ previewPdfByUrl ││ (全屏弹窗预览) │││
││└───────────────────────┘└───────────────────────────┘││
│└────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────┘
</pre></div>
<p class="maodian"><a name="_lab2_2_5"></a></p><h3>3.2 技术栈</h3>
<table><thead><tr><th>层级</th><th>技术</th><th>版本</th></tr></thead><tbody><tr><td>前端框架</td><td>Vue2</td><td>^2.6.11</td></tr><tr><td>PDF 渲染 (桌面)</td><td>PDF.js</td><td>5.4.449</td></tr><tr><td>PDF 渲染 (移动)</td><td>PDF.js</td><td>3.11.174</td></tr><tr><td>Android 原生</td><td>android-pdf-viewer</td><td>3.2.0-beta.3</td></tr><tr><td>工具库</td><td>Hutool</td><td>5.8.16</td></tr></tbody></table>
<p class="maodian"><a name="_label3"></a></p><h2>四、前端实现</h2>
<p class="maodian"><a name="_lab2_3_6"></a></p><h3>4.1 PDF.js 部署</h3>
<p>将两个版本的 PDF.js 放到 Vue 项目的 <code>public</code> 目录:</p>
<div class="jb51code"><pre class="brush:js;">public/
├── pdfjs-v3/
│ ├── build/
│ │ ├── pdf.js
│ │ ├── pdf.worker.js
│ │ └── ...
│ └── web/
│ ├── viewer.html
│ ├── viewer.js
│ ├── viewer.css
│ ├── locale/
│ ├── images/
│ └── ...
├── pdfjs-v5/
│ ├── build/
│ │ ├── pdf.mjs
│ │ ├── pdf.worker.mjs
│ │ └── ...
│ └── web/
│ ├── viewer.html
│ ├── viewer.mjs
│ ├── viewer.css
│ ├── locale/
│ ├── images/
│ └── ...
</pre></div>
<blockquote><p>📝 <strong>说明</strong>:直接将下载的 PDF.js 发布包解压到 public 目录即可。</p></blockquote>
<p><strong>下载地址</strong>:</p>
<ul><li>PDF.js: https://github.com/mozilla/pdf.js/releases</li></ul>
<p class="maodian"><a name="_lab2_3_7"></a></p><h3>4.2 Vue PDF 预览组件</h3>
<p><strong>文件路径</strong>:<code>src/components/PdfJsViewer/index.vue</code></p>
<div class="jb51code"><pre class="brush:xhtml;"><template>
<div class="pdfjs-viewer-container" :style="containerStyle">
<!-- Loading 状态 -->
<div v-if="loading" class="pdfjs-loading">
<div class="loading-spinner"></div>
<span class="loading-text">{{ loadingText }}</span>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="pdfjs-error">
<i class="el-icon-warning-outline"></i>
<span class="error-text">{{ error }}</span>
<el-button type="primary" @click="loadPdf">重新加载</el-button>
</div>
<template v-else-if="pdfBlobUrl">
<!-- Android PDF 组件预览按钮 -->
<span v-if="isAndroidApp" class="native-preview-btn" @click="openWithNativeViewer">
查看原图
</span>
<!-- PDF 预览 iframe -->
<iframe
ref="pdfIframe"
:src="viewerUrl"
class="pdfjs-iframe"
frameborder="0"
allowfullscreen
></iframe>
</template>
</div>
</template>
<script>
import request from '@/utils/request'
import {isNotNull} from "@/utils/common";
import defaultSettings from "@/settings";
import {getToken} from "@/utils/auth";
import {md5} from "@/utils/secret";
export default {
name: 'PdfJsViewer',
props: {
// PDF 文件路径(用于 API 请求)
url: {
type: String,
required: true
},
// 容器高度
height: {
type: String,
default: '100%'
},
// 容器宽度
width: {
type: String,
default: '100%'
},
// 加载提示文字
loadingText: {
type: String,
default: '正在加载PDF文件...'
}
},
data() {
return {
loading: false,
error: null,
pdfBlobUrl: null
}
},
computed: {
containerStyle() {
return {
height: this.height,
width: this.width
}
},
// 检测是否为移动端
isMobile() {
const userAgent = navigator.userAgent.toLowerCase()
const mobileKeywords = ['android', 'iphone', 'ipad', 'ipod', 'windows phone', 'mobile']
return mobileKeywords.some(keyword => userAgent.includes(keyword))
},
// 检测是否为 Android App
isAndroidApp() {
const userAgent = navigator.userAgent.toLowerCase()
return userAgent.includes('android') && window.android
},
viewerUrl() {
if (!this.pdfBlobUrl) return ''
// 根据设备类型选择不同的 pdfjs 版本
// 移动端使用 pdfjs-v3,桌面端使用 pdfjs-v5
const pdfjsPath = this.isMobile ? '/pdfjs-v3/web/viewer.html' : '/pdfjs-v5/web/viewer.html'
// 将 Blob URL 作为参数传给 viewer.html
return `${defaultSettings.publicPath}${pdfjsPath}?file=${encodeURIComponent(this.pdfBlobUrl)}#zoom=page-width`
}
},
watch: {
url: {
immediate: true,
handler(newUrl) {
if (isNotNull(newUrl)) {
this.loadPdf()
} else {
// 清理之前的 PDF 数据
this.revokePdfData()
}
}
}
},
methods: {
/**
* 获取 PDF 文件流
*/
async fetchPdfFile(previewUrl) {
return request({
url: previewUrl,
method: 'get',
responseType: 'arraybuffer'
})
},
/**
* 加载 PDF 文件
*/
async loadPdf() {
// 清理之前的 PDF 数据
this.revokePdfData()
this.loading = true
this.error = null
try {
// 获取文件流
const response = await this.fetchPdfFile(this.url)
// 检查响应数据
if (!response || response.byteLength === 0) {
this.error = '获取到的文件内容为空'
this.$emit('load-error', new Error(this.error))
return
}
// 创建 Blob 对象
const blob = new Blob(, { type: 'application/pdf' })
// 创建 Blob URL
this.pdfBlobUrl = URL.createObjectURL(blob)
this.$emit('load-success')
// 启动字体问题修复机制
this.fixFontIssue()
} catch (err) {
console.error('PDF 加载失败:', err)
this.error = err.message || '加载PDF文件失败,请重试'
this.$emit('load-error', err)
} finally {
this.loading = false
}
},
/**
* 修复移动端PDF字体显示问题
*/
fixFontIssue() {
// 只在移动端执行
if (!this.isMobile) return
// 最大重试次数
const MAX_RETRY_COUNT = 3;
const attemptFix = (retryCount = 0) => {
if (retryCount > MAX_RETRY_COUNT) return
setTimeout(() => {
try {
const iframe = this.$refs.pdfIframe
if (iframe && iframe.contentWindow && iframe.contentWindow.PDFViewerApplication) {
const app = iframe.contentWindow.PDFViewerApplication
// 检查PDF是否已加载
if (app.pdfDocument && app.pdfViewer.pagesPromise) {
// 等待页面渲染完成后再进行修复
app.pdfViewer.pagesPromise.then(() => {
// 切换缩放模式触发重新渲染
app.pdfViewer.currentScaleValue = "page-fit"
setTimeout(() => {
app.pdfViewer.currentScaleValue = "page-width"
}, 50)
})
} else if (retryCount < MAX_RETRY_COUNT) {
// 如果PDF还未完全加载,稍后重试
attemptFix(retryCount + 1)
}
} else if (retryCount < MAX_RETRY_COUNT) {
// 如果iframe还未准备好,稍后重试
attemptFix(retryCount + 1)
}
} catch (e) {
alert("字体修复尝试失败")
console.warn("字体修复尝试失败:", e)
if (retryCount < MAX_RETRY_COUNT) {
attemptFix(retryCount + 1)
}
}
}, 500 * (retryCount + 1)) // 递增延迟时间
}
attemptFix()
},
/**
* 使用 Android PDF 预览插件打开 PDF
*/
async openWithNativeViewer() {
if (!this.pdfBlobUrl) {
this.$message.warning('PDF 文件未加载完成')
return
}
if (!this.isAndroidApp) {
this.$message.warning('当前环境不支持 Android PDF 预览插件')
return
}
try {
// 调用Android PDF预览插件方法
const success = this.previewPdfByNative(this.url)
if (!success) {
this.$message.error({message: "打开 Android PDF 预览插件失败,请重试", offset: 80})
}
} catch (err) {
console.error('打开 Android PDF 预览插件失败:', err)
this.$message.error({message: "打开 Android PDF 预览插件失败,请重试", offset: 80})
}
},
/**
* 使用 Android PDF 预览插件
* @param {String} url - PDF 文件 URL
* @returns {Boolean} 是否成功调用
*/
previewPdfByNative(url) {
if (!this.isAndroidApp || !window.android.previewPdfByUrl) {
this.$message.warning({message: "'当前环境不支持 Android PDF 预览插件", offset: 80})
return false
}
try {
const fileName = md5(url) + ".pdf";
window.android.previewPdfByUrl(
url,
fileName,
JSON.stringify({
Authorization: getToken()
})
)
return true
} catch (err) {
console.error('调用 Android PDF 预览插件失败:', err)
return false
}
},
/**
* 释放 PDF 数据
*/
revokePdfData() {
if (this.pdfBlobUrl) {
// 组件销毁时释放 Blob URL
URL.revokeObjectURL(this.pdfBlobUrl)
this.pdfBlobUrl = null
}
}
},
beforeDestroy() {
// 组件销毁时释放 PDF 数据
this.revokePdfData()
}
}
</script>
<style scoped>
.pdfjs-viewer-container {
position: relative;
overflow: hidden;
background-color: #525659;
}
.pdfjs-iframe {
width: 100%;
height: 100%;
border: none;
}
.pdfjs-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #f5f7fa;
color: #606266;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(64, 158, 255, 0.3);
border-top-color: #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
margin-top: 16px;
font-size: 14px;
}
.pdfjs-error {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #525659;
color: #fff;
}
.pdfjs-error .el-icon-warning-outline {
font-size: 48px;
color: #e6a23c;
margin-bottom: 16px;
}
.error-text {
font-size: 14px;
margin-bottom: 16px;
text-align: center;
padding: 0 20px;
}
/* Android PDF 预览插件按钮样式 */
.native-preview-btn {
position: absolute;
right: 50px;
height: 32px;
line-height: 33px;
z-index: 1000;
text-align: center;
font-size: 14px;
cursor: pointer;
:active {
background: none;
}
}
</style>
</pre></div>
<p class="maodian"><a name="_label3_3_7_0"></a></p><h4>4.3 组件使用方式</h4>
<div class="jb51code"><pre class="brush:js;"><template>
<div>
<pdf-js-viewer
:url="pdfUrl"
height="100vh"
@load-success="onLoadSuccess"
@load-error="onLoadError"
/>
</div>
</template>
<script>
import PdfJsViewer from '@/components/PdfJsViewer'
export default {
components: { PdfJsViewer },
data() {
return {
pdfUrl: 'https://example.com/document.pdf'
}
},
methods: {
onLoadSuccess() {
console.log('PDF 加载成功')
},
onLoadError(err) {
console.error('PDF 加载失败:', err)
}
}
}
</script>
</pre></div>
<p class="maodian"><a name="_label4"></a></p><h2>五、Android 端实现</h2>
<p class="maodian"><a name="_lab2_4_8"></a></p><h3>5.1 添加权限</h3>
<p><strong>AndroidManifest.xml</strong>:</p>
<div class="jb51code"><pre class="brush:xml;"><manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 网络权限,用于下载 PDF 文件 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- ... -->
</manifest>
</pre></div>
<p class="maodian"><a name="_lab2_4_9"></a></p><h3>5.2 添加依赖</h3>
<p><strong>app/build.gradle</strong>:</p>
<div class="jb51code"><pre class="brush:js;">dependencies {
// ... 其他依赖
// PDF 预览插件
implementation 'com.github.mhiew:android-pdf-viewer:3.2.0-beta.3'
// 工具库(用于 HTTP 请求和文件操作)
implementation 'cn.hutool:hutool-all:5.8.16'
}
</pre></div>
<p><strong>注意</strong>:该库托管在 JitPack,需要在项目根 <code>build.gradle</code> 中添加:</p>
<div class="jb51code"><pre class="brush:js;">allprojects {
repositories {
// ...
maven { url 'https://jitpack.io' }
}
}
</pre></div>
<p class="maodian"><a name="_lab2_4_10"></a></p><h3>5.3 PDF 预览弹窗布局</h3>
<p><strong>res/layout/dialog_pdf_preview.xml</strong>:</p>
<div class="jb51code"><pre class="brush:xml;"><?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<!-- 标题栏 -->
<RelativeLayout
android:id="@+id/titleBar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_alignParentTop="true"
android:background="#F5F5F5"
android:paddingHorizontal="16dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:text="PDF 预览"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPageInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="#666666"
android:textSize="14sp" />
<ImageView
android:id="@+id/ivClose"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="关闭"
android:padding="8dp"
android:src="@android:drawable/ic_menu_close_clear_cancel" />
</RelativeLayout>
<!-- PDF 预览区域 -->
<com.github.barteksc.pdfviewer.PDFView
android:id="@+id/pdfView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/titleBar" />
</RelativeLayout>
</pre></div>
<p class="maodian"><a name="_lab2_4_11"></a></p><h3>5.4 WebActivity 核心代码</h3>
<div class="jb51code"><pre class="brush:js;">package com.qms.android;
import androidx.appcompat.app.AlertDialog;
import android.app.Activity;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.util.Base64;
import com.github.barteksc.pdfviewer.PDFView;
import cn.hutool.core.io.FileUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class WebActivity extends Activity {
private WebView mWebView;
// ... 其他代码省略 ...
/**
* 初始化 WebView,添加 JavaScript 接口
*/
public void initWebView() {
// ... WebView 配置代码 ...
// 添加 JavaScript 接口,供前端调用
mWebView.addJavascriptInterface(this, "android");
}
// ==================== PDF 预览相关方法 ====================
/**
* JavaScript 接口:通过 URL 预览 PDF
* @param url PDF 文件的下载地址
* @param fileName 文件名(用于缓存)
* @param headersJson 请求头信息的 JSON 字符串
*/
@JavascriptInterface
public void previewPdfByUrl(String url, String fileName, String headersJson) {
runOnUiThread(() -> {
if (TextUtils.isEmpty(url)) {
ToastUtils.showLong(this, "PDF 地址为空");
return;
}
downloadPdfAndPreview(url, fileName, headersJson);
});
}
/**
* JavaScript 接口:通过 Base64 数据预览 PDF
* @param base64Data PDF 文件的 Base64 编码数据
*/
@JavascriptInterface
public void previewPdfByData(String base64Data) {
runOnUiThread(() -> {
if (TextUtils.isEmpty(base64Data)) {
ToastUtils.showLong(this, "PDF 数据为空");
return;
}
saveBase64PdfAndPreview(base64Data);
});
}
/**
* 显示加载提示弹窗
*/
private AlertDialog showLoadingDialog(String message) {
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.HORIZONTAL);
layout.setPadding(48, 32, 48, 32);
layout.setGravity(android.view.Gravity.CENTER_VERTICAL);
ProgressBar progressBar = new ProgressBar(this);
layout.addView(progressBar);
TextView textView = new TextView(this);
textView.setText(message);
textView.setTextSize(16);
textView.setPadding(32, 0, 0, 0);
layout.addView(textView);
AlertDialog dialog = new AlertDialog.Builder(this)
.setView(layout)
.setCancelable(false)
.create();
dialog.show();
return dialog;
}
/**
* 下载 PDF 文件并预览
*/
private void downloadPdfAndPreview(String url, String fileName, String headersJson) {
Map<String, String> headers = parseHeaders(headersJson);
if (TextUtils.isEmpty(fileName)) {
// 使用 URL 的 MD5 作为缓存文件名
fileName = SecureUtil.md5(url) + ".pdf";
}
// 下载到缓存目录
String cachePath = getCacheDir().getAbsolutePath() + File.separator + "pdf_preview";
File pdfFile = new File(cachePath, fileName);
if (pdfFile.exists()) {
// 缓存已存在,直接显示
showPdfDialog(pdfFile);
return;
}
// 显示加载提示
AlertDialog loadingDialog = showLoadingDialog("正在加载文件,请稍候...");
// 在子线程中执行下载
new Thread(() -> {
try {
HttpRequest request = HttpRequest.get(url);
// 添加请求头
if (!headers.isEmpty()) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
request.header(entry.getKey(), entry.getValue());
}
}
HttpResponse response = request.execute();
runOnUiThread(loadingDialog::dismiss);
if (response.isOk()) {
FileUtil.writeBytes(response.bodyBytes(), pdfFile);
runOnUiThread(() -> showPdfDialog(pdfFile));
} else {
runOnUiThread(() -> ToastUtils.showLong(this, "PDF 下载失败"));
}
} catch (Exception e) {
e.printStackTrace();
runOnUiThread(() -> {
loadingDialog.dismiss();
ToastUtils.showLong(this, "PDF 下载失败: " + e.getMessage());
});
}
}).start();
}
/**
* 解析请求头信息
*/
private Map<String, String> parseHeaders(String headersJson) {
Map<String, String> headers = new HashMap<>();
if (TextUtils.isEmpty(headersJson)) return headers;
try {
JSONObject jsonObject = new JSONObject(headersJson);
Iterator<String> keys = jsonObject.keys();
while (keys.hasNext()) {
String key = keys.next();
headers.put(key, jsonObject.optString(key));
}
} catch (JSONException e) {
e.printStackTrace();
}
return headers;
}
/**
* 保存 Base64 格式的 PDF 数据并预览
*/
private void saveBase64PdfAndPreview(String base64Data) {
AlertDialog loadingDialog = showLoadingDialog("正在加载文件,请稍候...");
try {
// 使用 Base64 数据的 MD5 作为缓存文件名
String cacheFileName = SecureUtil.md5(base64Data) + ".pdf";
String cachePath = getCacheDir().getAbsolutePath() + File.separator + "pdf_preview";
File pdfFile = new File(cachePath, cacheFileName);
if (!pdfFile.exists()) {
byte[] bytes = Base64.decode(base64Data, Base64.DEFAULT);
FileUtil.writeBytes(bytes, pdfFile);
}
loadingDialog.dismiss();
showPdfDialog(pdfFile);
} catch (Exception e) {
loadingDialog.dismiss();
ToastUtils.showLong(this, "PDF 数据处理失败");
}
}
/**
* 显示 PDF 预览对话框
*/
private void showPdfDialog(File pdfFile) {
if (pdfFile == null || !pdfFile.exists()) {
ToastUtils.showLong(this, "PDF 文件不存在");
return;
}
View view = LayoutInflater.from(this).inflate(R.layout.dialog_pdf_preview, null, false);
PDFView pdfView = view.findViewById(R.id.pdfView);
ImageView ivClose = view.findViewById(R.id.ivClose);
TextView tvPageInfo = view.findViewById(R.id.tvPageInfo);
AlertDialog dialog = new AlertDialog.Builder(this)
.setView(view)
.setCancelable(true)
.create();
ivClose.setOnClickListener(v -> dialog.dismiss());
dialog.show();
// 设置弹窗全屏显示
Window window = dialog.getWindow();
if (window != null) {
window.setLayout(WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT);
window.setBackgroundDrawableResource(android.R.color.white);
}
// 延迟加载 PDF,确保 View 已完成布局
pdfView.post(() -> loadPdf(pdfView, pdfFile, tvPageInfo));
}
/**
* 加载并显示 PDF 内容
*/
private void loadPdf(PDFView pdfView, File pdfFile, TextView tvPageInfo) {
pdfView.fromFile(pdfFile)
.enableSwipe(true) // 启用滑动翻页
.swipeHorizontal(false) // 垂直滑动
.enableDoubletap(true) // 启用双击缩放
.defaultPage(0) // 默认显示第一页
.enableAnnotationRendering(false)
.enableAntialiasing(true) // 启用抗锯齿
.spacing(4) // 页面间距
.onPageChange((page, pageCount) -> {
tvPageInfo.setText(String.format("%d / %d", page + 1, pageCount));
})
.onLoad(nbPages -> {
tvPageInfo.setText(String.format("1 / %d", nbPages));
// 设置缩放范围
pdfView.setMinZoom(0.5f);
pdfView.setMaxZoom(10.0f);
pdfView.setMidZoom(2f);
})
.onError(t -> {
ToastUtils.showLong(this, "PDF 加载失败: " + t.getMessage());
})
.load();
}
}
</pre></div>
<p class="maodian"><a name="_label5"></a></p><h2>六、前端调用 Android PDF 组件方法</h2>
<p class="maodian"><a name="_lab2_5_12"></a></p><h3>6.1 调用方式一:通过 URL 预览(推荐)</h3>
<p><strong>推荐使用此方式</strong>,由 Android 端负责下载和渲染,前端只需传递 URL 和请求头。</p>
<div class="jb51code"><pre class="brush:js;">// 检测是否为 Android App 环境
const isAndroidApp = navigator.userAgent.toLowerCase().includes('android') && window.android
if (isAndroidApp && window.android.previewPdfByUrl) {
window.android.previewPdfByUrl(
'https://api.example.com/file/xxx',// 下载 URL
'xxx.pdf', // 文件名称(缓存使用)
JSON.stringify({ // 请求头
Authorization: 'Bearer xxx'
})
)
}
</pre></div>
<p class="maodian"><a name="_lab2_5_13"></a></p><h3>6.2 调用方式二:通过 Base64 数据预览(不推荐)</h3>
<blockquote><p>⚠️ <strong>不推荐使用此方式</strong></p>
<p>原因:前端使用 <code>btoa()</code> 将大文件转换为 Base64 时会导致浏览器卡死,100MB+ 的文件基本无法处理。</p>
<p>此接口仅适用于小文件(< 5MB)场景。</p></blockquote>
<div class="jb51code"><pre class="brush:js;">// 获取 PDF 的 ArrayBuffer
const response = await fetch(pdfUrl)
const arrayBuffer = await response.arrayBuffer()
// ⚠️ 大文件会导致浏览器卡死!
const base64Data = btoa(
new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '')
)
// 调用 Android 原生方法
if (window.android?.previewPdfByData) {
window.android.previewPdfByData(base64Data)
}
</pre></div>
<p class="maodian"><a name="_label6"></a></p><h2>七、缓存策略优化</h2>
<p>为了避免重复下载相同的 PDF 文件,我使用 <strong>MD5 哈希</strong> 作为缓存文件名:</p>
<table><thead><tr><th>场景</th><th>缓存键</th><th>说明</th></tr></thead><tbody><tr><td>URL 方式(推荐)</td><td><code>MD5(url) + ".pdf"</code></td><td>相同 URL 只下载一次</td></tr><tr><td>Base64 方式</td><td><code>MD5(base64Data) + ".pdf"</code></td><td>仅适用于小文件</td></tr></tbody></table>
<p class="maodian"><a name="_label7"></a></p><h2>八、实现效果</h2>
<p class="maodian"><a name="_lab2_7_14"></a></p><h3>8.1 桌面端浏览器 PDF 预览(PDF.js v5)</h3>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202512/2025123009112492.png" /></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202512/2025123009112477.png" /></p>
<p><strong>效果说明</strong>:</p>
<ul><li>桌面端使用 PDF.js v5,功能完善</li><li>支持缩放、翻页、搜索等功能</li><li>缩放流畅、放大后字体依然清晰</li></ul>
<p class="maodian"><a name="_lab2_7_15"></a></p><h3>8.2 Android WebView PDF 预览(PDF.js v3)</h3>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202512/2025123009112496.png" /></p>
<p><strong>效果说明</strong>:</p>
<ul><li>Android WebView 使用 PDF.js v3 以保证兼容性</li><li>支持手势缩放、翻页、搜索等功能</li><li>大文件缩放时可能存在卡顿、放大后字体不清晰</li><li>右上角提供「查看原图」按钮,点击后以弹窗方式打开 Android PDF 组件预览</li></ul>
<p class="maodian"><a name="_lab2_7_16"></a></p><h3>8.3 Android PDF 组件预览(android-pdf-viewer)</h3>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202512/2025123009112530.png" /></p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202512/2025123009112582.png" /></p>
<p><strong>效果说明</strong>:</p>
<ul><li>全屏弹窗预览,沉浸式体验</li><li>顶部显示页码信息(如 1 / 1)</li><li>右上角关闭按钮,操作便捷</li><li>支持双指缩放</li><li><strong>放大后字体渲染清晰,缩放流畅无卡顿</strong></li></ul>
<p class="maodian"><a name="_lab2_7_17"></a></p><h3>8.4 使用体验对比</h3>
<table><thead><tr><th>指标</th><th>WebView + PDF.js</th><th>Android PDF 组件</th></tr></thead><tbody><tr><td>缩放流畅度</td><td>大文件卡顿明显,甚至卡死</td><td>丝滑流畅</td></tr><tr><td>字体清晰度</td><td>放大后模糊</td><td>始终清晰</td></tr><tr><td>内存占用</td><td>较高,易 OOM</td><td>较低,稳定</td></tr></tbody></table>
<blockquote><p>📝 以上为实际使用中的主观体验对比,非精确测量数据。</p></blockquote>
<p class="maodian"><a name="_label8"></a></p><h2>九、总结</h2>
<p>本文介绍了在 Vue + Android WebView 环境下预览大文件 PDF 的完整解决方案:</p>
<ol><li><strong>PDF.js 多版本策略</strong>:桌面端用 v5,移动端用 v3,解决兼容性问题</li><li><strong>Android PDF 组件</strong>:使用 <code>android-pdf-viewer</code> 实现高性能预览</li><li><strong>JS Bridge 通信</strong>:通过 <code>@JavascriptInterface</code> 实现前端与原生的交互</li><li><strong>MD5 缓存策略</strong>:避免重复下载,提升用户体验</li></ol>
<p>这套方案已在生产环境稳定运行,成功解决了大文件 PDF 在 PAD 上的预览问题。</p>
<p class="maodian"><a name="_label9"></a></p><h2>十、参考资源</h2>
<ul><li><a href="https://mozilla.github.io/pdf.js/" rel="external nofollow" target="_blank">PDF.js 官方文档</a></li><li><a href="https://github.com/mhiew/AndroidPdfViewer" rel="external nofollow" target="_blank">android-pdf-viewer GitHub</a></li><li><a href="https://hutool.cn/" rel="external nofollow" target="_blank">Hutool 工具库</a></li></ul>
頁:
[1]