【Android】RecyclerViewで一覧を表示する方法|ドラッグ&ドロップで移動・追加・削除

RecyclerViewで一覧を表示し、ドラッグ&ドロップで移動、追加、削除する流れをまとめたアイキャッチ画像

Android Studioで、データを RecyclerView に一覧表示し、そのデータを ドラッグ&ドロップで移動 したり、追加・削除 したりする方法を紹介します。

今回作成するのは、RecyclerViewに文字列データを一覧表示するシンプルなアプリです。

一覧画面では、次の操作ができます。

  • 右下の +ボタン をタップすると、データを1行追加する
  • リスト内の文字を編集する
  • リスト右側の 移動ボタン をドラッグ&ドロップして、並び順を変更する
  • リスト右側の ×(削除)ボタン をタップして、データを削除する

動きのイメージは、次のようになります。

Android RecyclerView

このシンプルなアプリを題材に、RecyclerViewにデータを表示する方法や、RecyclerViewに配置したボタンで操作する方法を見ていきます。

目次

この記事で作るもの

今回作成するアプリの完成イメージです。

RecyclerViewにコンテンツ1からコンテンツ5までが表示され、右側に移動ボタンと削除ボタン、右下に追加ボタンがある画面

画面には、文字列データが縦に並んでいます。

1行の中には、次の部品を配置します。

  • EditText:リストの内容を表示・編集する
  • 移動ボタン:ドラッグ&ドロップの目印として使う
  • 削除ボタン:その行を削除する

画面全体の役割を図にすると、次のようになります。

RecyclerViewの一覧画面と各部品の役割をまとめた説明図

Android Studioのインストールや基本的な使い方、ListViewを使った一覧表示については、先にこちらの記事を読んでおくと進めやすいです。

RecyclerViewとは

RecyclerView は、データを一覧表示するためのウィジェットです。

ListViewよりも自由度が高く、表示する1行分の見た目や操作を細かく作り込みやすいのが特徴です。

また、画面外に出たViewを再利用しながら表示するため、大きなデータを扱うリストにも向いています。

ただし、RecyclerViewでは次のような要素が出てくるため、最初は少し複雑に見えます。

  • Adapter
  • ViewHolder
  • LayoutManager
  • ItemTouchHelper

この記事では、まずシンプルな文字列リストを作りながら、それぞれの役割を確認していきます。

アプリの構成

今回のアプリの構成は、次のようになります。

MainActivity、RecyclerView、Adapter、ViewHolder、row_main.xml、ArrayList、リソースの関係をまとめた構成図

実際の構成図では、MainActivity、Adapter、ViewHolder、row_main.xml、リソースが次のようにつながっています。

Android constitution

主な役割は次のとおりです。

  • MainActivity:メイン画面を表示し、RecyclerViewを準備する
  • activity_main.xml:メイン画面のレイアウトを定義する
  • RecyclerView:一覧を表示する
  • SampAdapter:データと1行分の表示を結び付ける
  • SampViewHolder:1行分のViewへの参照を保持する
  • row_main.xml:1行分のレイアウトを定義する
  • ArrayList:一覧に表示するデータを保持する
  • strings.xml / drawable:文字列やアイコンなどのリソースを管理する

RecyclerViewでは、データを直接画面に置くのではなく、AdapterとViewHolderを通して1行ずつ表示していきます。

プロジェクト作成

プロジェクトは、こちらの記事の「プロジェクト作成」と同じ流れで作成できます。

Android Studioをインストール|プロジェクト作成の章

今回のプロジェクト名は、SampRecyclerView とします。

リソース準備

まずは、アプリ内から参照する文字列やアイコンを準備します。

strings.xml

app/res/values/strings.xml を開いて、次の内容に編集します。

<resources>
    <string name="app_name">SampRecyclerView</string>
    <string name="contents">コンテンツ</string>
    <string name="reg">登録</string>
    <string name="del">削除</string>
    <string name="move">移動</string>
</resources>

ここで定義した name を指定して、プログラムやレイアウトから参照します。

