Kotlin 第二弹:Android 中 PDF 创建与渲染实践

时间:2021-09-06 20:04:49

这是 Kotlin 练习的的第二篇。这一篇的由来是因为刚刚在 Android 开发者官网查看 API 的时候,偶然看到了角落里面的 pdf 相关。
Kotlin 第二弹:Android 中 PDF 创建与渲染实践
我仔细看看了详细文档,发现这个还蛮有意思的,关键是编码流程很简单。所以就想写篇博客记录备忘一下。本来是用 Java 实现的,后来想到最近自己也在熟悉 Kotlin,于是索性就改成 Kotlin 来实现了。

但是,我一起认为编程最重要的是编程思想,不管 Java 也好,Kotlin 也好,都是为了实现功能的。而本文的主要目的是介绍在 Android 如何创建 PDF 文件。而在实现的过程中,大家可以见识到一些常见的 Kotlin 用法,特别的地方我会稍微讲解一下。比如难于理解的 lambda 表达式我有在代码中运用,然后文中会做比较详细的解释。

准备

用 Kotlin 开发之前,首先得准备语言环境,大家在 Android Studio 安装 Kotlin 的插件,然后重启就好了。这个我不作过多的说明。

接下来就是要引入相关的依赖。我直接张贴我的 build.gradle 文件好了。
顶层 build.gradle

buildscript {
ext.support_version = '25.0.1'
ext.kotlin_version = '1.1.2'
ext.anko_version = '0.8.2'
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

然后是模块部分的 build.gradle

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 25
buildToolsVersion "24.0.1"
defaultConfig {
applicationId "com.frank.pdfdemo"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile "com.android.support:appcompat-v7:$support_version"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
compile "org.jetbrains.anko:anko-common:$anko_version"
testCompile 'junit:junit:4.12'
}

这是最基础的内容,我不说太多,接下来进入主题。

Android PDF 相关 API

Android SDK 中提供的 PDF 相关类分为两种,它们的作用分别是创建内容和渲染内容。通俗地讲就是一个是用来写 PDF 的,一个是用来展示 PDF 的。
Kotlin 第二弹:Android 中 PDF 创建与渲染实践

上面的线框图简单明了说明了各个功能相关联的类。我们先从 PDF 文件的创建开始。

需要注意的是,PdfDocument 这个类是在 API 19 的版本中添加的,所以设备必须是 4.4 版本以上。而 PdfRenderer 是在 API 21 的版本中添加的,同样要注意。

创建 PDF 文件

先看看官网的文档,上面有介绍基于 SDK 怎么样来创建 PDF 文件的流程。

//先创建一个 PdfDocument 对象 document
PdfDocument document = new PdfDocument();

//创建 PageInfo 对象,用于描述 PDF 中单个的页面
PageInfo pageInfo = new PageInfo.Builder(new Rect(0, 0, 100, 100), 1).create();

//开始启动内容填写
Page page = document.startPage(pageInfo);

//绘制页面,主要是从 page 中获取一个 Canvas 对象。
View content = getContentView();
content.draw(page.getCanvas());

//停止对页面的填写
document.finishPage(page);
. . .
// 加入更多的 page
. . .
//将文件写入流
document.writeTo(getOutputStream());

//关闭流
document.close();

示例很详细,接下来我们就可以参考这个流程进行代码的编写。

首先,确定我们要生成一个什么样子的 PDF。因为是做试验用的,所以简单一点,第一页将 MainActivity 的界面截取到 PDF 文件的第 1 页,之后连续写 10 页,每一页画一个圆形,然后绘制一条固定的语句。

我们可以在 MainActivity 的布局文件中随意弄一些布局。
Kotlin 第二弹:Android 中 PDF 创建与渲染实践

注意布局中的那个按钮,当点击按钮后将生成 PDF 文件,由于生成 PDF 比较耗时,所以在生成过程中会弹出一个进度对话框,生成成功后将消失,然后打开生成的 PDF 文件。

好了,我们可以创建 Activity 了。

import  kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
private val CODE_WRITE_EXTERNAL = 1
var file : File? = null
var mPaint : Paint? = null
//
var dialog : ProgressDialog? = null
var screenWidth : Int = 0
var screenHeight : Int = 0

@RequiresApi(Build.VERSION_CODES.KITKAT)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

btn_test.setOnClickListener { testCreatPDF(activity_main) }

mPaint = Paint()
mPaint?.isAntiAlias = true
mPaint?.color = Color.RED

screenWidth = displayMetrics.widthPixels
screenHeight = displayMetrics.heightPixels

}



