RecyclerViewが発表されて1年半ほど経ちましたが、みなさんRecyclerViewは活用していますか?
これまではListView・GridViewを頑張って使っていたiQONも、直近のリリースから少しずつRecyclerViewに置き換えはじめました。 RecyclerViewはListView・GridViewよりも柔軟になり拡張しやすくなった代わりに、必要なものは自分で実装しないといけなくなりました。 そのため、ListView・GridViewにはあったけどRecyclerViewではなくなった機能が存在します。
今回はRecyclerViewの GridLayoutManager を使う際、データロード中フッターにProgressBarを出す方法を紹介したいと思います。 LinearLayoutManagerに関しては今回触れませんが 『ProgressBarを表示する』 の項を参考にしてもらえれば実装できると思います。
サンプルコード
今回の内容のサンプルコードはこちらになります。 https://github.com/nissiy/GridLayoutSample
参照していただけると理解が深まると思います。 興味がある方はビルドもしてみてください。
実装
ProgressBarを表示する
RecyclerViewには ListView#addFooterView
のような仕組みがないためフッターを自分で実装しないといけません。
フッターを作成してそこにProgressBarを表示させるには、以下のことを行う必要があります。
- データロード前と後でデータセットに細工をする
- データセットの中身を見て
RecyclerView.Adapter#getItemViewType
の返す値を変える
データロード前と後でデータセットに細工をする
以下のように、通信処理の前後でデータセットにStubをセットしたり、取り除いたりします。
// MainActivity.java private void loadData(final int page) { // ProgressBarを表示させるためにStubをセット if (page > 1) { adapter.add(new ProgressStub()); } // postDelayedして通信処理を仮想しています handler.postDelayed(new Runnable() { @Override public void run() { // 通信処理が終わったのでセットしたStubを取り除く if (page > 1) { adapter.remove(adapter.getItemCount() - 1); } // 通信して取得したデータを処理 ... } }, 2000); }
データセットの中身を見て RecyclerView.Adapter#getItemViewType の返す値を変える
RecyclerView.Adapter#getItemViewType(int position)
をOverrideして、返す値を変えることで RecyclerView.Adapter#onCreateViewHolder
側で、ViewTypeによってViewHolderを分けることができます。
今回もデータセット内のStubの有無をチェックして、ViewTypeを返し分けて、ViewHolderを分けることでProgressBarを表示させるようにしています。 ヘッダーなどを実装する場合にも同様のアプローチを取ることで実装できます。
// PhotoGridAdapter.java @Override public int getItemViewType(int position) { Object object = objects.get(position); if (object instanceof ProgressStub) { // データがProgressStubの場合は通常とは違う値を返す return TYPE_PROG; } else { return TYPE_ITEM; } }
// PhotoGridAdapter.java @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { RecyclerView.ViewHolder viewHolder; if (viewType == TYPE_ITEM) { FrameLayout view = (FrameLayout) inflater.inflate(R.layout.photo_layout, parent, false); AppCompatImageView photoImageView = (AppCompatImageView) view.findViewById(R.id.photo_image_view); photoImageView.setLayoutParams(new FrameLayout.LayoutParams(imageSize, imageSize)); viewHolder = new PhotoLayoutHolder(view); } else { // ViewTypeがTYPE_PROGの場合はProgressBarのViewHolderを返す FrameLayout view = (FrameLayout) inflater.inflate(R.layout.progress_bar_layout, parent, false); viewHolder = new ProgressBarLayoutHolder(view); } return viewHolder; }
カラム数をpositionごとに変える
GridLayoutManagerを使う際には、SpanCountを 2 や 3 などに設定してカラム数を決めると思います。 GridLayoutManagerは拡張することでカラム数をpositionごとに変更することができるため、ヘッダー・フッターを作りたい時や、グリッドの途中でぶち抜きのコンテンツを出したいときに細工を行います。
今回もフッターに出すProgressBarはキレイに中央寄りになってほしいので、ProgressBarを表示するpositionではカラム数が変わるようにGridLayoutManagerを拡張しました。
public class GridWithProgressLayoutManager extends GridLayoutManager { public GridWithProgressLayoutManager(Context context, final int spanCount, final RecyclerBaseAdapter adapter) { super(context, spanCount); setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { // 今回はここを細工しています @Override public int getSpanSize(int position) { // ProgressBarを表示するpositionではSpanSizeをいっぱいに広げる if (adapter != null && adapter.getItemViewType(position) == RecyclerBaseAdapter.TYPE_PROG) { return spanCount; } // 1を返すと通常通りのSpanSizeになる return 1; } // 今回は触れませんが高速化のためにOverrideしています。詳しくは下記のURLを参照してください。 // http://developer.android.com/intl/ja/reference/android/support/v7/widget/GridLayoutManager.SpanSizeLookup.html @Override public int getSpanIndex(int position, int spanCount) { if (adapter != null && adapter.getItemViewType(position) == RecyclerBaseAdapter.TYPE_PROG) { return 0; } return position % spanCount; } }); } }
ItemDecorationを使っている場合は注意が必要
ItemDecorationを使っている場合、処理が複数回呼ばれてProgressBarがカクついてしまいます。 そのため、ProgressBarの場合は処理をスキップしてあげる必要があります。
GridLayoutManager特有の問題のためLinearLayoutManagerに関しては気にしなくて大丈夫です。
// GridSpacingItemDecoration.java @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { // ProgressBarのViewHolderの場合は処理をスキップする RecyclerView.ViewHolder viewHolder = parent.getChildViewHolder(view); if (viewHolder instanceof ProgressBarLayoutHolder) { return; } int position = parent.getChildAdapterPosition(view); int column = position % spanCount; outRect.left = column * spacing / spanCount; outRect.right = spacing - (column + 1) * spacing / spanCount; outRect.bottom = spacing; }
まとめ
長年、ListView・GridViewを使い続けているプロジェクトの場合、RecyclerViewへ移行するとなると自分で実装しないといけないものが多くかなりハードであると思います。 iQONの場合も最適化のための独自の仕組みや、広告表示処理などが複雑に絡まっているためRecyclerViewへの移行には時間がかかっています。 ただ、RecyclerViewへ置き換えが完了したページを見ると、もともと巨大で手を入れにくかった処理がモジュールごとに分散できているのでメンテナンスがしやすくなっています。
シンプルなリスト表示・グリッド表示の場合には今まで通りListView・GridViewを使った方が良いと思いますが、positionごとにコンテンツを変えたり、アニメーションを駆使したりしたい場合は、長期的考えてRecyclerViewを使ったほうが良いと思います。
最後に
VASILYでは、一緒にiQONを開発してくれる仲間を募集しています。少しでもご興味のある方は是非こちらからご応募よろしくお願いいたします。