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

相关推荐
killerbasd40 分钟前
牧苏苏传 我不装了 4/7
前端·javascript·vue.js
橘子编程2 小时前
JavaScript与TypeScript终极指南
javascript·ubuntu·typescript
叫我一声阿雷吧2 小时前
JS 入门通关手册(45):浏览器渲染原理与重绘重排(性能优化核心,面试必考
javascript·前端面试·前端性能优化·浏览器渲染·浏览器渲染原理,重排重绘·reflow·repaint
大家的林语冰2 小时前
《前端周刊》尤大开源 Vite+ 全家桶,前端工业革命启动;尤大爆料 Void 云服务新产品,Vite 进军全栈开发;ECMA 源码映射规范......
前端·javascript·vue.js
jiayong233 小时前
第 8 课:开始引入组合式函数
前端·javascript·学习
天若有情6734 小时前
【C++原创开源】formort.h:一行头文件,实现比JS模板字符串更爽的链式拼接+响应式变量
开发语言·javascript·c++·git·github·开源项目·模版字符串
zh_xuan4 小时前
Android Hilt实现依赖注入
android·hilt
yuki_uix4 小时前
重排、重绘与合成——浏览器渲染性能的底层逻辑
前端·javascript·面试
freshman_y4 小时前
Qtcreator怎么新建安卓项目?编写一个五子棋游戏APP?
android·qt
止观止5 小时前
拥抱 ESNext:从 TC39 提案到生产环境中的现代 JS
开发语言·javascript·ecmascript·esnext