最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue?+?Android?WebView實(shí)現(xiàn)大文件PDF預(yù)覽完整解決方案(附詳細(xì)代碼)

 更新時(shí)間:2025年12月30日 09:23:04   作者:一只小羊啊  
這篇文章主要介紹了Vue?+?Android?WebView實(shí)現(xiàn)大文件PDF預(yù)覽完整解決方案的相關(guān)資料,解決了在AndroidWebView環(huán)境下預(yù)覽大文件PDF的問(wèn)題,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下

一、問(wèn)題背景

在企業(yè)級(jí) PAD 應(yīng)用開(kāi)發(fā)中,我遇到了一個(gè)棘手的問(wèn)題:100MB+ 的大 PDF 文件在 Android WebView 中預(yù)覽時(shí),出現(xiàn)嚴(yán)重的性能問(wèn)題

1.1 具體表現(xiàn)

問(wèn)題表現(xiàn)
縮放卡頓雙指縮放時(shí)明顯卡頓,甚至卡死
字體模糊放大后字體不清晰,影響閱讀
內(nèi)存溢出大文件加載時(shí)容易 OOM 崩潰
渲染異常部分頁(yè)面白屏或渲染不完整

關(guān)鍵發(fā)現(xiàn):同樣的 Vue2 項(xiàng)目,在桌面端瀏覽器訪問(wèn)完全正常,問(wèn)題只出現(xiàn)在 Android WebView 環(huán)境。

1.2 嘗試過(guò)的方案

我嘗試了市面上幾乎所有主流的 Vue PDF 預(yù)覽插件,均存在不同程度的問(wèn)題:

插件名稱存在的問(wèn)題
vue-pdf• 部分字體丟失或顯示異常
• WebView 中大文件縮放嚴(yán)重卡頓或卡死
• 需要手動(dòng)實(shí)現(xiàn)縮放、翻頁(yè)等功能
vue-pdf-signature• 部分字體渲染模糊
• WebView 中大文件縮放嚴(yán)重卡頓或卡死
• 需要手動(dòng)實(shí)現(xiàn)縮放、翻頁(yè)等功能
vue-pdf-app• WebView 中大文件縮放嚴(yán)重卡頓或卡死
• 放大后字體不清晰
• 功能完善(縮放、翻頁(yè)、搜索等)
@vue-office/pdf• WebView 中大文件縮放嚴(yán)重卡頓或卡死
• 需要手動(dòng)實(shí)現(xiàn)縮放、翻頁(yè)等功能
pdfjs-dist• 高版本(v4+)在 WebView/移動(dòng)端瀏覽器不兼容,樣式錯(cuò)亂或報(bào)錯(cuò)
• 低版本放大后字體不清晰
• 自帶 viewer.html,功能完善(縮放、翻頁(yè)、搜索等)

?? 關(guān)鍵發(fā)現(xiàn):以上插件在桌面端瀏覽器中表現(xiàn)正常,問(wèn)題主要出現(xiàn)在 Android WebView 環(huán)境。推測(cè)可能與 WebView 的硬件加速、Canvas 渲染性能、內(nèi)存限制等因素有關(guān)。

二、最終解決方案

經(jīng)過(guò)大量測(cè)試,我采用了 “PDF.js 多版本 + Android PDF 組件” 的混合方案:

┌─────────────────────────────────────────────────────────────┐
│                      PDF 預(yù)覽策略                            │
├─────────────────────────────────────────────────────────────┤
│  桌面端瀏覽器  →  PDF.js v5(最新特性)                         │
│  移動(dòng)端瀏覽器  →  PDF.js v3(兼容性好)                         │
│  Android App   →  android-pdf-viewer 組件(性能最優(yōu))          │
└─────────────────────────────────────────────────────────────┘

2.1 為什么需要 PDF.js 多版本?

  • PDF.js v5+:使用了現(xiàn)代 JavaScript 特性,在部分 Android WebView 和移動(dòng)端瀏覽器中會(huì)出現(xiàn)樣式錯(cuò)亂或報(bào)錯(cuò)
  • PDF.js v3:兼容性更好,適合移動(dòng)端環(huán)境

2.2 為什么需要 Android PDF 組件?

即使使用 PDF.js v3,大文件在 WebView 中仍有性能瓶頸。使用 android-pdf-viewer PDF 組件:

  • 直接調(diào)用 Android 原生渲染能力
  • 支持流暢的縮放和翻頁(yè)
  • 字體渲染清晰
  • 內(nèi)存占用更低