@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private fun creatPDF(view: View) {

if (dialog == null ) {
dialog = indeterminateProgressDialog ("正在创建 PDF 中,请稍后...")
}

dialog?.show()
async {
val document = PdfDocument()

val info = PdfDocument.PageInfo.Builder(
screenWidth,screenHeight, 1).create()

val page = document.startPage(info)

view.draw(page.canvas)

document.finishPage(page)
for (index in 0..10) {
val info1 = PdfDocument.PageInfo.Builder(
screenWidth,screenHeight,index).create()

val page1 = document.startPage(info1)
mPaint?.color = Color.RED
page1.canvas.drawCircle(100.0f,100.0f,20.0f,mPaint)
mPaint?.color = Color.BLACK
mPaint?.textSize = 36.0f
page1.canvas.drawText("Kotlin test create PDF page$index.",
20.0f,200.0f,mPaint)

document.finishPage(page1)

}


try {
document.writeTo(outputStream)
} catch (e: IOException) {
e.printStackTrace()
}
document.close()

uiThread { toast("生成pdf成功,路径:$file")
dialog?.dismiss()

}

// viewPDFByApp()

viewPDF()

}

}

}

上面的核心方法是 creatPDF(view: View) 它接收一个 View 对象的参数。在这之前,我得先讲一个小知识点。

大家可以注意到,我在 onCreate() 方法中并没有运用常见的 findViewById() 但是程序竟然没有报错。其实,我能够这样是因为我 import 了一个包。大家仔细看一下。

import  kotlinx.android.synthetic.main.activity_main.*

activity_main 正是布局文件。

<?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:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.frank.pdfdemo.MainActivity">


<CheckBox
android:text="CheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/radioButton"
android:layout_toRightOf="@+id/radioButton"
android:layout_toEndOf="@+id/radioButton"
android:layout_marginLeft="63dp"
android:layout_marginStart="63dp"
android:id="@+id/checkBox" />


<Button
android:id="@+id/btn_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:text="生成 PDF"
android:layout_marginLeft="70dp"
android:layout_marginStart="70dp"
android:layout_marginBottom="22dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />


<RatingBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/ratingBar"
android:layout_above="@+id/btn_test"
android:layout_alignRight="@+id/btn_test"
android:layout_alignEnd="@+id/btn_test" />


<RadioButton
android:text="RadioButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/radioButton"
android:layout_alignParentTop="true"
android:layout_alignLeft="@+id/ratingBar"
android:layout_alignStart="@+id/ratingBar" />


<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:ems="10"
android:layout_below="@+id/checkBox"
android:layout_alignLeft="@+id/radioButton"
android:layout_alignStart="@+id/radioButton"
android:layout_marginTop="35dp"
android:id="@+id/editText"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />


<CalendarView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/calendarView2"
android:layout_alignLeft="@+id/ratingBar"
android:layout_alignStart="@+id/ratingBar"
android:layout_above="@+id/ratingBar"
android:layout_below="@+id/editText"
android:layout_alignRight="@+id/checkBox"
android:layout_alignEnd="@+id/checkBox" />


</RelativeLayout>

最外层那个 RelativeLayout 的 id 是 activity_main,所以调用 creatPDF(view: View) 时这个 view 就是 activity_main,我的目的就是在 PDF 的第一页映射这个布局。聚集到核心方法 creatPDF(view: View) 上来,我们可以发现一些有趣的东西。

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private fun creatPDF(view: View) {


async {
val document = PdfDocument()

val info = PdfDocument.PageInfo.Builder(
screenWidth,screenHeight, 1).create()

val page = document.startPage(info)

view.draw(page.canvas)

document.finishPage(page)
for (index in 0..10) {
val info1 = PdfDocument.PageInfo.Builder(
screenWidth,screenHeight,index).create()

val page1 = document.startPage(info1)
mPaint?.color = Color.RED
page1.canvas.drawCircle(100.0f,100.0f,20.0f,mPaint)
mPaint?.color = Color.BLACK
mPaint?.textSize = 36.0f
page1.canvas.drawText("Kotlin test create PDF page$index.",
20.0f,200.0f,mPaint)

document.finishPage(page1)

}


try {
document.writeTo(outputStream)
} catch (e: IOException) {
e.printStackTrace()
}
document.close()

uiThread { toast("生成pdf成功,路径:$file")
dialog?.dismiss()

}

// viewPDFByApp()

viewPDF()

}

}

首先,是异步的调用。

async {
......

uiThread {......}
}

之前用 Java 开发 Android 的时候,异步调用通常是用 AsyncTask,但是比较难用。后来大家用 RxJava,感受好多了。现在 Kotlin 方便多了,用一个扩展函数 async 就可以搞定了。
async 其实是 Anko 库中实现的。我们在 build.gradle 引入了它的依赖。