アイコン

今回使うアイコンは、次の3つです。

  • +アイコン:アイテム追加用
  • ×アイコン:アイテム削除用
  • 移動アイコン:ドラッグ&ドロップ用

追加用・削除用のアイコンは、前回の記事と同じようにVector Assetから追加します。

今回はさらに、移動ボタンとして使う ic_baseline_unfold_more_24.xml も追加します。

android move

アイコンの追加方法は、こちらの記事の「アイコンを追加する」の部分も参考にしてください。

画面レイアウト

今回作成するレイアウトは、主に次の2つです。

  • activity_main.xml:メイン画面全体
  • row_main.xml:RecyclerViewの1行分

activity_main.xml

activity_main.xml では、メイン画面に RecyclerViewFloatingActionButton を配置します。

RecyclerViewのメイン画面。コンテンツ1からコンテンツ5までの一覧と右下の追加ボタンが表示されている画面
activity_main.xml(クリックして表示)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/mainList"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_reg"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:focusable="true"
        android:clickable="true"
        android:contentDescription="@string/reg"
        android:onClick="onAddItem"
        app:srcCompat="@drawable/ic_baseline_add_24"
        app:tint="@color/white"
        app:backgroundTint="@color/purple_200"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
 
</androidx.constraintlayout.widget.ConstraintLayout>

このレイアウトでは、ConstraintLayout の中に次の2つだけを配置しています。

FloatingActionButton:右下の+ボタン

RecyclerView:一覧表示部分

ConstraintLayoutと+ボタン

RecyclerView は、上下左右を親レイアウトに制約して、画面いっぱいに配置しています。

FloatingActionButton は、下と右に制約して、画面右下に配置しています。

ConstraintLayoutでRecyclerViewとFloatingActionButtonを配置する考え方をまとめた図

ConstraintLayout は、画面の端や他の部品との位置関係を指定してレイアウトを作る方法です。

画面サイズが変わっても位置関係を保ちやすいので、一覧画面のようなレイアウトでも使いやすいです。

row_main.xml

次に、RecyclerViewの1行分の見た目を row_main.xml に定義します。

row_main.xml(クリックして表示)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
 
    <EditText
        android:id="@+id/edit_contents"
        android:layout_width="310dp"
        android:layout_height="70dp"
        android:layout_marginStart="10dp"
        android:background="#00000000"
        android:gravity="center_vertical"
        android:inputType="textMultiLine"
        android:textSize="20sp" />
 
    <ImageButton
        android:id="@+id/btn_move"
        android:layout_width="40dp"
        android:layout_height="70dp"
        android:background="#00000000"
        android:contentDescription="@string/move"
        android:gravity="center_vertical"
        app:srcCompat="@drawable/ic_baseline_unfold_more_24" />
 
    <ImageButton
        android:id="@+id/btn_del"
        android:layout_width="40dp"
        android:layout_height="70dp"
        android:background="#00000000"
        android:contentDescription="@string/del"
        android:gravity="center_vertical"
        app:srcCompat="@drawable/ic_baseline_close_24" />
 
</LinearLayout>

この1行分のレイアウトには、次の3つを配置しています。

  • EditText:リストの内容を入力・編集する
  • btn_move:ドラッグ&ドロップで移動するためのボタン
  • btn_del:削除用のボタン

1行分の構成は、次の図を見るとイメージしやすいです。

RecyclerViewの1行分にあるEditText、移動ボタン、削除ボタンの役割を整理した図

アダプターとビューホルダー

RecyclerViewでは、データを直接表示するのではなく、AdapterViewHolder を使って表示します。

アダプターは、データとウィジェットを関連付ける橋渡し役です。

ArrayListのデータがAdapterとViewHolderを通ってRecyclerViewに表示される流れをまとめた図

今回のアプリでは、SampAdapter というRecyclerView用のアダプターを作成します。

SampAdapter (クリックして表示)
package com.ma_chanblog.samprecyclerview;
 
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.widget.EditText;
import android.widget.ImageButton;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
 
