Android本地浏览PDF(Android PDF.js 简要学习手册)

环境

Min SDK: 21

依赖:

gradle 复制代码
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1"
implementation "androidx.webkit:webkit:1.12.0"

权限:

xml 复制代码
<uses-permission android:name="android.permission.INTERNET" />

下载pdf.js :https://github.com/mozilla/pdf.js/tree/v2.10.377/web

引入步骤

plain 复制代码
app/src/main/assets/pdfjs/
├── web/
│   ├── viewer.html
│   ├── viewer.js
│   └── ...
├── build/
│   ├── pdf.js
│   ├── pdf.worker.js
│   └── ...

实现

kotlin 复制代码
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.WindowManager
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.content.ContextCompat
import androidx.webkit.WebViewAssetLoader
import com.gyf.immersionbar.ImmersionBar
import com.sunward.markettools.R
import com.sunward.markettools.base.BaseActivity
import com.sunward.markettools.databinding.ActivityWebviewBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.net.URL
import java.net.URLEncoder
import java.nio.channels.Channels

/**
 *@dateTime:2025/4/29
 *@作者:MaoYoung
 */
class WebViewPDFActivity : BaseActivity<MarketViewModel, ActivityWebviewBinding>(
    R.layout.activity_webview
) {
    companion object {
        fun newInstance(activity: Context?, url: String, fileName: String) {
            activity?.startActivity(Intent(activity, WebViewPDFActivity::class.java).apply {
                putExtra(activity.getString(R.string.params_web_url), url)
                putExtra(activity.getString(R.string.params_file_name), fileName)
            })
        }
    }

    private val coroutineScope = MainScope()
    private val domain = "appassets.androidplatform.net"
    override fun initView(savedInstanceState: Bundle?) {
        val pdfUrl =
            intent.getStringExtra(getString(R.string.params_web_url)) ?: ""
        val fileName = intent.getStringExtra(getString(R.string.params_file_name)) ?: ""
        window.setFlags(
            WindowManager.LayoutParams.FLAG_SECURE,
            WindowManager.LayoutParams.FLAG_SECURE
        )
        mBinding.apply {
            tvTitle.text = fileName
            toolbar.setNavigationOnClickListener { finishActivity() }
        }

        val downloadDir = saveDownloadFilePath()

        val assetLoader = WebViewAssetLoader.Builder()
            .setDomain(domain)
            .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(this))
            .addPathHandler(
                "/Download/",
                WebViewAssetLoader.InternalStoragePathHandler(this, downloadDir)
            )
            .build()
        mBinding.apply {
            webview.apply {
                settings.apply {
                    javaScriptEnabled = true
                    domStorageEnabled = true
                    allowFileAccess = false // 不再需要 file 访问
                }
                //设置WebViewClient与AssetLoader
                webViewClient = object : WebViewClient() {
                    override fun shouldInterceptRequest(
                        view: WebView?,
                        request: WebResourceRequest?
                    ): android.webkit.WebResourceResponse? {
                        return assetLoader.shouldInterceptRequest(request?.url!!)
                    }

                    override fun shouldOverrideUrlLoading(
                        view: WebView?,
                        request: WebResourceRequest?
                    ): Boolean {
                        return super.shouldOverrideUrlLoading(view, request)
                    }

                    override fun onPageFinished(view: WebView?, url: String?) {
                        view?.loadUrl(
                            "javascript:(function() {" +
                                    "var rightToolbar = document.getElementById('toolbarViewerRight');" +
                                    "if (rightToolbar) { rightToolbar.style.display = 'none'; }" +
                                    "})()"
                        )
                    }

                    override fun onReceivedError(
                        view: WebView?,
                        errorCode: Int,
                        description: String?,
                        failingUrl: String?
                    ) {
                        Log.e("WebViewActivity", "Error: $description, URL: $failingUrl")
                    }
                }

                webChromeClient = WebChromeClient()


                downloadAndDisplayPdf(pdfUrl, fileName)
            }
        }
    }

    private fun saveDownloadFilePath(): File {
        val downloadDir = File(this.filesDir, "Download")
        if (!downloadDir.exists()) {
            downloadDir.mkdirs()
        }
        return downloadDir
    }

    private fun downloadAndDisplayPdf(pdfUrl: String, fileName: String) {
        coroutineScope.launch {
            try {
                val finalFileName = if (fileName.endsWith(".pdf")) fileName else "$fileName.pdf"

                val downloadedFileName =
                    withContext(Dispatchers.IO) {
                        val downloadDir = saveDownloadFilePath()
                        val file = File(downloadDir, finalFileName)
                        if (file.exists()) file.delete()
                        val url = URL(pdfUrl)
                        val connection = url.openConnection()
                        connection.connect()
                        val inputStream = connection.getInputStream()
                        FileOutputStream(file).use { output ->
                            Channels.newChannel(inputStream).use { inputChannel ->
                                output.channel.use { outputChannel ->
                                    outputChannel.transferFrom(inputChannel, 0, Long.MAX_VALUE)
                                }
                            }
                        }
                        finalFileName
                    }

                val encodedPath = URLEncoder.encode("$downloadedFileName", "UTF-8")
                val viewerUrl =
                    "https://$domain/assets/pdfjs/web/viewer.html?file=%2FDownload%2F$encodedPath"
                mBinding.webview.loadUrl(viewerUrl)
            } catch (e: Exception) {
                e.printStackTrace()
                val encodedUrl = try {
                    URLEncoder.encode(pdfUrl, "UTF-8")
                } catch (e: Exception) {
                    pdfUrl
                }
                val fallbackUrl = "file:///android_asset/pdfjs/web/viewer.html?file=$encodedUrl"
                mBinding.webview.loadUrl(fallbackUrl)
            }
        }
    }


    override fun onResume() {
        super.onResume()
        mBinding.webview.onResume()
    }

    override fun onPause() {
        super.onPause()
        mBinding.webview.onPause()
    }

    override fun onDestroy() {
        mBinding.webview.destroy()
        coroutineScope.cancel()
        super.onDestroy()
    }

    override fun initImmersionBar() {
        ImmersionBar.with(this)
            .statusBarColorTransformEnable(false)
            .keyboardEnable(false)
            .statusBarDarkFont(true)
            .navigationBarDarkIcon(true)
            .fitsSystemWindowsInt(true, ContextCompat.getColor(this, R.color.color_fa))
            .navigationBarColor(R.color.color_fa)
            .init()
    }
}
kotlin 复制代码
WebViewActivity.newInstance(context, "这里传递下载PDF的地址")