三、技術(shù)架構(gòu)

3.1 整體架構(gòu)圖

┌──────────────────────────────────────────────────────────────────┐
│                          Vue2 前端                                │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │                    PdfJsViewer 組件                         │  │
│  │  ┌─────────────┐  ┌─────────────┐  ┌──────────────────┐    │  │
│  │  │ PDF.js v5   │  │ PDF.js v3   │  │ Android Bridge   │    │  │
│  │  │   (桌面端)   │  │   (移動(dòng)端)   │  │ 	(原生預(yù)覽)      │    │  │
│  │  └─────────────┘  └─────────────┘  └──────────────────┘    │  │
│  └────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘
                              │
                              │ JavaScript Bridge
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│                       Android WebView                            │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │                     WebActivity                            │  │
│  │  ┌───────────────────────┐  ┌───────────────────────────┐  │  │
│  │  │ @JavascriptInterface  │  │ android-pdf-viewer 插件    │  │  │
│  │  │ previewPdfByUrl       │  │ (全屏彈窗預(yù)覽)              │  │  │
│  │  └───────────────────────┘  └───────────────────────────┘  │  │
│  └────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘

3.2 技術(shù)棧

層級(jí)技術(shù)版本
前端框架Vue2^2.6.11
PDF 渲染 (桌面)PDF.js5.4.449
PDF 渲染 (移動(dòng))PDF.js3.11.174
Android 原生android-pdf-viewer3.2.0-beta.3
工具庫(kù)Hutool5.8.16

四、前端實(shí)現(xiàn)

4.1 PDF.js 部署

將兩個(gè)版本的 PDF.js 放到 Vue 項(xiàng)目的 public 目錄:

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/
│       └── ...

?? 說(shuō)明:直接將下載的 PDF.js 發(fā)布包解壓到 public 目錄即可。

下載地址

  • PDF.js: https://github.com/mozilla/pdf.js/releases

4.2 Vue PDF 預(yù)覽組件

文件路徑src/components/PdfJsViewer/index.vue