public class SampAdapter extends RecyclerView.Adapter<SampAdapter.SampViewHolder>{
 
    private final List<String> arrayList;
    private MainActivity activity;
 
    // アダプターのコンストラクタ
    SampAdapter(List<String> arrayList) {
        this.arrayList = arrayList;
    }
 
    // ビューホルダーを生成
    @NonNull
    @Override
    public SampViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
 
        // レイアウトファイルに対応したViewオブジェクトを生成
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.row_main, parent, false);
 
        // MainActivityを取得
        activity = (MainActivity) parent.getContext();
 
        // ビューホルダーを生成してreturn
        return new SampViewHolder(view);
    }
 
    // ビューホルダーにデータを割り当てる 
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public void onBindViewHolder(SampViewHolder holder, int position) {
 
        // EditTextにデータを設定
        holder.edit_contents.setText(arrayList.get(position));
 
        // テキストウォッチャーリスナーが既にあれば削除
        if (holder.textWatcher  != null) {
            holder.edit_contents.removeTextChangedListener(holder.textWatcher);
        }
        // テキストウォッチャーを設定
        holder.textWatcher = createEditTextWatcher(holder);
        holder.edit_contents.addTextChangedListener(holder.textWatcher);
 
        // 移動ボタンをタッチ
        holder.btn_move.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                    // 長押しではなく、タッチしてすぐにドラッグ状態にする
                    activity.itemTouchHelper.startDrag(holder);
                    return true;
                }
                return v.onTouchEvent(event);
            }
        });
 
        // 削除ボタンをクリック
        holder.btn_del.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int adapterPosition = holder.getAdapterPosition();
                if (adapterPosition != -1) {
                    arrayList.remove(adapterPosition);
                    notifyItemRemoved(adapterPosition);
                }
            }
        });
    }
 
    // アイテム数を取得
    @Override
    public int getItemCount() {
        return arrayList.size();
    }
 
    // ビューホルダー
    public static class SampViewHolder extends RecyclerView.ViewHolder {
 
        // ビューに配置されたウィジェットへの参照を保持しておくためのフィールド
        public EditText    edit_contents;  // リストの内容
        public ImageButton btn_move;       // 移動ボタン
        public ImageButton btn_del;        // 削除ボタン
 
        // テキストウォッチャー
        public TextWatcher textWatcher;
 
        // ビューホルダーのコンストラクタ
        public SampViewHolder(View view) {
            super(view);
 
            // ウィジェットへの参照を取得
            edit_contents = (EditText) view.findViewById(R.id.edit_contents);
            btn_move      = (ImageButton) view.findViewById(R.id.btn_move);
            btn_del       = (ImageButton) view.findViewById(R.id.btn_del);
        }
    }
 
    // テキストウォッチャー
    private TextWatcher createEditTextWatcher(final SampViewHolder viewHolder) {
        return new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
 
            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
 
            // 入力されたら、List内のデータを更新
            @Override
            public void afterTextChanged(Editable editable) {
                arrayList.set(viewHolder.getAdapterPosition(), editable.toString());
            }
        };
    }
}

SampAdapterで行っていること

SampAdapter の中では、次の処理をしています。

TextWatcher でEditTextの入力内容をArrayListに反映する

onCreateViewHolder() で1行分のViewを作る

onBindViewHolder() でデータを1行分のViewに設定する

移動ボタンに触れたときにドラッグを開始する

削除ボタンを押したときにデータを削除する

ViewHolder

ViewHolderは、RecyclerViewの1行分に配置されたウィジェットへの参照を保持するクラスです。

findViewById(R.id.edit_contents) のような参照取得を何度も行わなくて済むように、各ウィジェットへの参照をまとめて持っています。

このサンプルでは、次の参照を保持しています。

  • edit_contents
  • btn_move
  • btn_del
  • textWatcher

TextWatcher

TextWatcher は、EditTextに入力された内容を監視するための仕組みです。

このサンプルでは、EditTextに文字が入力されるたびに、ArrayList のデータを更新しています。

