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

相关推荐
zttbee6 小时前
echarts图表tooltip色块失效
前端·javascript·echarts
虚无境8 小时前
如何用AI帮我写出实用的插件
前端·javascript·ai编程
zimoyin9 小时前
在 C# 里嵌入 JavaScript:用 Jint 做可热更新的自动化脚本(从入门到踩坑)
javascript·c#·自动化
FRing0310 小时前
JavaScript入门到精通二十六(变量提升、函数剩余参数、展开运算符、ES6箭头函数)
开发语言·前端·javascript·es6·箭头函数
xiaoshiquan120611 小时前
Gradle 复合构建:宿主项目直接依赖 SDK 源码模块的配置实践
android
OpsEye11 小时前
怎么实现大模型的按用户、按应用配额限流?
javascript·ai编程
云舜言传12 小时前
Bloom 手势演示操作 · 录屏指南
javascript·深度学习·机器学习
weixin_4487717213 小时前
使用Aspose给PDF添加水印
java·pdf
小师兄吃牛肉13 小时前
Android Studio 2025 专业介绍:新特性、性能与开发体验全面解析
android·ide·android studio
美摄科技13 小时前
安卓离线类美颜特效SDK
android