环境
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的地址")
工作原理
-
初始化
- 从intent中获取PDF 的URL
- 配置域名: .setDomain(domain)
-
下载
使用协成下载到/Download/文件夹中
-
显示
将文件加载到本地PDF路径(file://.../temp.pdf)的"https://$domain/assets/pdfjs/web/viewer.html