RecyclerView の使い方。ドラッグ&ドロップで並び替え、スワイプで削除する。【Android】

2020年3月2日

RecyclerView の基本的な使い方を確認したので、備忘録として残しています。

ItemTouchHelper を使用して RecyclerView のアイテムをドラッグ&ドロップで並べ替え、スワイプで削除する方法についても確認しました。

試作品は以下のような感じです↓↓

RecyclerView で表示するアイテムには、複数のレイアウトを指定できます。

上記試作品でも、3種類のレイアウトファイルを使用しています。

  • TextView が2つあるレイアウト(ViewType1 の下にあるアイテム)
  • TextView が1つだけのレイアウト(ViewType2 の下にあるアイテム)
  • アイテムを区切るセクションタイトルのレイアウト(「ViewType1 」「ViewType2」 というテキストを表示しているアイテム)

上記試作品のソースコードは、GitHub にアップしています。

RecyclerView の基本

RecyclerView を使ってデータセットをリスト表示するためには、以下のクラスを実装します。

RecyclerView.Adapter


リスト表示したいデータセットを受け取り、リスト1行ごとのView (ViewHolder)を作成する役割を担うクラス。
行ごとに異なるレイアウトファイルを指定したりできる。

RecyclerView.ViewHolder


リストの1行として表示する View への参照を保持するクラス。
ViewHolder は Adapter で1行ごとに生成される。

RecyclerView.LayoutManager


RecyclerView に表示するアイテムの配置方法や、画面外に見えなくなったアイテムをどのように扱うかなどのポリシーを決定するクラス。
あらかじめ以下のようなサブクラスが提供されているので、基本的な用途ではこれらを使用することになる。

LinearLayoutManager ... 垂直または水平方向にアイテムを並べる場合に使用

GridLayoutManager ... アイテムをグリッド表示する場合に使用

StaggeredGridLayoutManager ... アイテムをスタッガードグリッド表示する場合に使用

RecyclerView まとめ
  1. Adapter によって RecyclerView に表示するデータを定義する
  2. RecyclerView の1行ごとの参照は、Adapter -> ViewHolder に保持されている
  3. RecyclerView のレイアウト(水平・垂直・グリッド表示など)は、LayoutManager によって管理される

レイアウトファイルの作成

RecyclerView を配置


RecyclerView のアイテム用のレイアウトを作成

試作品では3つのレイアウトファイルを作成しています。以下は1例になります。





Adapter の実装

RecyclerView に表示するデータとViewを結びつける Adapter を実装します。

アイテム行への参照を保持する ViewHolder も、Adapter の中で定義しています。


class SampleAdapter (var myDataset : ArrayList, var activity: MainActivity) :
    RecyclerView.Adapter(){

    
    /* ViewHolder の実装 */
    open class SampleViewHolder(v : View) : RecyclerView.ViewHolder(v) {
        var titleTextView : TextView = v.findViewById(R.id.title_text_view)
    }

    open class DefaultSampleViewHolder(v : View, activity: MainActivity) : SampleViewHolder(v) {

        var thumbnailImageView : ImageView = v.findViewById(R.id.thumbnail_image_view)
        var hamburgerImageView : ImageView = v.findViewById(R.id.hamburger_image_view)
        init {
           //省略...
        }
    }

    class SubSampleViewHolder(v : View, activity: MainActivity) : DefaultSampleViewHolder(v, activity){
        var subTitleTextView : TextView = v.findViewById(R.id.subtitle_text_view)
    }

    // ViewType に応じてレイアウトを決定し、View(ViewHolder)を作成する。
    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): SampleViewHolder {

        //viewType に応じてレイアウトを変更している。
        return when (viewType){

            RecyclerViewCustomViewType.SECTION.ordinal -> {
                val v = LayoutInflater.from(viewGroup.context)
                    .inflate(R.layout.sample_recyclerview_row_view_section, viewGroup, false)

                SampleViewHolder(v)
            }

            RecyclerViewCustomViewType.DEFAULT.ordinal -> {

                val v = LayoutInflater.from(viewGroup.context)
                    .inflate(R.layout.sample_recyclerview_row_view_default, viewGroup, false)

                DefaultSampleViewHolder(v, activity)
            }
            else -> {

                val v = LayoutInflater.from(viewGroup.context)
                    .inflate(R.layout.sample_recyclerview_row_view_sub, viewGroup, false)

                SubSampleViewHolder(v, activity)
            }
        }

    }

    //表示するView(ViewHolder)に、データを紐づける。
    override fun onBindViewHolder(holder : SampleViewHolder, position: Int){

        holder.titleTextView.text = myDataset[position].title

        if (holder is DefaultSampleViewHolder) holder.thumbnailImageView.setImageResource(myDataset[position].imageResource)
        if (holder is SubSampleViewHolder) holder.subTitleTextView.text = myDataset[position].subTitle

    }

    // データの数を返す
    override fun getItemCount(): Int = myDataset.size

    // アイテムのViewTypeを返す
    override fun getItemViewType(position: Int): Int {

        return when(myDataset[position].viewType){
            RecyclerViewCustomViewType.SECTION -> RecyclerViewCustomViewType.SECTION.ordinal
            RecyclerViewCustomViewType.SUB -> RecyclerViewCustomViewType.SUB.ordinal
            else -> RecyclerViewCustomViewType.DEFAULT.ordinal
        }
    }

}

