背景
とある Android アプリで PDF を閲覧する機能に AndroidPdfViewer (jcenter で配布されている) を使っていたのですが、2022年2月に jcenter が使えなくなるので、「その内、MavenCentral なりで publish してくれるだろう」 と呑気に構えていたのですが、README.md の先頭行に Looking for new maintainer!
と書かれていたので、これはもう更新されないんだろうなと悟りました。
Android では iOS と違い OS 標準の WebView で PDF 表示とかしてくれません。
以下の記事にあるように、これで苦労したことがある Android プログラマは多いかもしれません。
ただし、Android 5.0 から API (PdfRenderer) が提供されているので、対応 OS が Android 5.0 以降であれば自前で PDF レンダリング処理を実装する必要が無くなりました。
でも、View までは提供してくれないのが Android スタイルw
Android ではよくあること...
別件ですが MediaCodec とかも同じようなパターンですね。
MediaCodec は使い方がムズくて苦労しました...
(今は ExoPlayer があるからだいぶ楽になりましたが)
欲しい PDF ビューアの機能
- 1ページづつ表示
- スワイプでページ切り替え
こんな感じの仕様の PDF ビューアであれば、ViewPager2 + PdfRenderer で割と簡単に作れたので、作成方法を紹介します。
実装
<?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>
<?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>
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 で公開しています。
つまづきポイント
page.close()
が漏れると、2ページ目を openPage
した時に java.lang.IllegalStateException: Current page not closed
でクラッシュするので注意しましょう。
ピンチイン・アウトで拡縮したい場合
view_holder.xml の ImageView を PhotoView にすれば OK です。
<?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>