睿诚科技协会

android 网络 pdf

核心流程概览

无论使用哪种方案,基本流程都遵循以下步骤:

android 网络 pdf-图1
(图片来源网络,侵删)
  1. 发起网络请求:使用 HTTP 客户端(如 OkHttp)从指定的 URL 获取 PDF 文件。
  2. 处理响应流:不要一次性将整个 PDF 文件加载到内存中,而是以流的形式逐步读取数据,以避免 OutOfMemoryError
  3. 渲染或保存 PDF
    • 方案一(推荐):直接在 App 内渲染显示,使用 PDF 渲染库(如 AndroidPdfViewerPdfRenderer)将流中的数据解码并绘制到 View 上。
    • 下载到本地存储,将网络流写入到设备的内部或外部存储中,然后使用系统或其他应用打开它。
    • 分享或通过 Intent 打开,类似于方案二,但完成后会触发一个分享或打开动作。

直接在 App 内部渲染显示 PDF (最佳用户体验)

这是最常见的需求,用户在 App 内就能直接浏览 PDF,无需下载。

步骤 1:添加依赖

在你的 app/build.gradle 文件中添加必要的库,我们推荐使用 OkHttp 进行网络请求,AndroidPdfViewer 进行渲染(因为它简单易用)。

// app/build.gradle
dependencies {
    // OkHttp for networking
    implementation("com.squareup.okhttp3:okhttp:4.12.0") // 使用最新版本
    // AndroidPdfViewer for rendering PDF
    implementation("com.github.barteksc:android-pdf-viewer:3.2.0-beta.1") // 使用最新版本
}

步骤 2:添加网络权限

app/src/main/AndroidManifest.xml 中添加网络权限。

<manifest ...>
    <uses-permission android:name="android.permission.INTERNET" />
    <application ...>
        ...
    </application>
</manifest>

步骤 3:布局文件

activity_main.xml 或其他布局文件中添加 PDFView

android 网络 pdf-图2
(图片来源网络,侵删)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <com.github.barteksc.pdfviewer.PDFView
        android:id="@+id/pdfView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

步骤 4:编写 Java/Kotlin 代码

MainActivity.kt 中,从网络加载 PDF 并显示。

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.github.barteksc.pdfviewer.PDFView
import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener
import com.github.barteksc.pdfviewer.listener.OnPageChangeListener
import com.github.barteksc.pdfviewer.scroll.DefaultScrollHandle
import com.shockwave.pdfium.PdfDocument
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.BufferedInputStream
import java.io.IOException
import java.io.InputStream
class MainActivity : AppCompatActivity(), OnPageChangeListener, OnLoadCompleteListener {
    private lateinit var pdfView: PDFView
    private val pdfUrl = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" // 示例 PDF
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        pdfView = findViewById(R.id.pdfView)
        title = "PDF Viewer"
        // 从网络加载 PDF
        loadPdfFromUrl()
    }
    private fun loadPdfFromUrl() {
        // 在 IO 线程执行网络请求
        val client = OkHttpClient()
        val request = Request.Builder().url(pdfUrl).build()
        client.newCall(request).enqueue(object : okhttp3.Callback {
            override fun onFailure(call: okhttp3.Call, e: IOException) {
                // 在主线程更新 UI
                runOnUiThread {
                    Log.e("MainActivity", "Failed to load PDF", e)
                    // 可以在这里显示一个错误提示
                }
            }
            override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
                if (!response.isSuccessful) {
                    runOnUiThread {
                        Log.e("MainActivity", "Unexpected code $response")
                    }
                    return
                }
                val inputStream: InputStream = response.body!!.byteStream()
                // 使用 BufferedInputStream 提高性能
                val bufferedInputStream = BufferedInputStream(inputStream)
                // 在主线程渲染 PDF
                runOnUiThread {
                    pdfView.fromStream(bufferedInputStream)
                        .defaultPage(0) // 设置默认显示第一页
                        .enableSwipe(true) // 允许滑动
                        .swipeHorizontal(false) // 禁止横向滑动
                        .onPageChange(this@MainActivity) // 页面变化监听
                        .onLoad(this@MainActivity) // 加载完成监听
                        .enableDoubletap(true) // 允许双击缩放
                        .password(null) // 设置密码(如果有)
                        .scrollHandle(DefaultScrollHandle(this@MainActivity)) // 滚动条
                        .load()
                }
            }
        })
    }
    override fun onPageChanged(page: Int, pageCount: Int) {
        title = String.format("%s / %s", page + 1, pageCount)
    }
    override fun loadComplete(nbPages: Int) {
        val metaInfo = pdfView.documentMeta
        Log.i("MainActivity", "Page loaded, $nbPages pages")
        // 可以在这里获取 PDF 的元数据
        Log.i("MainActivity", "Title: ${metaInfo.title}")
        Log.i("MainActivity", "Author: ${metaInfo.author}")
        Log.i("MainActivity", "Subject: ${metaInfo.subject}")
        Log.i("MainActivity", "Keywords: ${metaInfo.keywords}")
        Log.i("MainActivity", "Creator: ${metaInfo.creator}")
        Log.i("MainActivity", "Producer: ${metaInfo.producer}")
        Log.i("MainActivity", "Creation Date: ${metaInfo.creationDate}")
        Log.i("MainActivity", "Modified Date: ${metaInfo.modifiedDate}")
    }
}

