GridViewからRecyclerViewへの乗り換え


## 今日の話
リスト表示に使っているGridViewをRecyclerViewに置き換えました. 
やったことは，大きく分けて以下の3つです．

- layout.xmlでGridViewをRecyclerViewに置き換える
- AdapterをRecyclerView用に書き換える
- ActivityでGridViewをRecyclerViewに置き換える

今日は，このことについて書いていきます．

## ライブラリのインポート
app配下のbuild.gradleに
`compile 'com.android.support:recyclerview-v7:22.2.1'`
などと書けばOKです．

```
dependencies {
    compile 'com.android.support:appcompat-v7:22.2.1'
    compile 'com.android.support:recyclerview-v7:22.2.1'
}
```

appcompatとrecyclerviewのバージョンは揃えないと注意されるので，気をつけてください．
自分は最初，次のようにして，注意されました．(v7:22とv7:21でずれてる)

```
dependencies {
    compile 'com.android.support:appcompat-v7:22.2.1'
    compile 'com.android.support:recyclerview-v7:21.0.0'
}
```
では，内容に入っていきます．

## layout.xmlで`GridView`を`RecyclerView`に置き換える
GridViewでこのように書いていたものを．

```xml:activity_list.xml
<GridView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:numColumns="2" />
```

RecyclerViewでは，このように書き換えます．

```xml:activity_list.xml
<android.support.v7.widget.RecyclerView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"/>
```
なくなったリストの列数の指定(`android:numColumns`)は，Activity内で行います．

## AdapterをRecyclerView用に書き換える
`GridView`では`ArrayAdapter<ListItem>`を使っていましたが，
`RecyclerView`では，`RecyclerView.Adapter<ImageAdapter.ViewHolder>`を使います．
GridViewの時はこんな感じでした．

```java:ImageAdapter.java
public class ImageAdapter extends ArrayAdapter<ListItem> {
        private final LayoutInflater mInflater;

        public static class ViewHolder {
            ImageView imageview;
        }

        public ImageAdapter(Context context, List<ListItem> objects) {
            super(context, 0, objects);
            mInflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }

        @Override
        public View getView(int position, View convertView,
                            ViewGroup parent) {
            ViewHolder holder;

            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.row_image_list, parent, false);
                holder = new ViewHolder();
                holder.imageview = (ImageView) convertView.findViewById(R.id.image);
　　　　　　　　　// you can also set view size here. like this
                // ViewGroup.LayoutParams params = convertView.getLayoutParams();
                // params.height = view_size;
                // convertView.setLayoutParams(params);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }

            // attach data to covertView
            ListItem item = getItem(position);
            holder.imageview.setImageResource(item.img_id);

            return convertView;
        }

    }
```

RecyclerViewではこうなります．

```java:ImageAdapter.java
public class ImageAdapter extends RecyclerView.Adapter<ImageAdapter.ViewHolder> {
    private final LayoutInflater mInflater;
    private List<ListItem> mDataSet;

    public static class ViewHolder extends RecyclerView.ViewHolder {
        public ImageView imageview;
        public ViewHolder(View v) {
            super(v);
            imageview = (ImageView) v.findViewById(R.id.image);
        }
    }

    public ImageAdapter(Context context, List<ListItem> myDataSet) {
        mDataSet = myDataSet;
        mInflater = (LayoutInflater) context
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public ImageAdapter.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        // Create a new view.
        View v = mInflater.inflate(R.layout.row_image_list, viewGroup, false);
        // you can also set view size here. like this
        // ViewGroup.LayoutParams params = v.getLayoutParams();
        // params.height = view_size;
        // v.setLayoutParams(params);
        return new ImageAdapter.ViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        // attach data to covertView
        ListItem item = mDataSet.get(position);
        holder.imageview.setImageResource(item.img_id);
    }

    @Override
    public int getItemCount() {
        return mDataSet.size();
    }
}
```
いろいろ変わったように見えますが，新しくできたメソッドによって，
書く位置が変わっただけで，やっていることは同じです．

変更点をまとめると

- ViewHolderをRecyclerView.ViewHolderを継承するように変える
- Listに表示するデータは，Constructorで受け取ってAdapterのフィールドとして持っておく
- `if (convertView == null)`の内容を`onCreateViewHolder(...)`に移す
- `getItem(position)`してデータをバインドするところは，`onBindViewHolder(...)`に移す
- 新しくできた`getItemCount()`で，データ数を返すようにする．

## ActivityでGridViewをRecyclerViewに置き換える
GridViewの時はこうでした．

```java:MyListActivity.java
mGridView = (GridView) findViewById(android.R.id.list);
mAdapter = new ImageAdapter(mAppContext, mDataSet);
mGridView.setAdapter(mAdapter);
```

RecyclerViewではちょっと長くなります．

```java:MyListActivity.java
mRecyclerView = (RecyclerView) findViewById(android.R.id.list);
mRecyclerView.setHasFixedSize(true);
mLayoutManager = new GridLayoutManager(mAppContext, COLUMN_NUM);
mRecyclerView.setLayoutManager(mLayoutManager);
mAdapter = new MyAdapter(myDataset);
mRecyclerView.setAdapter(mAdapter);
```
`GridView`にはなかった，`setHasFixedSize()`と`setLayoutManager()`が出てきました．

