この記事はVASILY DEVELOPERS BLOGにも同じ内容で投稿しています。よろしければ他の記事も御覧ください。
RecyclerViewが発表されて1年半ほど経ちましたが、みなさん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をセットしたり、取り除いたりします。
private void loadData(final int page) {
// ProgressBarを表示させるためにStubをセット
final ProgressStub progressStub = new ProgressStub();
if (page > 1) {
adapter.add(progressStub);
}
// postDelayedして通信処理を仮想しています
handler.postDelayed(new Runnable() {
@Override
public void run() {
// 通信処理が終わったのでセットしたStubを取り除く
if (page > 1) {
adapter.remove(progressStub);
}
// 通信して取得したデータを処理
...
}
}, 2000);
}
データセットの中身を見て RecyclerView.Adapter#getItemViewType の返す値を変える
RecyclerView.Adapter#getItemViewType(int position)
をOverrideして、返す値を変えることで RecyclerView.Adapter#onCreateViewHolder
側で、ViewTypeによってViewHolderを分けることができます。
今回もデータセット内のStubの有無をチェックして、ViewTypeを返し分けて、ViewHolderを分けることでProgressBarを表示させるようにしています。
ヘッダーなどを実装する場合にも同様のアプローチを取ることで実装できます。
@Override
public int getItemViewType(int position) {
Object object = objects.get(position);
if (object instanceof ProgressStub) {
// データがProgressStubの場合は通常とは違う値を返す
return TYPE_PROG;
} else {
return TYPE_ITEM;
}
}
@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に関しては気にしなくて大丈夫です。
@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へ移行するとなると自分で実装しないといけないものが多くかなりハードであると思います。
ただし、RecyclerViewへ置き換えができると処理がモジュールごとに分散できるのでメンテナンスがしやすくなります。
シンプルなリスト表示・グリッド表示の場合には今まで通りListView・GridViewを使った方が良いと思いますが、positionごとにコンテンツを変えたり、アニメーションを駆使したりしたい場合は、長期的考えてRecyclerViewを使ったほうが良いと思います。