关键点解释

  • OkHttpenqueue:这是异步请求,不会阻塞主线程。
  • BufferedInputStream:包装网络输入流,可以显著提高 I/O 性能。
  • runOnUiThread:网络请求的回调在后台线程执行,而更新 UI(如 pdfView.fromStream(...))必须在主线程进行。
  • 流式处理pdfView.fromStream() 直接接收一个 InputStream,它会自己负责从流中读取数据并渲染,完美地避免了内存问题。

下载 PDF 到本地存储

如果用户需要离线查看或分享 PDF,就需要先下载到本地。

步骤 1:添加存储权限

对于 Android 13 (API 33) 及以上版本,需要请求 READ_MEDIA_IMAGES 权限来访问共享存储中的文件,对于更早的版本,WRITE_EXTERNAL_STORAGE 即可,这里为了简单,我们使用 Context.getExternalFilesDir(),它不需要任何运行时权限。

android 网络 pdf-图3
(图片来源网络,侵删)

步骤 2:编写下载代码

import android.os.Environment
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
class DownloadActivity : AppCompatActivity() {
    private val pdfUrl = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
    private lateinit var downloadButton: Button
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_download)
        downloadButton = findViewById(R.id.downloadButton)
        downloadButton.setOnClickListener {
            downloadPdf()
        }
    }
    private fun downloadPdf() {
        val client = OkHttpClient()
        val request = Request.Builder().url(pdfUrl).build()
        client.newCall(request).enqueue(object : okhttp3.Callback {
            override fun onFailure(call: okhttp3.Call, e: IOException) {
                runOnUiThread {
                    Toast.makeText(this@DownloadActivity, "Download Failed", Toast.LENGTH_SHORT).show()
                    Log.e("DownloadActivity", "Download failed", e)
                }
            }
            override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
                if (!response.isSuccessful) {
                    runOnUiThread {
                        Toast.makeText(this@DownloadActivity, "Download Failed: ${response.code}", Toast.LENGTH_SHORT).show()
                    }
                    return
                }
                val inputStream: InputStream = response.body!!.byteStream()
                val outputFile = File(getExternalFilesDir(null), "downloaded_file.pdf")
                try {
                    val outputStream = FileOutputStream(outputFile)
                    inputStream.copyTo(outputStream) // Kotlin 扩展函数,方便复制流
                    outputStream.close()
                    inputStream.close()
                    runOnUiThread {
                        Toast.makeText(this@DownloadActivity, "Downloaded to ${outputFile.absolutePath}", Toast.LENGTH_LONG).show()
                        // 下载完成后,可以调用系统应用打开
                        openPdfWithSystemApp(outputFile)
                    }
                } catch (e: IOException) {
                    runOnUiThread {
                        Toast.makeText(this@DownloadActivity, "Failed to save file", Toast.LENGTH_SHORT).show()
                        Log.e("DownloadActivity", "Failed to save file", e)
                    }
                }
            }
        })
    }
    private fun openPdfWithSystemApp(file: File) {
        val intent = Intent(Intent.ACTION_VIEW).apply {
            setDataAndType(file.toUri(), "application/pdf")
            flags = Intent.FLAG_ACTIVITY_NO_HISTORY
        }
        val chooser = Intent.createChooser(intent, "Open With")
        startActivity(chooser)
    }
}