Anko 提供了非常简单的 DSL 来处理异步任务,它满足大部分的需求。它提供了一个基本的 async 函数用于在其它线程执行代码,也可以选择通过调用 uiThread 的方式回到主线程。在子线程中执行请求。就这么简单。

lambda 表达式

在上面的代码中,我们还可以发现新的大陆:

btn_test.setOnClickListener { testCreatPDF(activity_main) }

这是 Kotlin 中 lambda 表达式的具体表现,上面的代码等同于

btn_test.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
testCreatPDF(activity_main)
}

})

上面的形式才是我们在 Java 中常见的形式,用 object 关键字表示匿名内部类,到这一点的时候,大家应该还可以看明白。

但是 Kotlin 神奇的地方在于,它可以对具有函数式接口( functional Java interface )进行优化。

函数式接口的定义其实很简单:任何接口,如果只包含唯一一个抽象方法,那么它就是一个函数式接口。

值得注意的是这个接口一定是 Java 接口。如果是在 kotlin 中编写这样一个接口却不能这样子,这个地方我被坑了好久。

public interface Test {
void t ( View view);
}

上面的 Test 就是一个函数式接口,因为它只有单个方法。在 Kotlin 中可以对这类进行优化,它能够将这类接口直接用一个函数替换。上面的接口优化结果如下:

// 假设我要创建一个 Test 接口的实现类,我可以这样
var test = Test { }

所以

btn_test.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
testCreatPDF(activity_main)
}

})


btn_test.setOnClickListener(View.OnClickListener { testCreatPDF(activity_main) })

上面两个是等同的。如果一个参数本身没有使用就可以省略。比如这个 v:View 并没有使用。

btn_test.setOnClickListener({ testCreatPDF(activity_main) })

如果函数最后一个参数是一个 lambda 表达式,则可以将它移动括号外。

btn_test.setOnClickListener(){ testCreatPDF(activity_main) }

最后,如果括号里面没有参数,也可以省略。

btn_test.setOnClickListener { testCreatPDF(activity_main) }

最终可以演变成了这个样子。代码是不是很精简。

现在可以对 lambda 进行一些简单总结
1 一个 lambda 表达式主要用来代替和精简匿名内部类的工作。
2 一个 lambda 表达式被 { } 包围。
3 一个 lambda 表达式通常是 { (T) -> Unit } 形式。箭头左边是参数,参数可选可以省略,右边是函数体。如果参数省略后,箭头也省略。

接下来回归主题,PDF 的制作。

val document = PdfDocument()

val info = PdfDocument.PageInfo.Builder(
screenWidth,screenHeight, 1).create()

val page = document.startPage(info)

view.draw(page.canvas)

document.finishPage(page)
for (index in 0..10) {
val info1 = PdfDocument.PageInfo.Builder(
screenWidth,screenHeight,index).create()

val page1 = document.startPage(info1)
mPaint?.color = Color.RED
page1.canvas.drawCircle(100.0f,100.0f,20.0f,mPaint)
mPaint?.color = Color.BLACK
mPaint?.textSize = 36.0f
page1.canvas.drawText("Kotlin test create PDF page$index.",
20.0f,200.0f,mPaint)

document.finishPage(page1)

}


try {
document.writeTo(outputStream)
} catch (e: IOException) {
e.printStackTrace()
}
document.close()

创建 PDF 主要流程:

  1. 创建 PdfDocument 对象。
  2. 为每一页准备 PageInfo。
  3. 调用 PdfDocument 的 startPage() 方法并传入 PageInfo 作为参数生成 Page 对象。
  4. 获取 Page 对象中的 Canvas 对象进入内容的绘制。
  5. 结束当前 Page 的绘制。
  6. 将 PdfDocument 保存到外部流中。
  7. 关闭 PdfDocument 对象。

PDF 文件生成验证

首先,设备下载一个能够读取 PDF 文件的第三方应用。然后编写调用这个应用的代码。当 PDF 文件生成后,申请打开这个文件,当然本文的后半部就是自己用代码实现 PDF 文件的渲染。调用第三方应用读取 PDF 文件的具体代码如下:

private fun viewPDFByApp() {
if (Build.VERSION.SDK_INT >= 24) {
try {
val m = StrictMode::class.java.getMethod("disableDeathOnFileUriExposure")
m.invoke(null)
} catch (e: Exception) {
e.printStackTrace()
}

}

var intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.setDataAndType(Uri.fromFile(file), "application/pdf")
startActivity(intent)
}

