ViewPager2+PdfRendererで簡単なPDFビューアを作る 

# 背景

とある Android アプリで PDF を閲覧する機能に AndroidPdfViewer (jcenter で配布されている) を使っていたのですが、2022年2月に jcenter が使えなくなるので、_「その内、MavenCentral なりで publish してくれるだろう」_ と呑気に構えていたのですが、README.md の先頭行に `Looking for new maintainer!` と書かれていたので、これはもう更新されないんだろうなと悟りました。

https://github.com/barteksc/AndroidPdfViewer

Android では iOS と違い OS 標準の WebView で PDF 表示とかしてくれません。

以下の記事にあるように、これで苦労したことがある Android プログラマは多いかもしれません。

https://qiita.com/s_of_p/items/1c1a467d246ab7dc45e1

ただし、Android 5.0 から API (PdfRenderer) が提供されているので、対応 OS が Android 5.0 以降であれば自前で PDF レンダリング処理を実装する必要が無くなりました。

でも、View までは提供してくれないのが Android スタイルw

_Android ではよくあること..._

> 別件ですが MediaCodec とかも同じようなパターンですね。
> MediaCodec は使い方がムズくて苦労しました...
> _（今は ExoPlayer があるからだいぶ楽になりましたが）_

# 欲しい PDF ビューアの機能

- 1ページづつ表示
- スワイプでページ切り替え

こんな感じの仕様の PDF ビューアであれば、ViewPager2 + PdfRenderer で割と簡単に作れたので、作成方法を紹介します。

# 実装

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>
```

```view_holder.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:contentDescription="@string/pdf_page" />
</FrameLayout>
```

```MainActivity.kt
package com.suzukiplan.pdfviewer

import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2

class MainActivity : AppCompatActivity() {
    private lateinit var viewPager: ViewPager2
    private var pdfRenderer: PdfRenderer? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewPager = findViewById(R.id.view_pager)
        // 横スワイプで切り替えたい場合は ViewPager2.ORIENTATION_HORIZONTAL にする
        viewPager.orientation = ViewPager2.ORIENTATION_VERTICAL

        // レンダラを作成（以下の処理は本当は非同期の方が良い）
        pdfRenderer = PdfRenderer(assets.openFd("example.pdf").parcelFileDescriptor)

        viewPager.adapter = Adapter()
    }

    inner class Adapter : RecyclerView.Adapter<ViewHolder>() {
        override fun getItemCount() = pdfRenderer?.pageCount ?: 0
        override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(position)
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
            ViewHolder(layoutInflater.inflate(R.layout.view_holder, parent, false))
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val imageView = itemView.findViewById<ImageView>(R.id.image_view)
        private var bitmap: Bitmap? = null

        fun bind(position: Int) {
            val page = pdfRenderer?.openPage(position) ?: return
            if (page.width != bitmap?.width || page.height != bitmap?.height) {
                bitmap?.recycle()
                bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888)
            }
            val bitmap = this.bitmap
            if (null != bitmap) {
                page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
                imageView.setImageBitmap(bitmap)
            }
            page.close()
        }
    }
}
```

上記コードは GitHub で公開しています。

https://github.com/suzukiplan/pdf-viewer-android

- [activity_main.xml](https://github.com/suzukiplan/pdf-viewer-android/blob/master/app/src/main/res/layout/activity_main.xml)
- [view_holder.xml](https://github.com/suzukiplan/pdf-viewer-android/blob/master/app/src/main/res/layout/view_holder.xml)
- [MainActivity.kt](https://github.com/suzukiplan/pdf-viewer-android/blob/master/app/src/main/java/com/suzukiplan/pdfviewer/MainActivity.kt)

# つまづきポイント

`page.close()` が漏れると、2ページ目を `openPage` した時に `java.lang.IllegalStateException: Current page not closed` でクラッシュするので注意しましょう。

# ピンチイン・アウトで拡縮したい場合

view_holder.xml の ImageView を PhotoView にすれば OK です。

https://github.com/Baseflow/PhotoView

```view_holder.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black">

    <com.github.chrisbanes.photoview.PhotoView
        android:id="@+id/image_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>
```