【Android】RecyclerViewをドラッグ&ドロップで移動、追加・削除

Android RecyclerView

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

今回作成するのは、RecyclerViewにデータを一覧表示する画面です。

一覧表示画面 右下の[+]ボタンをタップするとデータが1行追加されます。

リストをタップするとデータを編集できます。

リスト右側の移動ボタンをドラッグ&ドロップすると、データを移動できます。

リスト右側の ×(削除) ボタンをタップすると、データが削除されます。

Android RecyclerView

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

Android Studioのインストール方法と簡単な使い方については、こちらの記事で紹介しています。

目次

構成

RecyclerView(リサイクラービュー)は、ListViewよりも自由度の高いリストを生成するためのウィジェットです。

ビューをリサイクルしながらリストを処理するため、大きなデータを扱うことができます。

今回のプロジェクトのオブジェクト構成は、このようになっています。

Android constitution

メインアクティビティ(MainActivity)は、メイン画面のレイアウト(activity_main.xml)を使ってメイン画面を表示します。

アダプターは、1行分のレイアウト(row_main.xml)内の各ウィジェットへの参照を保持するビューホルダーにデータを割り当て、RecyclerViewに設定します。

表示に使用する文字列などは、リソースに定義しているstrings.xmlなどを参照します。

追加ボタンの「+」や削除ボタンなどの「×」アイコンは、drawable に追加して使用します。

順を追って、詳しく説明していきます。

プロジェクト作成

Android Studioをインストール」の「プロジェクト作成」で紹介した手順で、プロジェクトを作成します。

プロジェクト名は、 「SampRecyclerView」 としました。

リソース準備

アプリ内から参照する次のリソースを準備します。

strings.xml

アプリ内で使用する文字列の定義をします。

app/res/vlues/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を指定して、プログラムやレイアウトから参照します。

アイコン

アイコンは、前回の記事「【Android Studio】SQLiteデータベースをListViewに一覧表示&削除ボタンでDelete!」と同じように「+」と「×」アイコンを追加します。

今回はさらに、移動ボタンとして使用するアイコン(ic_baseline_unfold_more_24.xml)も追加します。

android move

追加方法については、記事内のアイコンの部分を参照してください。

画面レイアウト

activity_main.xml

app/res/values/activity_main.xmlに、メイン画面のレイアウトを設定します。

android main screen
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に、RecyclerViewとFloatingActionButtonのみ配置しています。

row_main.xml

row_main.xmlでリストの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>

リストの内容を入力するEditTextと移動用、削除用の ImageButton を配置しています。

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

アダプター(Adapter)とは、データとウィジェットを関連付け、橋渡しをしてくれるオブジェクトです。

今回のアプリでは、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の中では、インナークラスとして、ビューホルダー(ViewHolder)を定義し、そのビューホルダーの生成やデータの割り当て、ボタンにタッチしたときの処理などを記述しています。

ビューホルダー

ビューホルダーは、アダプターで利用するビューを保持するクラスです。

findViewById(R.id.edit_contents)といったコストのかかるウィジェットの参照取得を何度も行わなくていいように、個々のウィジェットへの参照を保持しておくクラスで、処理は記述しません。

テキストウォッチャー

EditTextに入力した内容がスクロールしても消えないようにしているのが、テキストウォッチャーを使った処理の部分です。

EditTextにテキストウォッチャーのリスナーを設定し、文字が入力されるたびにListを更新しています。

ただ、そのままリスナーを追加してしまうと、スクロールしてリサイクルされるたびに、リスナーが追加されていってしまいます。

2重、3重にリスナーが追加されてしまうことで、データの更新処理がおかしくなってしまうので、既にリスナーがある場合は削除してから追加しなおす処理としています。

アクティビティ

メイン画面を表示する 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(adapter.getItemCount()+1);
    }
}

MainActivityでは、レイアウトを管理するレイアウトマネージャーをリサイクラービューに設定しています。

レイアウトマネージャーの種類はいくつかありますが、今回はリストを縦・横に並べるLinearLayoutManagerを使用しています。

アダプターを生成してリサイクラービューに設定します。

ドラッグアンドドロップによる並び替えの処理は、ItemTouchHelper.SimpleCallbackを使って行っています。

ItemTouchHelperとは、RecyclerViewでドラッグアンドドロップなどの処理をするためのユーティリティクラスです。

onMove()とonSwiped()メソッドをオーバーライドしています。

リストを長押ししたときに、onMove()メソッドが呼ばれ、ドラッグアンドドロップによる移動ができます。

(移動アイコンがタッチされたときの処理は、アダプターからitemTouchHelper.startDrag()を呼んでいます。)

リストをスワイプした時と削除ボタンをタップした時に、リストが削除されます。

参考にさせていただいたサイト

こちらのサイトを参考にさせていただきました。ありがとうございます。
アンドロイドで RecyclerView のアイテムを「優しく」ドラッグ操作する
[Android] RecyclerViewとItemTouchHelperでドラッグ&ドロップ

まとめ

Android Studioで、RecyclerViewを使ってデータを一覧表示する方法を紹介しました。

RecyclerViewをドラッグ&ドロップして簡単にデータの並べ替えができる機能はとっても便利だと思います。

RecyclerViewは、他にもいろいろカスタマイズができるようなので、また試してみたいと思います。

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