<template>
    <div class="pdfjs-viewer-container" :style="containerStyle">
        <!-- Loading 狀態(tài) -->
        <div v-if="loading" class="pdfjs-loading">
            <div class="loading-spinner"></div>
            <span class="loading-text">{{ loadingText }}</span>
        </div>

        <!-- 錯(cuò)誤狀態(tài) -->
        <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 組件預(yù)覽按鈕 -->
            <span v-if="isAndroidApp" class="native-preview-btn" @click="openWithNativeViewer">
                查看原圖
            </span>

            <!-- PDF 預(yù)覽 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 請(qǐng)求)
        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
            }
        },
        // 檢測(cè)是否為移動(dòng)端
        isMobile() {
            const userAgent = navigator.userAgent.toLowerCase()
            const mobileKeywords = ['android', 'iphone', 'ipad', 'ipod', 'windows phone', 'mobile']
            return mobileKeywords.some(keyword => userAgent.includes(keyword))
        },
        // 檢測(cè)是否為 Android App
        isAndroidApp() {
            const userAgent = navigator.userAgent.toLowerCase()
            return userAgent.includes('android') && window.android
        },
        viewerUrl() {
            if (!this.pdfBlobUrl) return ''
            // 根據(jù)設(shè)備類型選擇不同的 pdfjs 版本
            // 移動(dòng)端使用 pdfjs-v3,桌面端使用 pdfjs-v5
            const pdfjsPath = this.isMobile ? '/pdfjs-v3/web/viewer.html' : '/pdfjs-v5/web/viewer.html'
            // 將 Blob URL 作為參數(shù)傳給 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 數(shù)據(jù)
                    this.revokePdfData()
                }
            }
        }
    },
    methods: {
        /**
         * 獲取 PDF 文件流
         */
        async fetchPdfFile(previewUrl) {
            return request({
                url: previewUrl,
                method: 'get',
                responseType: 'arraybuffer'
            })
        },

        /**
         * 加載 PDF 文件
         */
        async loadPdf() {
            // 清理之前的 PDF 數(shù)據(jù)
            this.revokePdfData()

            this.loading = true
            this.error = null

            try {
                // 獲取文件流
                const response = await this.fetchPdfFile(this.url)

                // 檢查響應(yīng)數(shù)據(jù)
                if (!response || response.byteLength === 0) {
                    this.error = '獲取到的文件內(nèi)容為空'
                    this.$emit('load-error', new Error(this.error))
                    return
                }

                // 創(chuàng)建 Blob 對(duì)象
                const blob = new Blob([response], { type: 'application/pdf' })

                // 創(chuàng)建 Blob URL
                this.pdfBlobUrl = URL.createObjectURL(blob)

                this.$emit('load-success')

                // 啟動(dòng)字體問(wèn)題修復(fù)機(jī)制
                this.fixFontIssue()
            } catch (err) {
                console.error('PDF 加載失敗:', err)
                this.error = err.message || '加載PDF文件失敗,請(qǐng)重試'
                this.$emit('load-error', err)
            } finally {
                this.loading = false
            }
        },

        /**
         * 修復(fù)移動(dòng)端PDF字體顯示問(wèn)題
         */
        fixFontIssue() {
            // 只在移動(dòng)端執(zhí)行
            if (!this.isMobile) return

            // 最大重試次數(shù)
            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) {
                                // 等待頁(yè)面渲染完成后再進(jìn)行修復(fù)
                                app.pdfViewer.pagesPromise.then(() => {
                                    // 切換縮放模式觸發(fā)重新渲染
                                    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還未準(zhǔn)備好,稍后重試
                            attemptFix(retryCount + 1)
                        }
                    } catch (e) {
                        alert("字體修復(fù)嘗試失敗")
                        console.warn("字體修復(fù)嘗試失敗:", e)
                        if (retryCount < MAX_RETRY_COUNT) {
                            attemptFix(retryCount + 1)
                        }
                    }
                }, 500 * (retryCount + 1)) // 遞增延遲時(shí)間
            }

            attemptFix()
        },

        /**
         * 使用 Android PDF 預(yù)覽插件打開(kāi) PDF
         */
        async openWithNativeViewer() {
            if (!this.pdfBlobUrl) {
                this.$message.warning('PDF 文件未加載完成')
                return
            }

            if (!this.isAndroidApp) {
                this.$message.warning('當(dāng)前環(huán)境不支持 Android PDF 預(yù)覽插件')
                return
            }

            try {
                // 調(diào)用Android PDF預(yù)覽插件方法
                const success = this.previewPdfByNative(this.url)

                if (!success) {
                    this.$message.error({message: "打開(kāi) Android PDF 預(yù)覽插件失敗,請(qǐng)重試", offset: 80})
                }
            } catch (err) {
                console.error('打開(kāi) Android PDF 預(yù)覽插件失敗:', err)
                this.$message.error({message: "打開(kāi) Android PDF 預(yù)覽插件失敗,請(qǐng)重試", offset: 80})
            }
        },

        /**
         * 使用 Android PDF 預(yù)覽插件
         * @param {String} url - PDF 文件 URL
         * @returns {Boolean} 是否成功調(diào)用
         */
        previewPdfByNative(url) {
            if (!this.isAndroidApp || !window.android.previewPdfByUrl) {
                this.$message.warning({message: "'當(dāng)前環(huán)境不支持 Android PDF 預(yù)覽插件", 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('調(diào)用 Android PDF 預(yù)覽插件失敗:', err)
                return false
            }
        },

        /**
         * 釋放 PDF 數(shù)據(jù)
         */
        revokePdfData() {
            if (this.pdfBlobUrl) {
                // 組件銷毀時(shí)釋放 Blob URL
                URL.revokeObjectURL(this.pdfBlobUrl)
                this.pdfBlobUrl = null
            }
        }
    },
    beforeDestroy() {
        // 組件銷毀時(shí)釋放 PDF 數(shù)據(jù)
        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 預(yù)覽插件按鈕樣式 */
.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>

4.3 組件使用方式

<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>

五、Android 端實(shí)現(xiàn)

5.1 添加權(quán)限

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
    <!-- 網(wǎng)絡(luò)權(quán)限,用于下載 PDF 文件 -->
    <uses-permission android:name="android.permission.INTERNET" />
    
    <!-- ... -->
</manifest>

5.2 添加依賴

app/build.gradle

dependencies {
    // ... 其他依賴
    
    // PDF 預(yù)覽插件
    implementation 'com.github.mhiew:android-pdf-viewer:3.2.0-beta.3'
    
    // 工具庫(kù)(用于 HTTP 請(qǐng)求和文件操作)
    implementation 'cn.hutool:hutool-all:5.8.16'
}

注意:該庫(kù)托管在 JitPack,需要在項(xiàng)目根 build.gradle 中添加:

allprojects {
    repositories {
        // ...
        maven { url 'https://jitpack.io' }
    }
}

5.3 PDF 預(yù)覽彈窗布局

res/layout/dialog_pdf_preview.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">

    <!-- 標(biāo)題欄 -->
    <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 預(yù)覽"
            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="關(guān)閉"
            android:padding="8dp"
            android:src="@android:drawable/ic_menu_close_clear_cancel" />

    </RelativeLayout>

    <!-- PDF 預(yù)覽區(qū)域 -->
    <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>

5.4 WebActivity 核心代碼

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 接口,供前端調(diào)用
        mWebView.addJavascriptInterface(this, "android");
    }

    // ==================== PDF 預(yù)覽相關(guān)方法 ====================

    /**
     * JavaScript 接口:通過(guò) URL 預(yù)覽 PDF
     * @param url PDF 文件的下載地址
     * @param fileName 文件名(用于緩存)
     * @param headersJson 請(qǐng)求頭信息的 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 接口:通過(guò) Base64 數(shù)據(jù)預(yù)覽 PDF
     * @param base64Data PDF 文件的 Base64 編碼數(shù)據(jù)
     */
    @JavascriptInterface
    public void previewPdfByData(String base64Data) {
        runOnUiThread(() -> {
            if (TextUtils.isEmpty(base64Data)) {
                ToastUtils.showLong(this, "PDF 數(shù)據(jù)為空");
                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 文件并預(yù)覽
     */
    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("正在加載文件,請(qǐng)稍候...");

        // 在子線程中執(zhí)行下載
        new Thread(() -> {
            try {
                HttpRequest request = HttpRequest.get(url);
                // 添加請(qǐng)求頭
                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();
    }

    /**
     * 解析請(qǐng)求頭信息
     */
    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 數(shù)據(jù)并預(yù)覽
     */
    private void saveBase64PdfAndPreview(String base64Data) {
        AlertDialog loadingDialog = showLoadingDialog("正在加載文件,請(qǐng)稍候...");

        try {
            // 使用 Base64 數(shù)據(jù)的 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 數(shù)據(jù)處理失敗");
        }
    }

    /**
     * 顯示 PDF 預(yù)覽對(duì)話框
     */
    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();

        // 設(shè)置彈窗全屏顯示
        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 內(nèi)容
     */
    private void loadPdf(PDFView pdfView, File pdfFile, TextView tvPageInfo) {
        pdfView.fromFile(pdfFile)
                .enableSwipe(true)           // 啟用滑動(dòng)翻頁(yè)
                .swipeHorizontal(false)      // 垂直滑動(dòng)
                .enableDoubletap(true)       // 啟用雙擊縮放
                .defaultPage(0)              // 默認(rèn)顯示第一頁(yè)
                .enableAnnotationRendering(false)
                .enableAntialiasing(true)    // 啟用抗鋸齒
                .spacing(4)                  // 頁(yè)面間距
                .onPageChange((page, pageCount) -> {
                    tvPageInfo.setText(String.format("%d / %d", page + 1, pageCount));
                })
                .onLoad(nbPages -> {
                    tvPageInfo.setText(String.format("1 / %d", nbPages));
                    // 設(shè)置縮放范圍
                    pdfView.setMinZoom(0.5f);
                    pdfView.setMaxZoom(10.0f);
                    pdfView.setMidZoom(2f);
                })
                .onError(t -> {
                    ToastUtils.showLong(this, "PDF 加載失敗: " + t.getMessage());
                })
                .load();
    }
}

六、前端調(diào)用 Android PDF 組件方法

6.1 調(diào)用方式一:通過(guò) URL 預(yù)覽(推薦)

推薦使用此方式,由 Android 端負(fù)責(zé)下載和渲染,前端只需傳遞 URL 和請(qǐng)求頭。

// 檢測(cè)是否為 Android App 環(huán)境
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({                	 // 請(qǐng)求頭
            Authorization: 'Bearer xxx'
        })
    )
}

6.2 調(diào)用方式二:通過(guò) Base64 數(shù)據(jù)預(yù)覽(不推薦)

?? 不推薦使用此方式

原因:前端使用 btoa() 將大文件轉(zhuǎn)換為 Base64 時(shí)會(huì)導(dǎo)致瀏覽器卡死,100MB+ 的文件基本無(wú)法處理。

此接口僅適用于小文件(< 5MB)場(chǎng)景。

// 獲取 PDF 的 ArrayBuffer
const response = await fetch(pdfUrl)
const arrayBuffer = await response.arrayBuffer()

// ?? 大文件會(huì)導(dǎo)致瀏覽器卡死!
const base64Data = btoa(
    new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '')
)

// 調(diào)用 Android 原生方法
if (window.android?.previewPdfByData) {
    window.android.previewPdfByData(base64Data)
}

七、緩存策略優(yōu)化

為了避免重復(fù)下載相同的 PDF 文件,我使用 MD5 哈希 作為緩存文件名:

場(chǎng)景緩存鍵說(shuō)明
URL 方式(推薦)MD5(url) + ".pdf"相同 URL 只下載一次
Base64 方式MD5(base64Data) + ".pdf"僅適用于小文件

八、實(shí)現(xiàn)效果

8.1 桌面端瀏覽器 PDF 預(yù)覽(PDF.js v5)

效果說(shuō)明

  • 桌面端使用 PDF.js v5,功能完善
  • 支持縮放、翻頁(yè)、搜索等功能
  • 縮放流暢、放大后字體依然清晰

8.2 Android WebView PDF 預(yù)覽(PDF.js v3)

效果說(shuō)明

  • Android WebView 使用 PDF.js v3 以保證兼容性
  • 支持手勢(shì)縮放、翻頁(yè)、搜索等功能
  • 大文件縮放時(shí)可能存在卡頓、放大后字體不清晰
  • 右上角提供「查看原圖」按鈕,點(diǎn)擊后以彈窗方式打開(kāi) Android PDF 組件預(yù)覽

8.3 Android PDF 組件預(yù)覽(android-pdf-viewer)

效果說(shuō)明

  • 全屏彈窗預(yù)覽,沉浸式體驗(yàn)
  • 頂部顯示頁(yè)碼信息(如 1 / 1)
  • 右上角關(guān)閉按鈕,操作便捷
  • 支持雙指縮放
  • 放大后字體渲染清晰,縮放流暢無(wú)卡頓

8.4 使用體驗(yàn)對(duì)比

指標(biāo)WebView + PDF.jsAndroid PDF 組件
縮放流暢度大文件卡頓明顯,甚至卡死絲滑流暢
字體清晰度放大后模糊始終清晰
內(nèi)存占用較高,易 OOM較低,穩(wěn)定

?? 以上為實(shí)際使用中的主觀體驗(yàn)對(duì)比,非精確測(cè)量數(shù)據(jù)。

九、總結(jié)

本文介紹了在 Vue + Android WebView 環(huán)境下預(yù)覽大文件 PDF 的完整解決方案:

  1. PDF.js 多版本策略:桌面端用 v5,移動(dòng)端用 v3,解決兼容性問(wèn)題
  2. Android PDF 組件:使用 android-pdf-viewer 實(shí)現(xiàn)高性能預(yù)覽
  3. JS Bridge 通信:通過(guò) @JavascriptInterface 實(shí)現(xiàn)前端與原生的交互
  4. MD5 緩存策略:避免重復(fù)下載,提升用戶體驗(yàn)

這套方案已在生產(chǎn)環(huán)境穩(wěn)定運(yùn)行,成功解決了大文件 PDF 在 PAD 上的預(yù)覽問(wèn)題。

十、參考資源

到此這篇關(guān)于Vue + Android WebView實(shí)現(xiàn)大文件PDF預(yù)覽完整解決方案的文章就介紹到這了,更多相關(guān)Vue Android WebView大文件PDF預(yù)覽內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

小金县| 临漳县| 房产| 陇南市| 浦县| 定安县| 通州市| 龙胜| 靖宇县| 台州市| 盐边县| 藁城市| 湟中县| 讷河市| 张北县| 东阿县| 图木舒克市| 佛冈县| 通渭县| 孟村| 雷波县| 高安市| 吉隆县| 玉树县| 巩留县| 璧山县| 五常市| 南郑县| 应城市| 东明县| 中山市| 海原县| 桃源县| 阿勒泰市| 普定县| 新营市| 特克斯县| 宜春市| 平乐县| 鹤峰县| 碌曲县|