工作原理

  1. 初始化

    • 从intent中获取PDF 的URL
    • 配置域名: .setDomain(domain)
  2. 下载

    使用协成下载到/Download/文件夹中

  3. 显示

    将文件加载到本地PDF路径(file://.../temp.pdf)的"https://$domain/assets/pdfjs/web/viewer.html

相关推荐
一 乐42 分钟前
婚纱摄影网站|基于ssm + vue婚纱摄影网站系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot·后端
xkxnq3 小时前
第二阶段:Vue 组件化开发(第 16天)
前端·javascript·vue.js
Van_Moonlight3 小时前
RN for OpenHarmony 实战 TodoList 项目:空状态占位图
javascript·开源·harmonyos
xkxnq3 小时前
第一阶段:Vue 基础入门(第 15天)
前端·javascript·vue.js
贺biubiu3 小时前
2025 年终总结|总有那么一个人,会让你千里奔赴...
android·程序员·年终总结
xuekai200809013 小时前
mysql-组复制 -8.4.7 主从搭建
android·adb
nono牛5 小时前
ps -A|grep gate
android
BBBBBAAAAAi5 小时前
Claude Code安装记录
开发语言·前端·javascript
源码获取_wx:Fegn08955 小时前
基于 vue智慧养老院系统
开发语言·前端·javascript·vue.js·spring boot·后端·课程设计
Jing_Rainbow6 小时前
【 前端三剑客-37 /Lesson61(2025-12-09)】JavaScript 内存机制与执行原理详解🧠
前端·javascript·程序员