[Android] RecyclerView を用いた横スクロールする ListView の実装

# 何を作ったか
Android でおなじみの ListView ですが、水平方向へのスクロールは出来ません。そこで、RecyclerView を継承して横方向へスクロールする ListView のようなウェジェットを作ります。

# RecyclerView とは？
複数の View を良しなに表示するウェジェットです。かなり自由度が高いので、大抵のものはこれでできます。詳細に関しては、[【Android】RecyclerViewの基本的な実装](https://qiita.com/HideMatsu/items/a9ab48608e4f681d31fe) が詳しいです。

# 完成品
```java:HorizontalListView.java

import android.content.Context;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

public class HorizontalListView extends RecyclerView{

    public interface OnItemClickListener{
        void onItemClick(View view, int position);
    }

    public HorizontalListView(Context context){
        super(context);
        initialize(context);
    }

    public HorizontalListView(Context context, AttributeSet set){
        super(context, set);
        initialize(context);
    }

    public HorizontalListView(Context context, AttributeSet set, int defaultAttr){
        super(context, set, defaultAttr);
        initialize(context);
    }

    private void initialize(Context context){
        LinearLayoutManager manager = new LinearLayoutManager(context);
        manager.setOrientation(LinearLayoutManager.HORIZONTAL);
        setLayoutManager(manager);
    }

    private OnItemClickListener mListener;

    public void setOnItemClickListener(OnItemClickListener listener){
        mListener = listener;
        Adapter adapter = getAdapter();
        if ( adapter instanceof ArrayAdapter ){
            ArrayAdapter a = (ArrayAdapter)adapter;
            a.mListener = this.mListener;
        }
    }

    @Override
    public void setAdapter(RecyclerView.Adapter adapter){
        super.setAdapter(adapter);
        if ( adapter instanceof ArrayAdapter ){
            ArrayAdapter a = (ArrayAdapter)adapter;
            a.mListener = this.mListener;
        }
    }

    @Override
    protected void onDetachedFromWindow(){
        super.onDetachedFromWindow();
        setOnItemClickListener(null);
        setAdapter(null);
        setLayoutManager(null);
    }

    private static class SimpleViewHolder extends ViewHolder{
        private SimpleViewHolder(View view){
            super(view);
        }
    }

    public static abstract class ArrayAdapter<E> extends RecyclerView.Adapter<ViewHolder> {

        public ArrayAdapter(List<E> list){
            mDataList = new ArrayList<>(list.size());
            mViews = new ArrayList<>(list.size());
            mDataList.addAll(list);
        }

        private List<E> mDataList;
        private OnItemClickListener mListener;
        private List<View> mViews;

        /**
         * リスト要素となるViewをインスタンス化する
         * @param parent
         * @return null not acceptable
         */
        public abstract View getView(ViewGroup parent);

        /**
         * リストに表示するデータをViewへ反映する
         * @param view 反映先のView
         * @param data 反映させるデータ{@link #getItem(int)}でも取得可能
         * @param position リスト上での位置
         */
        public abstract void onBindView(View view, E data, int position);

        @Override
        public final ViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
            return new SimpleViewHolder(getView(parent));

        }

        @Override
        public final void onBindViewHolder(final ViewHolder holder, int position){

            final View view = holder.itemView;
            E data = getItem(position);
            onBindView(view, data, position);

            mViews.add(holder.itemView);
            holder.itemView.setOnClickListener(new OnClickListener(){
                @Override
                public void onClick(View v){
                    if ( mListener != null ){
                        mListener.onItemClick(view, holder.getAdapterPosition());
                    }
                }
            });
        }

        @Override
        public void onDetachedFromRecyclerView(RecyclerView recyclerView){
            super.onDetachedFromRecyclerView(recyclerView);
            for ( View view : mViews ) view.setOnClickListener(null);
            mViews = null;
            mListener = null;
            mDataList = null;
        }

        @Override
        public final int getItemCount(){
            return mDataList.size();
        }

        public E getItem(int position){
            return mDataList.get(position);
        }
    }
}
```

# 実装の説明

## 横スクロールの実現
横方向を指定した LinearLayoutManager を RecyclerView に設定するだけでOK。

```java
private void initialize(Context context){
    LinearLayoutManager manager = new LinearLayoutManager(context);
    manager.setOrientation(LinearLayoutManager.HORIZONTAL);
    setLayoutManager(manager);
}
```
## アダプターの用意
RecyclerView も ListView と同様に、データと表示する View を管理するアダプターが必要となります。そこで、RecyclerView.Adapter を継承して HorizontalListView.ArrayAdapter を用意。使用時に実装すべきは以下の２つ。

```java
    public abstract View getView(ViewGroup parent);

    public abstract void onBindView(View view, E data, int position);
```
getView(ViewGroup) で表示に使う View をインフレートして、onBindView(View,E,int) でデータ E の内容を View へ反映させます。
ListView を使う時、android.widget.ArrayAdapter を継承したアダプターでレイアウトをカスタマイズするのと同じ感じで操作できるように、同様なAPIを持たせました。

## コールバックの用意
ListView と同様にリストの要素がクリックされたら通知するようなコールバックが欲しいですね。作りましょう。

```java
public interface OnItemClickListener{
    void onItemClick(View view, int position);
}
```
クリックされた View と表示位置を通知します。実装としては、先ほど作った HorizontalListView.ArrayAdapter が getView(ViewGroup) で取得して表示する View に OnClickListener をセットして、受け取ったコールバックを中継します。ただし、注意として RecyclerView#setAdapter() で設定するアダプターが HorizontalListView.ArrayAdapter でないと機能しません。