这是 Kotlin 练习的的第二篇。这一篇的由来是因为刚刚在 Android 开发者官网查看 API 的时候,偶然看到了角落里面的 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 的。
上面的线框图简单明了说明了各个功能相关联的类。我们先从 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 的布局文件中随意弄一些布局。
注意布局中的那个按钮,当点击按钮后将生成 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 主要流程:
- 创建 PdfDocument 对象。
- 为每一页准备 PageInfo。
- 调用 PdfDocument 的 startPage() 方法并传入 PageInfo 作为参数生成 Page 对象。
- 获取 Page 对象中的 Canvas 对象进入内容的绘制。
- 结束当前 Page 的绘制。
- 将 PdfDocument 保存到外部流中。
- 关闭 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() 中的方法体就会执行,然后把结果缓存下来,第二次调用时就直接调用缓存了。
好的,下面我们来实际演练一下。
可以观察到的是,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 能不能被我们自己编写的代码渲染成功。
可以看到,没有问题。
总结
1. PDF 文件的生成与渲染其实在 Android 中非常简单,算是一个小技巧,大家花点时间就能掌握。两个核心类就是 PdfDocument 和 PdfRenderer。
2. 文章中代码语言是 kotlin,其实 Java 当然也可以了。
3. kotlin 中 lambda 表达式比较抽象,大家要多思考才能理解,总之它是用来精简替换匿名内部类的。
4. 文章例子只是 Demo,真正能够拿来用的话需要花心思优化。
5. 在实战中学习一种新的语言比较有趣,或者说是理解的会更深刻一些吧。