关键点解释

  • getExternalFilesDir(null):这是一个很好的选择,它返回一个应用专有的外部存储目录(Android/data/你的包名/files/),这个目录不需要运行时权限,并且在应用卸载时会自动删除。
  • inputStream.copyTo(outputStream):Kotlin 提供的便捷函数,用于将一个输入流的所有内容复制到输出流。
  • ACTION_VIEW Intent:这是标准的 Android Intent,用于“查看”某个类型的数据,系统会自动弹出一个选择器,让用户选择用哪个应用(如 Chrome、Adobe Reader 等)来打开这个 PDF 文件。

使用 PdfRenderer (更底层,更灵活)

AndroidPdfViewer 是一个很好的封装库,但如果你需要更底层的控制(将 PDF 的某一页绘制到 Canvas 上),可以使用 Android SDK 自带的 PdfRenderer

注意PdfRenderer 只能处理已经存在于本地文件系统中的 PDF 文件,你必须先通过网络下载 PDF 到一个临时文件,然后再用 PdfRenderer 打开它。

// ... (网络下载部分与方案二相同,先下载到文件)
// 假设你已经下载好了 PDF 文件,文件路径为 pdfFile
// 在 Activity 或 Fragment 中使用 PdfRenderer
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    val pdfRenderer = PdfRenderer(pdfFile)
    // 为每一页创建一个 Bitmap
    val page = pdfRenderer.openPage(0)
    val width = resources.displayMetrics.densityDpi * 5 // 设置一个宽度
    val height = page.height * width / page.width
    val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    // 将页面渲染到 Bitmap 上
    page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
    // 将 Bitmap 显示到 ImageView 上
    findViewById<ImageView>(R.id.pdfPageImageView).setImageBitmap(bitmap)
    // 记得关闭页面和渲染器
    page.close()
    pdfRenderer.close()
}

适用场景

  • 需要预览 PDF 的某一页,而不是整个文档。
  • 需要将 PDF 内容作为纹理或背景绘制到自定义的 View 上。
  • 对性能有极致要求,希望避免第三方库的开销。

总结与最佳实践

特性 方案一: AndroidPdfViewer 方案二: 下载后打开 方案三: PdfRenderer
实现难度 简单 简单 中等
用户体验 最佳 (无缝内嵌) 一般 (需要切换应用) 一般 (需要自定义 UI)
内存管理 优秀 (流式处理) 优秀 (文件存储) 优秀 (按需渲染)
灵活性
适用场景 在线阅读器、文档预览 离线下载、分享功能 自定义 PDF 渲染、单页预览

推荐选择

  • 如果你的 App 需要一个完整的 PDF 阅读器功能,直接使用 方案一 (AndroidPdfViewer),它为你处理了所有复杂的事情,是最高效、最可靠的选择。
  • 如果你的 App 只是偶尔需要打开一个 PDF,或者需要提供下载/分享功能,使用 方案二 (下载 + Intent)
  • 如果你有非常特殊的需求,比如将 PDF 页面作为游戏背景或进行复杂的图像处理,那么深入研究 方案三 (PdfRenderer) 是必要的。

其他注意事项

  • 大文件处理:对于非常大的 PDF 文件,确保始终使用流式处理,避免一次性加载到内存。
  • 错误处理:网络请求可能会失败,PDF 可能已损坏或受密码保护,务必做好 try-catch 和错误提示。
  • 生命周期:在 ActivityonDestroyonPause 中,如果可能,应该取消正在进行的网络请求,以避免内存泄漏和无效的回调。
分享:
扫描分享到社交APP
上一篇
下一篇