RecyclerView:グリッド表示をスムーズにスクロール

投稿日:  更新日:

RecyclerViewで「グリッド表示をスムーズにスクロール」する方法について、まとめます。

画像ファイルをグリッド表示する場合は、スムーズなスクロールを行うための工夫が必要です。

その工夫について紹介します。

※環境:Android Studio Ladybug | 2024.2.1 Patch 1
    Kotlin 2.0.0
    Compose Compilerプラグイン 2.0.0
    androidx.recyclerview:recyclerview:1.1.0

スポンサーリンク

スクロール出来ない・遅い原因

別の記事で、画像ファイルのグリッド表示を行いました。
※「RecyclerView:画像ファイルをグリッド表示」を参照

しかし、記事のサンプルは、問題があります。グリッド表示はできますが、スムーズなスクロールが出来ません。

膨大なフレームのスキップを起こし、最後にANR(Application Not Responding)となって、アプリが落ちます。

Skipped 129 frames!  The application may be doing too much work on its main thread.
Skipped 139 frames!  The application may be doing too much work on its main thread.
ANR in com.example.res.recyclerview (com.example.res.recyclerview/.PhotoListActivity)

この原因は次の通りです。

  • (1)画像サイズが大きく、ファイルアクセスに時間がかかる
  • (2)画像サイズが大きく、JPEG⇒Bitmap変換に時間がかかる
  • (3)上記により、Mainスレッドが重くなり、フレーム処理が間に合わない

原因を排除して問題を解決するために、2つの対策を実施します。

スポンサーリンク

対策1:画素データのサンプリング

「画素データのサンプリング」を行うと、原因(1)と(2)が排除されます。

サンプリングの様子

JPEGは不可逆の画像圧縮データなので、JPEG(データ)⇒Bitmap(画像)へ変換するデコード処理が必要になります。

通常のデコード処理は全ての画素(ピクセル)を復号します。

JPEGをデコード、サンプリング無し

ただし、サンプリング用いると、飛び飛びの画素を復号の対象にします。

例えば、「sampling ⇐ 4」にした場合の対象は「桃色」の画素です。

JPEGをデコード、サンプリング有り

JPEGデータのアクセスと復号処理が、対象画素に関連するデータのみに限定されるため、処理量が格段に少なくなります。「sampling ⇐ 4」ならば、およそ1/16です。

サンプリングの決め方

サンプリングを行うと、Bitmapの縦横サイズは「1/サンプリング値」になります。例えば「sampling ⇐ 4」にした場合は「1/4」です。

また、デコードアリゴリズムの関係で、サンプリング値は最も近い2のべき乗(1,2,4,8,16,32 …)になるように、端数が切捨てられます(ドキュメントに記載)。

ですので、サンプリング値は次の条件を満たす値にします。

  • ・表示サイズより大きいBitmapサイズになる
  • ・表示サイズに最も近いBitmapサイズになる
  • ・2のべき乗
  • ※表示サイズ:サンプルのImageViewサイズ

決められたサンプリング値は「Bitmapサイズ=表示サイズ」になりません。表示サイズへのスケーリングは、GPUが行ってくれます。

サンプリング後のサイズ

サンプリング値をプログラムで表現すると次のようになります。

fun getSampling(srcSize: Int, dstSize: Int): Int {
    val _Sampling = srcSize / dstSize
    return when {
        _Sampling < 2 -> 1;
        _Sampling < 4 -> 2;
        _Sampling < 8 -> 4;
        _Sampling < 16-> 8;
        _Sampling < 32-> 16;
        _Sampling < 64-> 32;
        else -> 64;
    }
}

サンプリングの実装