`setHasFixedSize()`は，リストのコンテンツの大きさがデータによって変わらないなら`true`を渡します．
これを`RecyclerView`に知らせることで，パフォーマンスが良くなるそうです．

`setLayoutManager()`に渡す`LayoutManager`によって，
RecyclerViewに1列表示なのか，Grid表示なのかなどを教えてあげます．
ここでは，Grid表示にしたいので`GridLayoutManager`を渡しています．

## 終わり
乗り換えたことで何か得したかというと，今のところ何も得していません．(笑)
また，上には書いていませんが，`OnScrollLister`や`OnItemClickListener`も
`GridView`と`RecyclerView`との間で変更点があって，とまどいました．

そのあたりについては，おまけに書きました．
困っている人の助けになれれば幸いです．

## おまけ
乗り換えの時に，個人的に困ったことについて書きます．

###OnScrollListenerの実装がGridViewの時と違う
変わった点

- `firstVisibleItem`, `visibleItemCount`, `totalItemCount`が引数として渡されなくなる
- `onScroll(...)`から`onScrolled(...)`に名前が変わる
- `setOnScrollListener(listener)`から`addOnScrollListener(listener)`に名前が変わる

GridViewの時はScrollListenerの中がこんな感じでした．

```java:MyListActivity.java
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
                     int visibleItemCount, int totalItemCount) {
    //load next item
    if (!end_flg && !busy.get() && totalItemCount - firstVisibleItem <= LOAD_ITEM_NUM) {
        int page = totalItemCount / LOAD_ITEM_NUM;
        updateList(page, false);  // false means do not refresh
    }

    // if it's loading and reached to bottom of the list, then show loading animation.
    if (busy.get() && firstVisibleItem + visibleItemCount == totalItemCount) {
        mProgress.setVisibility(View.VISIBLE);
    }
}
```

それが，RecyclerViewになると．

```java:MyListActivity.java
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    super.onScrolled(recyclerView, dx, dy);

    GridLayoutManager layoutManager = (GridLayoutManager)recyclerView.getLayoutManager();
    int firstVisibleItem = layoutManager.findFirstVisibleItemPosition();
    int visibleItemCount = layoutManager.getChildCount();
    int totalItemCount = layoutManager.getItemCount();
    

    //load next item
    if (!end_flg && !busy.get() && totalItemCount - firstVisibleItem <= LOAD_ITEM_NUM) {
        int page = totalItemCount / LOAD_ITEM_NUM;
        updateList(page, false);  // false means do not refresh
    }

    // if it's loading and reached to bottom of the list, then show loading animation.
    if (busy.get() && firstVisibleItem + visibleItemCount == totalItemCount) {
        mProgress.setVisibility(View.VISIBLE);
    }
}
```

自分で`firstVisibleItem `などを用意します．
ちなみに，RecyclerViewの引数にある`dx`,`dy`はそれぞれ，スクロールの横，縦の変化量だそうです．

### `setOnItemClickListener()`がなくなる
RecyclerViewでは`setOnItemClickListener(listener)`がなくなってるので，
クリック時のアクションは`RecyclerView.Adapter`内で，
`setOnClickListener()`を使って実装しました．
とりあえず実装はできましたが，もっと良いやり方があると思います．

GridViewのときは，何も問題ありません．

```java:MyListActivity.java
mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                String parsed_json = gson.toJson(mAdapter.getItem(position));
                Intent intent = new Intent(getActivity(), DetailActivity.class);
                intent.putExtra(DetailActivity.EXTRA_CONTENT, parsed_json);
                startActivity(intent);
            }
        });

```

RecyclerViewでは，Adapterの`onCreateViewHolder`でviewにClickListerをセットすれば良いだろうと思いました．
ただ，実装してみると，クリックされたViewがリスト内のどの位置にいるのか分からないことに気づきました．

```java:ImageAdapter.java
@Override
public ImageAdapter.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
    View v = mInflater.inflate(R.layout.row_image_list, viewGroup, false);
    ImageView imageview = (ImageView)v.findViewById(R.id.image);
    imageview.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            int position = ???  // We don't know its position
            String parsed_json = gson.toJson(mDataSet.get(position));
            Intent intent = new Intent(mContext, DetailActivity.class);
            intent.putExtra(DetailActivity.EXTRA_CONTENT, parsed_json);
            mContext.startActivity(intent);
        }
    });
    return new ImageAdapter.ViewHolder(v);
}
```

そこで，Viewのリスト内の位置(`position`)をタグとして，記録しておくようにしました．

```java:ImageAdapter.java
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
    // set list position as tag
    holder.imageview.setTag(R.integer.list_pos_key, position);
    // 省略
}

@Override
public ImageAdapter.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
    // 省略
    imageview.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            int position = (Integer) v.getTag(R.integer.list_pos_key);
　　　　　　 // 省略
        }
     }
}
```

`onBindViewHolder`ではリスト内の`position`が分かるので，そこでクリックするviewに`position`をタグとしてくっつけるようにしました．
こうすることで，`onClick()`の中でも，viewにくっつけておいたタグから，クリックされたデータの`position`が分かるようになりました．

また，タグのkeyに使っている`R.integer.list_pos_key`は，
res/values/integers.xmlというファイルを作り，その中で下のように定義しています．

```xml:integers.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="list_pos_key">1</integer>
</resources>
```

## 参考文献
[Creating Lists and Cards | Android Developers](https://developer.android.com/training/material/lists-cards.html)