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

这是 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. 在实战中学习一种新的语言比较有趣,或者说是理解的会更深刻一些吧。

完整代码

上一篇:URAL 1934 Black Spot --- 最短的简单修改


下一篇:hdu 1102(最小生成树)