「画素データのサンプリング」を行ったグリッド表示は、次のようになります。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_photo_list)
        ...

        val _folder = "4000x3000"
        val _row = 4

        val _photos = assets.list(_folder)
        val _items = List(100) { PhotoData(it, "${_folder}/${_photos!![it]}") }

        val _sampling = getSampling(4000, 320 / _row) // 画面サイズ:320x480
        val _adapter = PhotoAdapter1a(_items, _sampling)
        val _manager = GridLayoutManager(this, _row)

        val _photoList = findViewById<RecyclerView>(R.id.rcyPhotoList)
        _photoList.adapter = _adapter
        _photoList.layoutManager = _manager
    }
}
class PhotoAdapter(
    val items: List<PhotoData>,
    val sampling: Int
) : RecyclerView.Adapter<PhotoAdapter.PhotoViewHolder>() {

    // ----- ビューホルダー ------------------------------------------
    class PhotoViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
        var id = -1
        val photo = view.findViewById<ImageView>(R.id.imgPhoto)
    }

    // ----- アダプター本体 ------------------------------------------
    override fun getItemCount(): Int { return items.size }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
            : PhotoViewHolder {
        val _view = LayoutInflater.from(parent.context)
            .inflate(R.layout.photo_item, parent, false)
        return PhotoViewHolder(_view)
    }

    override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
        holder.id = items[position].id
        holder.photo.setImageResource(android.R.drawable.ic_menu_gallery)
        val _filename = items[position].filename
        val _iStream = holder.view.context.assets.open(_filename)
        val _options = BitmapFactory.Options().apply {
            inSampleSize = sampling
        }
        val _bitmap = BitmapFactory.decodeStream(_iStream, null, _options)
        holder.photo.setImageBitmap(_bitmap)
    }
}

サンプルの結果

フレームのスキップは起こりますが、スキップされるフレーム数は格段に減りました。

また、ANRでアプリが落ちなくなりました。消費メモリーが少なくなったためです。

Skipped 30 frames!  The application may be doing too much work on its main thread.
Skipped 30 frames!  The application may be doing too much work on its main thread.

まだ、スクロールになっていません。これを改善するには、次にあげる「ワーカースレッドで分散処理」が必要になります。

スポンサーリンク

対策2:ワーカースレッドで分散処理

「ワーカースレッドで分散処理」を行うと、原因(3)が排除されます。

分散処理の様子

デコード処理は重いので、メインスレッドで行うとスレッドを独占してしまいます。

フレームのトリガが来てもメインスレッドに空きがないため、フレーム処理が出来ません。その結果、フレームをスキップします(左図)。

ワーカースレッドを起動してデコード処理を任せれば、メインスレッドに空きができて、フレーム処理が出来ます(右図)。

分散処理の様子

分散処理の実装

「ワーカースレッドで分散処理」を行ったグリッド表示は、次のようになります。
※「画素データのサンプリング」を含む

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_photo_list)
        ...

        val _folder = "4000x3000"
        val _row = 4

        val _photos = assets.list(_folder)
        val _items = List(100) { PhotoData(it, "${_folder}/${_photos!![it]}") }

        val _sampling = getSampling(4000, 320 / _row) // 画面サイズ:320x480
        val _adapter = PhotoAdapter(_items, lifecycleScope, _sampling)
        val _manager = GridLayoutManager(this, _row)

        val _photoList = findViewById<RecyclerView>(R.id.rcyPhotoList)
        _photoList.adapter = _adapter
        _photoList.layoutManager = _manager
    }
}

デコードをワーカースレッドで行い、ImageViewの設定をメインスレッドへ投げています(ImageView#postを利用)。

スレッドはDefaultプールから取得するので、並列処理のスレッド数は端末のCPUコア数を超えません。

class PhotoAdapter(
    val items: List<PhotoData>,
    val scope: CoroutineScope,
    val sampling: Int
) : RecyclerView.Adapter<PhotoAdapter.PhotoViewHolder>() {

    // ----- ビューホルダー ------------------------------------------
    class PhotoViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
        var id = -1
        val photo = view.findViewById<ImageView>(R.id.imgPhoto)
    }

    // ----- アダプター本体 ------------------------------------------
    override fun getItemCount(): Int { return items.size }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
            : PhotoViewHolder {
        val _view = LayoutInflater.from(parent.context)
            .inflate(R.layout.photo_item, parent, false)
        return PhotoViewHolder(_view)
    }

    override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
        holder.id = items[position].id
        holder.photo.setImageResource(android.R.drawable.ic_menu_gallery) // 初期画像
        scope.launch(Dispatchers.Default) {
            val _filename = items[position].filename
            val _iStream = holder.view.context.assets.open(_filename)
            val _options = BitmapFactory.Options().apply {
                inSampleSize = sampling
            }
            val _bitmap = BitmapFactory.decodeStream(_iStream, null, _options)
            holder.itemView.post {
                holder.photo.setImageBitmap(_bitmap)
            }
        }
    }
}

