RecyclerViewのGridLayoutManagerでフッターにProgressBarを出す方法


> _この記事は[VASILY DEVELOPERS BLOG](http://tech.vasily.jp/entry/android_gridlayoutmanager_progress)にも同じ内容で投稿しています。よろしければ他の記事も御覧ください。_

RecyclerViewが発表されて1年半ほど経ちましたが、みなさんRecyclerViewは活用していますか？
RecyclerViewはListView・GridViewよりも柔軟になり拡張しやすくなった代わりに、必要なものは自分で実装しないといけなくなりました。 そのため、ListView・GridViewにはあったけどRecyclerViewではなくなった機能が存在します。

今回はRecyclerViewの __GridLayoutManager__ を使う際、データロード中フッターにProgressBarを出す方法を紹介したいと思います。
LinearLayoutManagerに関しては今回触れませんが __『ProgressBarを表示する』__ の項を参考にしてもらえれば実装できると思います。

# サンプルコード
今回の内容のサンプルコードはこちらになります。
https://github.com/nissiy/GridLayoutSample

参照していただけると理解が深まると思います。
興味がある方はビルドもしてみてください。

![screenshot_for_qiita.gif](https://qiita-image-store.s3.amazonaws.com/0/40539/54884e18-97f6-b59e-c8e7-b796c0f3e0fd.gif "screenshot_for_qiita.gif")

# 実装

## ProgressBarを表示する
RecyclerViewには ```ListView#addFooterView``` のような仕組みがないためフッターを自分で実装しないといけません。
フッターを作成してそこにProgressBarを表示させるには、以下のことを行う必要があります。

- データロード前と後でデータセットに細工をする
- データセットの中身を見て ```RecyclerView.Adapter#getItemViewType``` の返す値を変える

### データロード前と後でデータセットに細工をする
以下のように、通信処理の前後でデータセットにStubをセットしたり、取り除いたりします。

```MainActivity.java
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を表示させるようにしています。
ヘッダーなどを実装する場合にも同様のアプローチを取ることで実装できます。

```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を拡張しました。

```GridWithProgressLayoutManager.java
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へ移行するとなると自分で実装しないといけないものが多くかなりハードであると思います。
ただし、RecyclerViewへ置き換えができると処理がモジュールごとに分散できるのでメンテナンスがしやすくなります。

シンプルなリスト表示・グリッド表示の場合には今まで通りListView・GridViewを使った方が良いと思いますが、positionごとにコンテンツを変えたり、アニメーションを駆使したりしたい場合は、長期的考えてRecyclerViewを使ったほうが良いと思います。

# 参考資料
- [Header and Footer layout for RecylerView](http://takeoffandroid.com/android-customview/header-and-footer-layout-for-recylerview/)
- [RecyclerView: Grid with header](http://blog.sqisland.com/2014/12/recyclerview-grid-with-header.html)