@Override
public void afterTextChanged(Editable editable) {
    arrayList.set(viewHolder.getAdapterPosition(), editable.toString());
}

RecyclerViewではViewが再利用されるため、入力欄に表示されている文字だけでなく、元データ側にも反映しておく必要があります。

また、再利用のたびにリスナーが増えないように、すでにTextWatcherがある場合は削除してから設定し直しています。

if (holder.textWatcher != null) {
    holder.edit_contents.removeTextChangedListener(holder.textWatcher);
}

MainActivity

次に、メイン画面を表示する MainActivity です。

MainActivity(クリックして表示)
package com.ma_chanblog.samprecyclerview;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback;
import java.util.ArrayList;
import java.util.Collections;
public class MainActivity extends AppCompatActivity {
    // データ格納用のList
    private ArrayList<String> arrayList;
    // アダプター
    private SampAdapter adapter;
    // ドラッグアンドドロップなどをするためのユーティリティクラス
    ItemTouchHelper itemTouchHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // データ準備
        arrayList = new ArrayList<>();
        for (int i=1; i < 6; i++) {
            arrayList.add("コンテンツ" + i);
        }
        // リサイクラービューへの参照を取得
        RecyclerView recyclerView = (RecyclerView)findViewById(R.id.mainList);
        // レイアウトマネージャーを準備
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        // レイアウトマネージャーを縦スクロールに設定
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        // リサイクラービューにレイアウトマネージャーを設定
        recyclerView.setLayoutManager(layoutManager);
        // アダプターを生成
        adapter = new SampAdapter(arrayList);
        // リサイクラービューにアダプターを設定
        recyclerView.setAdapter(adapter);
        // ドラッグアンドドロップで移動
        itemTouchHelper = new ItemTouchHelper(
                new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN ,
                        ItemTouchHelper.LEFT){
                    // 長押しで移動
                    @Override
                    public boolean onMove(@NonNull RecyclerView recyclerView,
                                          @NonNull RecyclerView.ViewHolder viewHolder,
                                          @NonNull RecyclerView.ViewHolder target) {
                        final int fromPos = viewHolder.getAdapterPosition();
                        final int toPos = target.getAdapterPosition();
                        // データを入れ替え
                        Collections.swap(arrayList, fromPos, toPos);
                        // 移動したことを通知
                        adapter.notifyItemMoved(fromPos, toPos);
                        return true;
                    }
                    // スワイプで削除
                    @Override
                    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
                        // アイテムを削除
                        arrayList.remove(viewHolder.getAdapterPosition());
                        // 削除したことを通知
                        adapter.notifyItemRemoved(viewHolder.getAdapterPosition());
                    }
                });
        // ItemTouchHelper を RecyclerView にアタッチ
        itemTouchHelper.attachToRecyclerView(recyclerView);
    }
    // 「+」フローティング操作ボタンがタップされたときに実行される
    public void onAddItem(View view) {
        // 新規のアイテムを追加
        arrayList.add("コンテンツ");
        // アイテムを追加したことを通知
        adapter.notifyItemInserted(arrayList.size() - 1);
    }
}

MainActivityで行っていること

MainActivity では、次の流れでRecyclerViewを準備しています。

  1. ArrayList に初期データを入れる
  2. RecyclerViewへの参照を取得する
  3. LinearLayoutManager を設定する
  4. SampAdapter を生成する
  5. RecyclerViewにAdapterを設定する
  6. ItemTouchHelper を設定する

LinearLayoutManager

RecyclerViewでは、リストをどのように並べるかを LayoutManager が管理します。

今回使っているのは、縦方向に並べる LinearLayoutManager です。

LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);

ItemTouchHelper

ドラッグ&ドロップでの並び替えや、スワイプでの削除には、ItemTouchHelper を使っています。

