这是 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 主要流程:

  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 大家可以参考stackoverflow 这个页面

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

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

完整代码

Kotlin 第二弹:Android 中 PDF 创建与渲染实践的更多相关文章

  1. 前端学习 第二弹: JavaScript中的一些函数与对象(1)

    前端学习 第二弹: JavaScript中的一些函数与对象(1) 1.apply与call函数 每个函数都包含两个非继承而来的方法:apply()和call(). 他们的用途相同,都是在特定的作用域中 ...

  2. Android中的创建型模式总结

    共5种,单例模式.工厂方法模式.抽象工厂模式.建造者模式.原型模式 单例模式 定义:确保某一个类的实例只有一个,而且向其他类提供这个实例. 单例模式的使用场景:某个类的创建需要消耗大量资源,new一个 ...

  3. Kotlin Coroutines在Android中的实践

    Coroutines在Android中的实践 前面两篇文章讲了协程的基础知识和协程的通信. 见: Kotlin Coroutines不复杂, 我来帮你理一理 Kotlin协程通信机制: Channel ...

  4. 【转载】如何在Android中避免创建不必要的对象

    在编程开发中,内存的占用是我们经常要面对的现实,通常的内存调优的方向就是尽量减少内存的占用.这其中避免创建不必要的对象是一项重要的方面. Android设备不像PC那样有着足够大的内存,而且单个App ...

  5. Android中保存静态秘钥实践(转)

    本文我们将讲解一个Android产品研发中可能会碰到的一个问题:如何在App中保存静态秘钥以及保证其安全性.许多的移动app需要在app端保存一些静态字符串常量,其可能是静态秘钥.第三方appId等. ...

  6. Android 中JNI创建实例

    参考文档: http://blog.sina.com.cn/s/blog_a11f64590101924l.html http://www.cnblogs.com/hoys/archive/2010/ ...

  7. 第二课android中activity启动模式

    一.标准启动模式可以用函数gettaskid得到任务的idtostring得到地址用textallcaps来设置是否全部大写应用启动自己是在任务栈里创建不同实例可以用返回来返回上一个任务栈在andro ...

  8. 车载以太网第二弹|测试之实锤 -DoIP测试开发实践

    前言 车载以太网测试之实锤系列,之前我们已经从环境设备组成.被测对象组成再到测试过程和测试结果分析,分享了完整的PMA测试 .IOP测试 .TC8中的TCP/IP协议一致性测试 .也分享了1000BA ...

  9. 车载以太网第二弹 | 测试之实锤-物理层PMA测试实践

    前言 本期先从物理层"PMA测试"开始,下图1为"PMA测试"的测试结果汇总图.其中,为了验证以太网通信对线缆的敏感度,特选取两组不同特性线缆进行测试对比,果然 ...

随机推荐

  1. iOS学习之数据持久化详解

    前言 持久存储是一种非易失性存储,在重启设备时也不会丢失数据.Cocoa框架提供了几种数据持久化机制: 1)属性列表: 2)对象归档: 3)iOS的嵌入式关系数据库SQLite3: 4)Core Da ...

  2. python2 跟3的区别

    1----python2:1 臃肿 , 源码的重复量很多2:语法不清晰,掺杂着 c,pyp,java,的一些陋习 python3: 几乎是重构后的源码,规范 清晰 优美 2.python的分类 分为编 ...

  3. Php ArrayIterator的几个常用方法

    搜索商低..从php.net找到 ,自己翻译一下 总结在一起   rewind()    指针回到初始位置 valid()        判断数组当前指针下是否有元素 key()        数组键 ...

  4. javaEE中的字符编码问题

    0 web.xml中注册的CharacterEncodingFilter <!-- 配置字符集过滤器 --> <filter> <filter-name>encod ...

  5. caffe训练自己的数据集

    默认caffe已经编译好了,并且编译好了pycaffe 1 数据准备 首先准备训练和测试数据集,这里准备两类数据,分别放在文件夹0和文件夹1中(之所以使用0和1命名数据类别,是因为方便标注数据类别,直 ...

  6. 单元测试JUnit 4

    介绍   JUnit 4.x 是利用了 Java 5 的特性(Annotation)的优势,使得测试比起 3.x 版本更加的方便简单,JUnit 4.x 不是旧版本的简单升级,它是一个全新的框架,整个 ...

  7. sql 转

  8. po dto vo bo

    DozerBeanMapper是JavaBean的映射工具,可以进行对象之间相同属性名赋值     关于PO.DTO.VO在分层模型之间的关系:首先在持久层由DAO访问数据库将数据对象封装成PO,然后 ...

  9. 最小可用 Spring MVC 配置

    [最小可用 Spring MVC 配置] 1.导入有概率用到的JAR包, -> pom.xml 的更佳实践 - 1.0 <- <project xmlns="http:// ...

  10. SpringBoot AOP控制Redis自动缓存和更新

    导入redis的jar包 <!-- redis --> <dependency> <groupId>org.springframework.boot</gro ...