专栏首页Frank909Kotlin 第二弹:Android 中 PDF 创建与渲染实践

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

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

完整代码

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 细说 AppbarLayout,如何理解可折叠 Toolbar 的定制

    Material Design 是个好东西,它的出现使得 Android 也能定制高颜值的界面,并且指导了如果实现复杂炫丽的交互效果,而 Android Sur...

    Frank909
  • Kotlin 第一弹:自定义 ViewGroup 实现流式标签控件

    上周 Google I/O 大会的召开,宣布了 Kotlin 语言正式成为了官方开发语言。一时间 Android 开发者的圈子炸开了锅,各种关于 Kotlin ...

    Frank909
  • 长谈:关于 View Measure 测量机制,让我一次把话说完

    首先声明,这一篇篇幅很长很长很长的文章。目的就是为了把 Android 中关于 View 测量的机制一次性说清楚。算是自己对自己较真。写的时候花了好几天,几次想...

    Frank909
  • Android实现类似iOS风格的对话框实例代码

    以上所述是小编给大家介绍的Android实现类似iOS风格的对话框样式,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大...

    砸漏
  • Android开发之ProgressBar字体随着进度条的加载而滚动

    在网上翻阅了很多关于ProgressBar滚动效果,但是始终没有找到适合项目中的这种效果,故自己写这篇文章,记录一下写作过程,给大家做一个参考。先看下最终效果效...

    砸漏
  • 1-AI--Activity生命周期

    张风捷特烈
  • UI的基本控件设计

    在activity_main.xml中添加控件的样式,在MainActivity中可以添加点击事件

    Dream城堡
  • Android实现底部弹出按钮菜单升级版

    本文实例为大家分享了Android实现底部弹出按钮菜单的具体代码,在Android实现底部缓慢弹出菜单的升级,供大家参考,具体内容如下

    砸漏
  • Android控件Tween动画(补间动画)实现方法示例

    本文实例讲述了Android控件Tween动画(补间动画)实现方法。分享给大家供大家参考,具体如下:

    砸漏
  • 安卓第六夜 凡高的自画像

    在上一讲中,我已经制作了一个简单的Android应用。项目的主要文件包括: MainActivity.java activity_main.xml 在这一讲,我...

    Vamei

扫码关注云+社区

领取腾讯云代金券