RecycerView の設定をする

RecyclerView に Adapter を渡したり、LayoutManager を設定したりします。

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView : RecyclerView
    private lateinit var viewAdapter : RecyclerView.Adapter<*>
    private lateinit var viewManager : RecyclerView.LayoutManager
    private lateinit var myDataSet : ArrayList

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // テストデータの作成
        myDataSet = createDataSet()

        // RecyclerView の設定
        viewManager = LinearLayoutManager(this)
        viewAdapter = SampleAdapter(myDataSet, this)

        recyclerView = findViewById(R.id.recycler_view).apply{
            setHasFixedSize(true)
            layoutManager = viewManager
            adapter = viewAdapter
        }
         

ドラッグ & ドロップ、スワイプでリスト操作をする

RecyclerView でドラッグ&ドロップやスワイプ操作を行うには、ItemTouchHelper クラスを利用します。

ItemTouchHelper


RecyclerView でドラッグ&ドロップやスワイプ機能を追加するためのクラス。Callback クラスと連携して、イベントを受け取り処理を行う。

ItemTouchHelper.Callback


ユーザがドラッグ&ドロップやスワイプ操作をした際にコールバックを受け取る。ItemTouchHelper クラスのコンストラクタに渡す必要がある。

ItemTouchHelper.SimpleCallback


ItemTouchHelper.Callback のサブクラスで、よりシンプルにドラッグ&ドロップやスワイプ時のイベント処理を記述できる。

(今回はこちらを利用しました。)

 

SimpleCallback を実装する

ItemTouchHelper のコンストラクタに渡す必要がある Callback を実装します。

    
    private fun getRecyclerViewSimpleCallBack() =
        // 引数で、上下のドラッグ、および左方向のスワイプを有効にしている。
        object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN,
            ItemTouchHelper.LEFT) {

            // ドラッグしたときに呼ばれる
            override fun onMove(p0: RecyclerView, p1: RecyclerView.ViewHolder, p2: RecyclerView.ViewHolder): Boolean {

                // どこからどこへアイテムが移動するのか
                val fromPosition = p1.adapterPosition
                val toPosition = p2.adapterPosition

                /*
                * ドラッグ時、viewType が異なるアイテムを超えるときに、
                * notifyItemMoved を呼び出すと、ドラッグ操作がキャンセルされてしまう。
                * (ドラッグは同じviewTypeを持つアイテム間で行う必要がある模様)
                *
                * 同じ ViewType アイテムを超える時だけ notifyItemMoved を呼び出す。
                * */
                if (p1.itemViewType == p2.itemViewType) {
                    // Adapter の持つ実データセットを操作している
                    myDataSet.add(toPosition, myDataSet.removeAt(fromPosition))
                    // Adapter にアイテムが移動したことを通知
                    viewAdapter.notifyItemMoved(fromPosition, toPosition)
                }

                return true
            }

            // スワイプしたとき
            override fun onSwiped(p0: RecyclerView.ViewHolder, p1: Int) {
                p0.let{
                    // 実データセットからアイテムを削除
                    myDataSet.removeAt(p0.adapterPosition)
                    // adapter に通知
                    viewAdapter.notifyItemRemoved(p0.adapterPosition)
                }
            }

ItemTouchHelper の設定

先ほどのMainActivityに処理を追加します。

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView : RecyclerView
    private lateinit var viewAdapter : RecyclerView.Adapter<*>
    private lateinit var viewManager : RecyclerView.LayoutManager
    private lateinit var myDataSet : ArrayList

    // 追加
    private lateinit var itemTouchHelper : ItemTouchHelper


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        /* 省略 */

        // RecyclerView のドラッグ、スワイプ操作に関する設定
        itemTouchHelper = ItemTouchHelper(getRecyclerViewSimpleCallBack())
        itemTouchHelper.attachToRecyclerView(recyclerView)

    }

おわりに

結構省略してソースコードをのせています。。。

全体像は GitHub の方で確認していただければと思います!!!