ListViewで作っていたアプリに並べ替え機能を追加したかったのですが、並べ替え機能を実現するためにはRecyclerViewを使わないといけないようでした。

なので、ListViewからRecyclerViewへ載せ替えた内容について書きたいと思います。

RecyclerViewの実装方法については様々な記事で見つかるのですが、この記事ではListViewで実現していた事をRecyclerViewでどのように実現したかに焦点を当てています。

 

以下のように行を長押しすると移動ができます。

 

ソースコードの全容が知りたい場合にはgitHubでコードを公開しています。

https://github.com/highcom/ComicMemo

 

まだ、動作を知りたい場合にはgoogle playでアプリを公開しています。

巻数メモ

 

では、説明をしていきます。

1.build.gradleにサポートライブラリを追加

dependenciesにサポートライブラリを使う事を記述します。
その際、minSdkVersionが14以上である事が必須で、compileSdkVersionとバージョンと合わせる事が推奨されます。
android {
    compileSdkVersion 26
    defaultConfig {
        ...
        minSdkVersion 14
    }
    ...
}
dependencies {
    implementation 'com.android.support:recyclerview-v7:26.1.0'
}

2.レイアウトxmlのListViewタグを置き換える

リスト画面のxmlでListViewタグで定義していたものをRecyclerViewに置き換えます。
    <ListView
        android:id="@+id/comicListView"
        ...
        android:layout_marginBottom="50dp" />
↓↓↓
    <android.support.v7.widget.RecyclerView
        android:id="@+id/comicListView"
        ...
        android:layout_marginBottom="50dp" />

また、カスタムViewを使っていたので、1行ずつのレイアウトファイルとしてrow.xmlを定義しているのですがそちらに変更はありません。

3.継承するAdapterを置き換えて実装

3.1.継承するAdapterクラスを変更

RecyclerViewのAdapterを利用する場合、ViewHolderを継承したジェネリクスクラスの実装が必要です。
public class ListViewAdapter extends SimpleAdapter {
    public class ViewHolder {
        TextView title;
        ....
    }
    ....
}
↓↓↓
public class ListViewAdapter extends RecyclerView.Adapter<ListViewAdapter.ViewHolder> {
    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView title;
        ....
    }
    ....
}

3.2.メソッドを修正

①Overrideするメソッドを変更
ListViewの場合は、SimpleAdapterを継承していたので、
・getView
・getCount
の2つをOverrideする必要がありましたが、RecyclerViewの場合には、
・onCreateViewHolder
・onBindViewHolder
・getItemCount
の3つをOverrideする必要があります。
 
②メソッド内で実装していた処理の移動
getView内で実装していた処理は、ViewHolderクラスのコンストラクタ内に移す必要があります。
 
③リスナーメソッドの独自実装
行選択時のリスナーメソッドについて、
ListViewではActivityクラスでListViewをインスタンス化する時にsetOnItemClickListenerで実装します(後述)。
RecyclerViewではリスナーメソッドをAdapter側で実装する必要があるので、onBindViewHolderメソッド内でリスナーメソッドを実装しています。
public class ListViewAdapter extends SimpleAdapter {
    public class ViewHolder {
        TextView title;
        ....
    }
	@Override
    public int getCount() {
        ....
    }
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        final ViewHolder holder;
        holder = new ViewHolder();
        holder.title = (TextView) view.findViewById(android.R.id.title);

        Button addbtn = (Button) view.findViewById(R.id.addbutton);
        addbtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View arg0) {
                ....
            }
        });
    }
}
↓↓↓
public class ListViewAdapter extends RecyclerView.Adapter<ListViewAdapter.ViewHolder> {
    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView title;
        ....
        public ViewHolder(final View itemView) {
            super(itemView);
            title = (TextView) itemView.findViewById(R.id.title);

            Button addbtn = (Button) view.findViewById(R.id.addbutton);
            addbtn.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View arg0) {
                    ....
                }
            });
        }
    }
    @Override
    public int getItemCount() {
        ....
    }
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ViewHolder(inflater.inflate(R.layout.row, parent, false));
    }
    @Override
    public void onBindViewHolder(ViewHolder holder, final int position) {
        holder.itemView.setTag(holder);
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ....
            }
        });
    }
}

4.行の区切り線を実装

ListViewでは行毎の区切り線はデフォルトで定義されていましたが、RecyclerViewでは独自で定義する必要があルため、ItemDecorationクラスを継承したDividerItemDecorationクラスを実装します。
public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    private Drawable mDivider;

    private int mOrientation;

    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }

    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

5.AdapterをViewにsetする

ListView/RecyclerView共に、Viewをインスタンス化してAdapterをsetする処理がありますが、以下のように変更します。
また、RecyclerViewでは区切り線を独自実装したので、その実装クラスもsetします。
public class ComicMemo extends Activity {
    private ListView listView;
    private ListViewAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        adapter = new ListViewAdapter();
        listView = (ListView) findViewById(R.id.comicListView);
        listView.setAdapter(adapter);
        // アイテムクリック時ののイベントを追加
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            public void onItemClick(AdapterView<?> parent,
                                    View view, int pos, long id) {
                ...
            }
        });
    }
}
↓↓↓
public class ComicMemo extends Activity {
    private RecyclerView recyclerView;
    private ListViewAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        adapter = new ListViewAdapter();
        recyclerView = (RecyclerView) findViewById(R.id.comicListView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(adapter);
        // セル間に区切り線を実装する
        RecyclerView.ItemDecoration itemDecoration = new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST);
        recyclerView.addItemDecoration(itemDecoration);
        ...
    }

6.ItemTouchHelperクラスで並べ替え操作を実装

onCreateメソッド内に、並べ替え操作を実現するItemTouchHelperクラスを実装し、RecyclerViewにアタッチします。
今回はItemTouchHelperのSimpleCallBackクラスを継承して実装します。
SimpleCallbackクラスでは
・onMove
・onSwiped
メソッドをOverrideする必要があります。
また、どのタッチ操作を有効にするかはコンストラクタのパラメータで指定できます。
    protected void onCreate(Bundle savedInstanceState) {
        ...
        // ドラックアンドドロップの操作を実装する
        ItemTouchHelper itemDecor = new ItemTouchHelper(
                new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
                        ItemTouchHelper.ACTION_STATE_IDLE) {
                    @Override
                    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
                        final int fromPos = viewHolder.getAdapterPosition();
                        final int toPos = target.getAdapterPosition();
                        adapter.notifyItemMoved(fromPos, toPos);
                        return true;
                    }

                    @Override
                    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
                    }
                });
        itemDecor.attachToRecyclerView(recyclerView);
    }
以上で、ListViewからRecyclerViewに変更する実装手順の説明は終わりになります。
ListView自体はもうレガシーな扱いになっているので、変更できてちょうどよかった。
なお、ListViewの実装については以前の記事の「[Java]ボタン付きのカスタムセルのListViewを作成する」で記載していますので、こちらも参考にしてみてください。