我们可以用 Intent.ACTION_VIEW 这个 action,然后设置它的 Uri 和 Type,这里的 Type 是 “application/pdf”,大家一看就懂。而由于模拟器是基于 7.0 版本的,直接这样操作会报错。这个 Bug 大家可以参考* 这个页面

好吧。为了防止大家忘记,再次张贴整个代码。

class MainActivity : AppCompatActivity() {
private val CODE_WRITE_EXTERNAL = 1
var file : File? = null
var mPaint : Paint? = null
var dialog : ProgressDialog? = null
var screenWidth : Int = 0
var screenHeight : Int = 0

@RequiresApi(Build.VERSION_CODES.KITKAT)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

btn_test.setOnClickListener { testCreatPDF(activity_main) }

mPaint = Paint()
mPaint?.isAntiAlias = true
mPaint?.color = Color.RED

screenWidth = displayMetrics.widthPixels
screenHeight = displayMetrics.heightPixels
}

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)

when (requestCode) {
CODE_WRITE_EXTERNAL ->

if (grantResults.size > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
creatPDF(activity_main)
} else {
toast("申请权限失败")
}
else -> {
}
}
}

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
fun testCreatPDF(view: View) {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
creatPDF(view)
} else {
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
CODE_WRITE_EXTERNAL)
}
} else {
creatPDF(view)
}

}

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private fun creatPDF(view: View) {

if (dialog == null ) {
dialog = indeterminateProgressDialog ("正在创建 PDF 中,请稍后...")
}

dialog?.show()
async {
val document = PdfDocument()

val info = PdfDocument.PageInfo.Builder(
screenWidth,screenHeight, 1).create()

val page = document.startPage(info)

view.draw(page.canvas)

document.finishPage(page)
for (index in 0..10) {
val info1 = PdfDocument.PageInfo.Builder(
screenWidth,screenHeight,index).create()

val page1 = document.startPage(info1)
mPaint?.color = Color.RED
page1.canvas.drawCircle(100.0f,100.0f,20.0f,mPaint)
mPaint?.color = Color.BLACK
mPaint?.textSize = 36.0f
page1.canvas.drawText("Kotlin test create PDF page$index.",
20.0f,200.0f,mPaint)

document.finishPage(page1)

}


try {
document.writeTo(outputStream)
} catch (e: IOException) {
e.printStackTrace()
}
document.close()

uiThread { toast("生成pdf成功,路径:$file")
dialog?.dismiss()

}

viewPDFByApp()

}

}



private fun viewPDFByApp() {
if (Build.VERSION.SDK_INT >= 24) {
try {
val m = StrictMode::class.java.getMethod("disableDeathOnFileUriExposure")
m.invoke(null)
} catch (e: Exception) {
e.printStackTrace()
}

}

var intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.setDataAndType(Uri.fromFile(file), "application/pdf")
startActivity(intent)
}

private val outputStream: OutputStream?
get() {
val root = Environment.getExternalStorageDirectory()
file = File(root, "test.pdf")
try {
val os = FileOutputStream(file)
return os
} catch (e: FileNotFoundException) {
e.printStackTrace()
}

return null
}
}

如果是在 6.0 以上系统,大家还要处理一下权限。可以看到最终生成的 PDF 文档会被保存为 SD 卡上的 test.pdf。至于有些人可能好奇的是 outputStream 变量,我把它形成一个 property 属性,然后复写了它的 get 方法,当它第一次调用时,get() 中的方法体就会执行,然后把结果缓存下来,第二次调用时就直接调用缓存了。

好的,下面我们来实际演练一下。
Kotlin 第二弹:Android 中 PDF 创建与渲染实践

可以观察到的是,PDF 文件确实是创建了,并且也将 MainActivity 中的布局映射到了第 1 页。并且总共生成了 12 页。

PDF 的渲染

上面例子中,PDF 文件的读取是依靠第三方应用实现的,现在我们要自己实现它。

文章开头的地方,已经说明了这一部分由 PdfRenderer 类来实现。官网上也有它的实现流程。

// create a new renderer
PdfRenderer renderer = new PdfRenderer(getSeekableFileDescriptor());


final int pageCount = renderer.getPageCount();
for (int i = 0; i < pageCount; i++) {
Page page = renderer.openPage(i);

// say we render for showing on the screen
page.render(mBitmap, null, null, Page.RENDER_MODE_FOR_DISPLAY);

// do stuff with the bitmap

// close the page
page.close();
}

// close the renderer
renderer.close();

