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

相关推荐
李少兄6 分钟前
JavaScript 数据类型完全指南
开发语言·javascript·ecmascript
tianshi48511 小时前
Android 应用启动窗口(Splash Screen)的创建与销毁流程
android
脚踏实地,坚持不懈!1 小时前
ICU Calendar 实际工作问题排查手册
android
ii_best2 小时前
安卓脚本/ios开发软件按键精灵实战:自动定位弹窗广告关闭按钮坐标的通用方案
android·ios·自动化
天空之城--2 小时前
Android Jetpack Compose 状态管理浅析
android·android jetpack
、如果3 小时前
PDF图片文字提取零依赖方案:pymupdf+AI视觉实战
人工智能·数据分析·pdf·图片提取·文字提取·skills
shmily麻瓜小菜鸡3 小时前
JS/TS 易踩坑知识点 — 模块化与工程化类
开发语言·javascript·ecmascript
默_笙5 小时前
🍕 AI 的嘴巴装了水管(上):从"等它说完"到"边说边听"的流式输出指南
前端·javascript
SamChan906 小时前
PDF翻译后的格式完整性校验:用Python自动比对译文与原文档的表格与段落结构
开发语言·python·ai·pdf·机器翻译
暖焰核心6 小时前
继承全解——继承、默认成员函数、切片、隐藏与虚继承
java·前端·javascript