itemTouchHelper = new ItemTouchHelper(
        new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
                ItemTouchHelper.LEFT) {

この部分で、上下方向の移動と、左方向のスワイプを設定しています。

onMove()

onMove() は、アイテムがドラッグで移動したときに呼ばれます。

Collections.swap(arrayList, fromPos, toPos);
adapter.notifyItemMoved(fromPos, toPos);

ここでは、ArrayList 内のデータを入れ替えて、RecyclerViewに「移動した」と通知しています。

onSwiped()

onSwiped() は、アイテムがスワイプされたときに呼ばれます。

arrayList.remove(viewHolder.getAdapterPosition());
adapter.notifyItemRemoved(viewHolder.getAdapterPosition());

ここでは、ArrayList から該当データを削除し、RecyclerViewに「削除した」と通知しています。

移動ボタンからドラッグを開始する

このサンプルでは、移動ボタンに触れたタイミングで、アダプター側から次の処理を呼んでいます。

activity.itemTouchHelper.startDrag(holder);

これにより、移動ボタンからドラッグ操作を開始できます。

追加・移動・削除の流れ

追加・移動・削除の処理を図にすると、次のような流れです。

RecyclerViewでアイテムを追加、ドラッグして移動、削除する流れを順番に説明した図

アイテムを追加する

+ボタンを押すと、onAddItem() が呼ばれます。

public void onAddItem(View view) {
    arrayList.add("コンテンツ");
    adapter.notifyItemInserted(arrayList.size() - 1);
}

ここでは、新しい文字列を arrayList に追加し、RecyclerViewに追加されたことを通知しています。

アイテムを移動する

移動ボタンを押してドラッグすると、onMove() が呼ばれます。

Collections.swap() でデータの順番を入れ替え、notifyItemMoved() でRecyclerViewに知らせます。

アイテムを削除する

×ボタンを押したときは、アダプター側でその位置のデータを削除します。

int adapterPosition = holder.getAdapterPosition();
if (adapterPosition != -1) {
    arrayList.remove(adapterPosition);
    notifyItemRemoved(adapterPosition);
}

また、スワイプ削除では onSwiped() の中で同じように削除処理を行っています。

確認しておきたいところ

ドラッグしても移動できない

次の点を確認します。

  • ItemTouchHelper を作成しているか
  • attachToRecyclerView() でRecyclerViewに設定しているか
  • 移動ボタンの onTouch()itemTouchHelper.startDrag(holder) を呼んでいるか
itemTouchHelper.attachToRecyclerView(recyclerView);

EditTextの内容がスクロール後に変わる

RecyclerViewはViewを再利用します。

そのため、EditTextに入力した値を元データの ArrayList に反映しておかないと、スクロール後に表示がずれることがあります。

このサンプルでは、TextWatcher を使って入力内容を ArrayList に反映しています。

TextWatcherが重複する

onBindViewHolder() の中でTextWatcherを追加する場合、再利用のたびにリスナーが増えてしまうことがあります。

そのため、すでに設定済みのTextWatcherがある場合は、次のように削除してから追加しています。

if (holder.textWatcher != null) {
    holder.edit_contents.removeTextChangedListener(holder.textWatcher);
}

追加したアイテムが表示されない

arrayList.add() でデータを追加したあと、RecyclerViewに変更を通知しているか確認します。

adapter.notifyItemInserted(arrayList.size() - 1);

まとめ

この記事では、Android Studioで RecyclerViewを使ってデータを一覧表示し、ドラッグ&ドロップで移動、追加、削除する方法 を紹介しました。

ポイントは次のとおりです。

  • RecyclerViewでは、AdapterとViewHolderを使って一覧を表示する
  • 1行分の見た目は row_main.xml で定義する
  • LinearLayoutManager で縦方向のリストにできる
  • ItemTouchHelper を使うと、ドラッグ&ドロップやスワイプ削除ができる
  • EditTextの入力内容は、TextWatcherで元データ側にも反映する
  • 追加や削除をしたときは、RecyclerViewに変更を通知する

RecyclerViewは、ListViewよりも柔軟にリスト画面を作れるウィジェットです。

最初はAdapterやViewHolderなどの用語が多く感じるかもしれませんが、1つずつ役割を分けて見ると流れが見えやすくなります。

こちらは、関連記事です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次