相信大家一看就懂。主要核心思想就是通过 PdfRenderer 将每个 Page 的内容渲染在一个 Bitmap 上,有了这个 Bitmap 那么我们肯定能够在 Android 设备上显示了。我们新建一个 Activity 专门用来渲染。
activity_render.xml

<?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:id="@+id/activity_render"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.frank.pdfdemo.RenderActivity">

<ImageView
android:id="@+id/iv_render"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<Button
android:id="@+id/btn_prev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="上一页"/>

<Button
android:id="@+id/btn_next"
android:layout_toRightOf="@id/btn_prev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="下一页"/>


</RelativeLayout>

我们用一个 ImageView 来显示渲染出来的 bitmap。然后两个按钮分别来控制上一页和下一页。

然后,我们编写 Activity 的代码。

import  kotlinx.android.synthetic.main.activity_render.*

class RenderActivity : AppCompatActivity() {
val TAG : String = "RenderActivity"

var renderer : PdfRenderer? = null
var file : File? = null
var parcelfd : ParcelFileDescriptor? = null
var mBitmap : Bitmap? = null
var mPageCount : Int = 0
var mCurrentPage : Int = 0

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_render)

file = File(intent.getStringExtra("path"))
parcelfd = ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY)
btn_prev.setOnClickListener { renderPrev() }
btn_next.setOnClickListener { renderNext() }
mBitmap = Bitmap.createBitmap(displayMetrics.widthPixels,displayMetrics.heightPixels
,Bitmap.Config.ARGB_8888)

startRender()

show()
}

fun show() {
if (mBitmap != null ) {
iv_render.setImageBitmap(mBitmap)
} else {
Log.d(TAG,"no bitmap")
}
}

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun startRender() {

renderer = PdfRenderer(parcelfd)
mPageCount = renderer?.pageCount!!

renderPage()
}

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onDestroy() {
super.onDestroy()
renderer?.close()
}


@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun renderPrev() {
if (mCurrentPage > 0) mCurrentPage--
renderPage()
Log.d(TAG,"cp:$mCurrentPage,pcount:$mPageCount")
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun renderNext() {
if (mCurrentPage < mPageCount - 1) mCurrentPage++
renderPage()
Log.d(TAG,"cp:$mCurrentPage,pcount:$mPageCount")
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun renderPage() {
async {
val page = renderer?.openPage(mCurrentPage)

mBitmap = Bitmap.createBitmap(displayMetrics.widthPixels,displayMetrics.heightPixels
,Bitmap.Config.ARGB_8888)
page?.render(mBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
page?.close()
uiThread { show() }
}

}
}

我们在 onCreate() 方法中创建 PdfRenderer 对象,然后在 onDestroy() 方法中关闭它。
注意的是 PdfRenderer 构造方法接受的参数是一个 ParcelFileDescriptor 对象。所以,我们要将 pdf 路径创建的 File 对象转换成 ParcelFileDescriptor。

parcelfd = ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY)

整个 Activity 最核心的方法是 renderPage()

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun renderPage() {
async {
val page = renderer?.openPage(mCurrentPage)

mBitmap = Bitmap.createBitmap(displayMetrics.widthPixels,displayMetrics.heightPixels
,Bitmap.Config.ARGB_8888)
page?.render(mBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
page?.close()
uiThread { show() }
}

}

fun show() {
if (mBitmap != null ) {
iv_render.setImageBitmap(mBitmap)
} else {
Log.d(TAG,"no bitmap")
}
}

将 render 出来的 bitmap 显示在 ImageView 上就 OK 了。

PDF 渲染的验证

接下来,我们需要更改 MainActivity,之前生成 PDF 文件后是由第三方应用读取,现在我们要它的的文件路径传递给 RenderActivity。所以我们要增加一个方法。

private fun viewPDF() {
var intent = Intent(this@MainActivity,RenderActivity::class.java)
intent.putExtra("path",file?.absolutePath)
startActivity(intent)
}

这个时候就可以重样验证了,不过这次验证的问题的 PDF 能不能被我们自己编写的代码渲染成功。
Kotlin 第二弹:Android 中 PDF 创建与渲染实践

可以看到,没有问题。

总结

1. PDF 文件的生成与渲染其实在 Android 中非常简单,算是一个小技巧,大家花点时间就能掌握。两个核心类就是 PdfDocument 和 PdfRenderer。
2. 文章中代码语言是 kotlin,其实 Java 当然也可以了。
3. kotlin 中 lambda 表达式比较抽象,大家要多思考才能理解,总之它是用来精简替换匿名内部类的。
4. 文章例子只是 Demo,真正能够拿来用的话需要花心思优化。
5. 在实战中学习一种新的语言比较有趣,或者说是理解的会更深刻一些吧。

完整代码