サンプルの結果

フレームのスキップもANRも起こりません。スムーズなスクロールになりました。

なお、一瞬、「白黒の山」が表示されるのは、初期画像を指定しているからです。初期画像は無くても良いです。無ければ元画像に新画像が上書きされます(元画像:GPU側に転送済みの画像)。

スポンサーリンク

関連記事:

RecyclerViewは子Viewを並べて表示するコンテナタイプ(ConstraintLayoutと同じ)のViewです。 複数のデータをスクリーン上に一覧表示したい時、例えば電話帳のような「氏名+住所+電話番号」の一覧を表示する場合などに最適です。 アプリを開発していると一覧表示したいデータが多いことに気付きます。 なのでRecyclerViewはとても重要で重宝するViewです。 しかし、思い通りの表示を行わせるためのテクニックが多すぎて、使いこなしが難しいです。 今まで調べたテクニックを忘れないように、整理して書き残そうと思います。 今回は基本の「RecyclerViewの実装」です。 ...
RecyclerViewでアイテムのクリックイベントを取得し、処理を実行する方法を紹介します。 ...
RecyclerViewはアイテムのレイアウトをアイテム毎に変更できます。その時に使う値がViewTypeです。 ViewTypeでアイテムのレイアウトを変更する方法を紹介します。 ...
RecyclerViewは表示が変更される(アイテムの更新、スクロール)時、アイテムのViewをリサイクル(再生利用)します。 これにより余分なViewの作成が行われなくなり、メモリーの節約とパフォーマンスの向上が望めます。 リサイクルはCachedViewsとRecyclerPoolという2つのキャッシュで行われます。 このキャッシュを使ったリサイクルの動作を調べたので紹介します。 ...
RecyclerViewのリサイクル動作で使われるキャッシュは、サイズを大きくすれば多くのViewHolderが保持できます。その分、多くのメモリを消費します。 ViewHolderを多く保持できたとしても、サイクル動作で効率よく使われなければ、メモリの浪費です。 キャッシュのサイズはRecyclerViewの使われ方よって適切なサイズがあります。 そのため、RecyclerViewはキャッシュのサイズを変更できるようになっています。 ...
RecyclerViewはアイテムを一覧表示してくれます。 ただ一覧表示するだけではなく、「追加・削除・移動・切り替え」といったアイテムの表示を効率よく変更する仕組み持っています。 今回はこの仕組みを使ったアイテムの変更方法を紹介します。 ...
RecyclerViewのリサイクル動作で使われるキャッシュは、サイズを大きくすれば多くのViewHolderが保持できます。その分、多くのメモリを消費します。 ViewHolderを多く保持できたとしても、サイクル動作で効率よく使われなければ、メモリの浪費です。 キャッシュのサイズはRecyclerViewの使われ方よって適切なサイズがあります。 そのため、RecyclerViewはキャッシュのサイズを変更できるようになっています。 ...
RecyclerViewが空(アイテムが無い)の時、EmptyViewを表示する実装を行ったので紹介します。 ...
RecyclerView上のアイテムを選択する方法を紹介します。 外部ライブラリー(AndroidX)で提供されるrecyclerview-selection APIを用いた方法です。 ...
RecyclerViewはListView(RecyclerViewの前身)の時に存在していたChoiceModeがありません。 同様な機能が欲しければプログラマ側で実装しなければなりません。 RecyclerView.AdapterをカスタマイズしてChoiceModeを実装してみたので紹介します。 ...
RecyclerViewはアイテムへ装飾を付けることが出来るようになっています。 装飾とは、例えばアイテムの区切り線などです。 今回はアイテムへ装飾を付ける方法を紹介します。 ...
RecyclerViewに表示しきれなかったアイテムはスクロールを行うことで表示されるようになっています。 スクロールは「外部入力(指でスクリーン上をタッチしてスライド)によるスクロール」の他に、「プログラムによるスクロール」をすることも出来ます。 今回はこのアイテムのスクロールについてまとめてみました。 ...
RecyclerViewはアイテムをスクロールさせて隠れたアイテムを表示できます。 スクロールを止める位置は任意です。 任意であるがゆえに、止めた位置によってはアイテムの一部が欠けてしまうこともあります。 携帯端末の画面は狭いので、効率よくコンテンツの表示を行いたいとアプリ開発者は考えます。 アイテムの一部が欠けてしまうことは、効率が良いとは言えません。 このような問題を解決するために、アイテムのスナップをRecyclerViewへ追加できます。 アイテムのスナップを追加する方法を紹介します。 ...
RecyclerViewでアイテムの変更(Change/Insert/Move/Remove)を行うと、変更される様子がアニメーション化されています。 これはデフォルトでアイテムの変更アニメーションが組み込まれているためです。 デフォルトは単純なアニメーションですが、「ある」と「ない」の違いは歴然で、アニメーションのある方が高価なアプリケーションに見えます。 GUI(Graphical User Interface)が主体の携帯端末にとって、利用者に対するアプリの見せ方は重要です。高価に見えた方が使ってもらえる可能性が高くなります。 上記のことから、アプリの機能に関係なくても、ちょっとした動きをアニメーション化するメリットがあります。 2回にわたりアイテムの変更アニメーションについてまとめてみました。 アイテムの変更アニメーション(DefaultItemAnimator、デフォルト) アイテムの変更アニメーション(SimpleItemAnimatorの継承、カスタム) 今回は第1回目「ItemAnimator、デフォルト」編です。デフォルト変更アニメーションの動作について説明します。 ...
RecyclerViewでアイテムの変更(Change/Insert/Move/Remove)を行うと、変更される様子がアニメーション化されています。 これはデフォルトでアイテムの変更アニメーションが組み込まれているためです。 デフォルトは単純なアニメーションですが、「ある」と「ない」の違いは歴然で、アニメーションのある方が高価なアプリケーションに見えます。 GUI(Graphical User Interface)が主体の携帯端末にとって、利用者に対するアプリの見せ方は重要です。高価に見えた方が使ってもらえる可能性が高くなります。 上記のことから、アプリの機能に関係なくても、ちょっとした動きをアニメーション化するメリットがあります。 2回にわたりアイテムの変更アニメーションについてまとめてみました。 アイテムの変更アニメーション(DefaultItemAnimator、デフォルト) アイテムの変更アニメーション(SimpleItemAnimatorの継承、カスタム) 今回は第2回目「SimpleItemAnimatorの継承、カスタム」編です。カスタム変更アニメーションの作り方を説明 ...
RecyclerViewへアイテムが表示されるとき、アニメーションはありません。。一瞬で表示されて終わりです。 「RecyclerViewへアイテムが表示される」ことを、ここでは「アイテムの出現」と言い表すことにします。 このアイテムの出現にアニメーションを付ける方法を紹介します。 アイテムの出現をアニメーションで演出することで、RecyclerViewに表示したい内容が際立つと思います。 ...
RecyclerViewで「画像ファイルをグリッド表示」する方法を、まとめます。 スマートフォンのアプリを作っていると、何度も遭遇するテクニックです。 ※環境:Android Studio Ladybug | 2024.2.1 Patch 1     Kotlin 2.0.0     Compose Compilerプラグイン 2.0.0     androidx.recyclerview:recyclerview:1.1.0 ...
スポンサーリンク