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

相关推荐
SuperEugene2 分钟前
Vue3 + Element Plus 表格实战:批量操作、行内编辑、跨页选中逻辑统一|表单与表格规范篇
开发语言·前端·javascript
极梦网络无忧26 分钟前
基于 Vite + Vue3 的组件自动注册功能
前端·javascript·vue.js
没有了遇见36 分钟前
Android 架构之网络框架多域名配置<三>
android
软弹1 小时前
深入理解 React Ref 机制:useRef 与 forwardRef 的协作原理
前端·javascript·react.js
雪碧聊技术1 小时前
前端vue代码架子搭建
前端·javascript·vue.js·前端项目代码框架搭建
han_1 小时前
JavaScript设计模式(二):策略模式实现与应用
前端·javascript·设计模式
x***r1511 小时前
Notepad++ 8.6 安装教程:详细步骤+自定义安装路径(附注意事项)
linux·前端·javascript
Hilaku1 小时前
为什么很多工作 5 年的前端,身价反而卡住了?🤷‍♂️
前端·javascript·面试
执行部之龙2 小时前
JS手写——call bind apply
前端·javascript
myloveasuka2 小时前
[Java]单列集合
android·java·开发语言