RecyclerView:アイテムの変更(Change/Insert/Move/Remove)

投稿日:  更新日:

RecyclerViewはアイテムを一覧表示してくれます。

ただ一覧表示するだけではなく、「追加・削除・移動・切り替え」といったアイテムの表示を効率よく変更する仕組み持っています。

今回はこの仕組みを使ったアイテムの変更方法を紹介します。

スポンサーリンク

アイテムの変更手順

アイテムの変更を行う手順は次の通りです。

  • (1)データの変更
  • (2)変更の通知(表示を更新)

(1)データの変更

アイテムデータはアダプターが参照(Kotlinの引数は参照渡し)しています。

参照元のデータを変更すれば、変更内容がそのままアダプターに伝わります。

ただし、全てのアイテムデータを入れ替えるために、参照先を変更する場合は注意してください。

例えばデータがCursorなどの場合、旧参照先の解放(Cursor#close)を忘れてはいけません。

(2)変更の通知(表示を更新)

データの変更が終わったら、アダプターへ変更の通知(notifyメソッドの実行)を行います。

変更の通知を受け取ったアダプターは、アイテムの表示を更新するようにRecyclerViewへ要求を出します。

変更箇所のみを更新するように、変更内容に合ったnotifyメソッドが用意されています。

 notifyメソッド変更箇所observerメソッド
更新notifyDataSetChanged( )全データonChanged
変更notifyItemChanged(position: Int)positiononItemRangeChanged
notifyItemRangeChanged(
  positionStart: Int, itemCount: Int)
positionStartから
itemCount個
挿入notifyItemInserted(position: Int)positiononItemRangeInserted
notifyItemRangeInserted(
  positionStart: Int, itemCount: Int)
positionStartから
itemCount個
削除notifyItemRemoved(position: Int)positiononItemRangeRemoved
notifyItemRangeRemoved(
  positionStart: Int, itemCount: Int)
positionStartから
itemCount個
移動
from⇒to
notifyItemMoved(
  fromPosition: Int, toPosition: Int)
fromPosition
toPosition
onItemRangeMoved
変更
payload付
notifyItemChanged(
  position: Int,
  payload: Any?)
positiononItemRangeChanged
notifyItemRangeChanged(
 positionStart: Int, itemCount: Int,
 payload: Any?)
positionStartから
itemCount個

常に全てのアイテムの表示を更新させることも可能ですが、変更のない箇所を更新することは無駄です。

端末のリソース(CPUの処理能力、バッテリー、など)消費の低減やパフォーマンスを確保するために、notifyメソッドは使い分けるようにしましょう。

スポンサーリンク

アイテムの変更例

アイテムの変更手順を先に示しました。

アイテムデータがListで構成されたアダプターを題材にして、アイテムの変更を行う例をあげます。

ListAdapter(アイテムの変更例のサンプル)
class ListAdapter(var items: MutableList<String>)
    : RecyclerView.Adapter<ListAdapter.ListViewHolder>() {

    // ----- ビューホルダー ------------------------------------------
    class ListViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
        val mesg = view.findViewById<TextView>(R.id.txtMesg)
    }

    // ----- アダプター本体 ------------------------------------------
    override fun getItemCount(): Int {
        return items.size                   // アイテム数を返す
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
    : ListViewHolder {
        val _view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item, parent, false)
        return ListViewHolder(_view)        // ViewHolderを返す
    }

    override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
        holder.mesg.text = items[position]  // 要素とデータを紐づける
    }
}

全データを更新(notifyDataSetChanged)

アイテムのデータの参照先を変更して、全データを更新しています。

        ...
        val _items = mutableListOf(
                "Item 0", "Item 1", "Item 2", "Item 3",
                "Item 4", "Item 5", "Item 6", "Item 7",
                "Item 8", "Item 9", "Item A", "Item B",
                "Item C", "Item D", "Item E", "Item F"
        )
		change(_items)
		...
    }

    private fun change(items: MutableList<String>) {
        val _adapter = rcySample.adapter as ListAdapter
        _adapter.items = items                                  // データの変更
        _adapter.notifyDataSetChanged()                         // 変更の通知(表示を更新)
    }

アイテムを変更(notifyItemChanged)

ポジション3のアイテムを変更しています。

        ...
		change(3, "Item X(Change)")
		...
	}

    private fun change(position: Int, item: String) {
        val _adapter = rcySample.adapter as ListAdapter
        _adapter.items[position] = item                         // データの変更
        _adapter.notifyItemChanged(position)                    // 変更の通知(表示を更新)
    }

指定範囲を変更(notifyItemRangeChanged)

ポジション3の位置から3つのアイテムを変更しています。

        ...
		val _items = arrayOf("Item X(Change)", "Item Y(Change)", "Item Z(Change)")
        change(3, _items)
        ...
    }
			
    private fun change(position: Int, items: Array<String>) {
        val _adapter = rcySample.adapter as ListAdapter
        for(i in 0 until items.size)                            // データの変更
            _adapter.items[position + i] = items[i]
        _adapter.notifyItemRangeChanged(position, items.size)   // 変更の通知(表示を更新)
    }

アイテムを挿入(notifyItemInserted)

ポジション3へアイテムを挿入しています。

        ...
		insert(3, "Item X(Insert)")
		...
	}
	
    private fun insert(position: Int, item: String) {
        val _adapter = rcySample.adapter as ListAdapter
        _adapter.items.add(position, item)                      // データの変更
        _adapter.notifyItemInserted(position)                   // 変更の通知(表示を更新)
    }

指定個数を挿入(notifyItemRangeInserted)

ポジション3へ3つのアイテムを挿入しています。

        ...
        val _items = arrayOf("Item X(Insert)", "Item Y(Insert)", "Item Z(Insert)")
        insert(3, _items)
		...
    }
			
    private fun insert(position: Int, items: Array<String>) {
        val _adapter = rcySample.adapter as ListAdapter
        for(i in 0 until items.size)                            // データの変更
            _adapter.items.add(position + i, items[i])
        _adapter.notifyItemRangeInserted(position, items.size)  // 変更の通知(表示を更新)
    }

アイテムを削除(notifyItemRemoved)

ポジション3のアイテムを削除しています。

        ...
		remove(3)
		...
    }
	
    private fun remove(position: Int) {
        val _adapter = rcySample.adapter as ListAdapter
        _adapter.items.removeAt(position)                       // データの変更
        _adapter.notifyItemRemoved(position)                    // 変更の通知(表示を更新)
    }

指定範囲を削除(notifyItemRangeRemoved)

ポジション3の位置から3つのアイテムを削除しています。

        ...
		remove(3, 3)
		...
    }

    private fun remove(position: Int, count: Int) {
        val _adapter = rcySample.adapter as ListAdapter
        for(i in 0 until count)                                 // データの変更
            _adapter.items.removeAt(position)
        _adapter.notifyItemRangeRemoved(position, count)        // 変更の通知(表示を更新)
    }

アイテムを移動(notifyItemMoved)

ポジション2のアイテムをポジション5へ移動しています。

        ...
		move(2, 5)
		...
	}
	
    private fun move(from: Int, to: Int) {
        val _adapter = rcySample.adapter as ListAdapter
        val _item = _adapter.items[from]                        // データの変更
        _adapter.items.removeAt(from)
        _adapter.items.add(to, _item)
        _adapter.notifyItemMoved(from, to)                      // 変更の通知(表示を更新)
    }

スポンサーリンク

「変更の通知(表示を更新)」の裏側

notifyItemChange( )を例に、notifyメソッドの動作を追ってみましょう。

AdapterDataObserverのメソッドが実行される

追っていくと、AdapterDataObservable#mObserversから要素を取り出し(get(i)の部分)、要素が持つメソッド(onItemRangeChanged)を実行していることが分かります。

※***Observableと***Observersの2つが登場します。名前が似ているので間違えないように!

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
		
    ...     // 6984行近辺
    public abstract static class Adapter<VH extends ViewHolder> {
        private final AdapterDataObservable mObservable = new AdapterDataObservable();
		
		... // 7369行近辺
		public final void notifyItemChanged(int position) {
            mObservable.notifyItemRangeChanged(position, 1);
        }
		...
	}
	
	...     // 12242行近辺
	static class AdapterDataObservable extends Observable<AdapterDataObserver> {
        public boolean hasObservers() {
            return !mObservers.isEmpty();
        }
        ...

        public void notifyItemRangeChanged(int positionStart, int itemCount) {
            notifyItemRangeChanged(positionStart, itemCount, null);
        }

        public void notifyItemRangeChanged(int positionStart, int itemCount,
                @Nullable Object payload) {
            ...
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
            }
        }
		...
    }
	...
}

mObserversとは、AdapterDataObservableのSuperクラス:Observableが持つフィールドです。

単なるListでAdapterDataObserver型を格納(ジェネリクス:Tで指定)します。

public abstract class Observable<T> {
    ...
    protected final ArrayList<T> mObservers = new ArrayList<T>();

    ...
    public void registerObserver(T observer) {
        ...
        synchronized(mObservers) {
            ...
            mObservers.add(observer);
        }
    }

    ...
    public void unregisterObserver(T observer) {
        ...
        synchronized(mObservers) {
            ...
            mObservers.remove(index);
        }
    }

つまり、AdapterDataObserverのメソッドonItemRangeChanged( )が実行されます。

デフォルトのRecyclerViewDataObserver

アダプターのインスタンスを作成した時点は、mObserversの要素はありません。

要素がないのでnotifyメソッドを実行しても何も起こらないはずですが、アイテムの更新が行われます。

これは、RecyclerViewへアダプターを設定する時に、RecyclerViewDataObserverが登録されるからです。

つまり、mObserversはデフォルトでRecyclerViewDataObserverを持ち、notifyメソッドの実行でRecyclerViewDataObserver#onItemRangeChanged( )が実行されます。

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
		
    ... // 365行近辺
	private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
	
    ... // 1158行近辺
	public void setAdapter(@Nullable Adapter adapter) {
        // bail out if layout is frozen
        setLayoutFrozen(false);
        setAdapterInternal(adapter, false, true);
        processDataSetCompletelyChanged(false);
        requestLayout();
    }
	
	... // 1195行近辺
    private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
            boolean removeAndRecycleViews) {
        ...
        mAdapter = adapter;
        if (adapter != null) {
            adapter.registerAdapterDataObserver(mObserver);
            adapter.onAttachedToRecyclerView(this);
        }
        ...
    }
	
    ...
    public abstract static class Adapter<VH extends ViewHolder> {
        private final AdapterDataObservable mObservable = new AdapterDataObservable();
		
		... // 7286行近辺
	    public void registerAdapterDataObserver(@NonNull AdapterDataObserver observer) {
            mObservable.registerObserver(observer);
        }

        ... // 7300行近辺
        public void unregisterAdapterDataObserver(@NonNull AdapterDataObserver observer) {
            mObservable.unregisterObserver(observer);
        }
		...
	}
}

RecyclerViewDataObserverの実装

RecyclerViewDataObserverは、変更内容に合ったアイテムの描画コマンドをAdapterHelperクラスへ設定した後に、View#requestLaytout( )を発行してRecyclerViewの再描画を依頼するのが仕事です。

RecyclerViewはこの描画コマンドを見てアイテムの描画を行います。

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {

    ... // 398行近辺
    final Runnable mUpdateChildViewsRunnable = new Runnable() {
        @Override
        public void run() {
            if (!mFirstLayoutComplete || isLayoutRequested()) {
                // a layout request will happen, we should not do layout here.
                return;
            }
            if (!mIsAttached) {
                requestLayout();
                // if we are not attached yet, mark us as requiring layout and skip
                return;
            }
            if (mLayoutSuppressed) {
                mLayoutWasDefered = true;
                return; //we'll process updates when ice age ends.
            }
            consumePendingUpdateOperations();
        }
    };
		
    ... // 5530行近辺
    private class RecyclerViewDataObserver extends AdapterDataObserver {
        ...
        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
            assertNotInLayoutOrScroll(null);
            if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
                triggerUpdateProcessor(); //←requestLayout( )発行↑描画コマンド設定
            }
        }
        ...
        void triggerUpdateProcessor() {
            if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
                ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
            } else {
                mAdapterUpdateDuringMeasure = true;
                requestLayout();
            }
        }
	}
	...
}
スポンサーリンク

Payload付きnotifyメソッド

notifyItemChangedとnotifyItemRangeChangedは引数にpayloadを持つものが用意されています。

このメソッドが呼び出された場合、Adapter#onBindViewHolder( )の引数へpayloadsが渡されます。

アイテムの変更例

アイテムを変更(notifyItemChanged)

ポジション3のアイテムを変更しています。

        ...
		change(3, "Item X(Change)")
		...
	}

    private fun change(position: Int, item: String) {
        val _adapter = rcySample.adapter as ListAdapter
        _adapter.notifyItemChanged(position, item)              // 変更の通知(表示を更新)
    }
class ListAdapter(var items: MutableList<String>)
    : RecyclerView.Adapter<ListAdapter.ListViewHolder>() {

    ...
    override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
        holder.mesg.text = items[position]  // 要素とデータを紐づける
    }

    override fun onBindViewHolder(
	    holder: ListViewHolder, position: Int, payloads: MutableList<Any>) {
        payloads?.let {
            if(it.size > 0)
                items[position] = it[0] as String
        }
        super.onBindViewHolder(holder, position, payloads)
    }
	
	...
}

指定範囲を変更(notifyItemRangeChanged)

ポジション3の位置から3つのアイテムを変更しています。

        ...
		change(3, 3, "Item Y(Change)")
		...
	}

    private fun change(position: Int, count: Int, item: String) {
        val _adapter = rcySample.adapter as ListAdapter
        _adapter.notifyItemRangeChanged(position, count, item)   // 変更の通知(表示を更新)
    }
class ListAdapter(var items: MutableList<String>)
    : RecyclerView.Adapter<ListAdapter.ListViewHolder>() {

    ...
    override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
        holder.mesg.text = items[position]  // 要素とデータを紐づける
    }

    override fun onBindViewHolder(
	    holder: ListViewHolder, position: Int, payloads: MutableList<Any>) {
        payloads?.let {
            if(it.size > 0)
                items[position] = it[0] as String
        }
        super.onBindViewHolder(holder, position, payloads)
    }
	
	...
}

※payloadsはListですが、複数のデータをListに含めて渡す方法が不明です。

注意点

onBindViewHolder( )は(1)payloads有りと(2)payloads無しの2つメソッドがあります。

public class RecyclerView extends ViewGroup implements ScrollingView,
        NestedScrollingChild2, NestedScrollingChild3 {
    ...
    public abstract static class Adapter<VH extends ViewHolder> {
	    ... // 7032行近辺 ↓(2)payloads無し
        public abstract void onBindViewHolder(@NonNull VH holder, int position);
        ... // 7063行近辺 ↓(1)payloads有り
        public void onBindViewHolder(@NonNull VH holder, int position,
                @NonNull List<Object> payloads) {
            onBindViewHolder(holder, position);    // (2)payloads無しが呼ばれる
        }
		...
	}
	...
}

payloadsを使用するカスタムAdapterを作成する時、(1)payloads有りのonBindViewHolderをオーバーライドすればよいのですが、(2)payloads無しのonBindViewHolderはabstractなので必ず実装が必要です。

また、onBaindViewHolder( )が実行される時、外部からは(1)payloads有りのonBindViewHolderが呼ばれます。(2)payloads無しのonBindViewHolderは(1)payloads有り側から呼ばれる下位メソッドになっています。

スポンサーリンク

関連記事:

RecyclerViewは子Viewを並べて表示するコンテナタイプ(ConstraintLayoutと同じ)のViewです。 複数のデータをスクリーン上に一覧表示したい時、例えば電話帳のような「氏名+住所+電話番号」の一覧を表示する場合などに最適です。 アプリを開発していると一覧表示したいデータが多いことに気付きます。 なのでRecyclerViewはとても重要で重宝するViewです。 しかし、思い通りの表示を行わせるためのテクニックが多すぎて、使いこなしが難しいです。 今まで調べたテクニックを忘れないように、整理して書き残そうと思います。 今回は基本の「RecyclerViewの実装」です。 ...
続きを読む
RecyclerViewでアイテムのクリックイベントを取得し、処理を実行する方法を紹介します。 ...
続きを読む
RecyclerViewはアイテムのレイアウトをアイテム毎に変更できます。その時に使う値がViewTypeです。 ViewTypeでアイテムのレイアウトを変更する方法を紹介します。 ...
続きを読む
RecyclerViewは表示が変更される(アイテムの更新、スクロール)時、アイテムのViewをリサイクル(再生利用)します。 これにより余分なViewの作成が行われなくなり、メモリーの節約とパフォーマンスの向上が望めます。 リサイクルはCachedViewsとRecyclerPoolという2つのキャッシュで行われます。 このキャッシュを使ったリサイクルの動作を調べたので紹介します。 ...
続きを読む
RecyclerViewのリサイクル動作で使われるキャッシュは、サイズを大きくすれば多くのViewHolderが保持できます。その分、多くのメモリを消費します。 ViewHolderを多く保持できたとしても、サイクル動作で効率よく使われなければ、メモリの浪費です。 キャッシュのサイズはRecyclerViewの使われ方よって適切なサイズがあります。 そのため、RecyclerViewはキャッシュのサイズを変更できるようになっています。 ...
続きを読む
RecyclerViewのリサイクル動作で使われるキャッシュは、サイズを大きくすれば多くのViewHolderが保持できます。その分、多くのメモリを消費します。 ViewHolderを多く保持できたとしても、サイクル動作で効率よく使われなければ、メモリの浪費です。 キャッシュのサイズはRecyclerViewの使われ方よって適切なサイズがあります。 そのため、RecyclerViewはキャッシュのサイズを変更できるようになっています。 ...
続きを読む
RecyclerViewが空(アイテムが無い)の時、EmptyViewを表示する実装を行ったので紹介します。 ...
続きを読む
RecyclerView上のアイテムを選択する方法を紹介します。 外部ライブラリー(AndroidX)で提供されるrecyclerview-selection APIを用いた方法です。 ...
続きを読む
RecyclerViewはListView(RecyclerViewの前身)の時に存在していたChoiceModeがありません。 同様な機能が欲しければプログラマ側で実装しなければなりません。 RecyclerView.AdapterをカスタマイズしてChoiceModeを実装してみたので紹介します。 ...
続きを読む
RecyclerViewはアイテムへ装飾を付けることが出来るようになっています。 装飾とは、例えばアイテムの区切り線などです。 今回はアイテムへ装飾を付ける方法を紹介します。 ...
続きを読む
RecyclerViewに表示しきれなかったアイテムはスクロールを行うことで表示されるようになっています。 スクロールは「外部入力(指でスクリーン上をタッチしてスライド)によるスクロール」の他に、「プログラムによるスクロール」をすることも出来ます。 今回はこのアイテムのスクロールについてまとめてみました。 ...
続きを読む
RecyclerViewはアイテムをスクロールさせて隠れたアイテムを表示できます。 スクロールを止める位置は任意です。 任意であるがゆえに、止めた位置によってはアイテムの一部が欠けてしまうこともあります。 携帯端末の画面は狭いので、効率よくコンテンツの表示を行いたいとアプリ開発者は考えます。 アイテムの一部が欠けてしまうことは、効率が良いとは言えません。 このような問題を解決するために、アイテムのスナップをRecyclerViewへ追加できます。 アイテムのスナップを追加する方法を紹介します。 ...
続きを読む
RecyclerViewでアイテムの変更(Change/Insert/Move/Remove)を行うと、変更される様子がアニメーション化されています。 これはデフォルトでアイテムの変更アニメーションが組み込まれているためです。 デフォルトは単純なアニメーションですが、「ある」と「ない」の違いは歴然で、アニメーションのある方が高価なアプリケーションに見えます。 GUI(Graphical User Interface)が主体の携帯端末にとって、利用者に対するアプリの見せ方は重要です。高価に見えた方が使ってもらえる可能性が高くなります。 上記のことから、アプリの機能に関係なくても、ちょっとした動きをアニメーション化するメリットがあります。 2回にわたりアイテムの変更アニメーションについてまとめてみました。 アイテムの変更アニメーション(DefaultItemAnimator、デフォルト) アイテムの変更アニメーション(SimpleItemAnimatorの継承、カスタム) 今回は第1回目「ItemAnimator、デフォルト」編です。デフォルト変更アニメーションの動作について説明します。 ...
続きを読む
RecyclerViewでアイテムの変更(Change/Insert/Move/Remove)を行うと、変更される様子がアニメーション化されています。 これはデフォルトでアイテムの変更アニメーションが組み込まれているためです。 デフォルトは単純なアニメーションですが、「ある」と「ない」の違いは歴然で、アニメーションのある方が高価なアプリケーションに見えます。 GUI(Graphical User Interface)が主体の携帯端末にとって、利用者に対するアプリの見せ方は重要です。高価に見えた方が使ってもらえる可能性が高くなります。 上記のことから、アプリの機能に関係なくても、ちょっとした動きをアニメーション化するメリットがあります。 2回にわたりアイテムの変更アニメーションについてまとめてみました。 アイテムの変更アニメーション(DefaultItemAnimator、デフォルト) アイテムの変更アニメーション(SimpleItemAnimatorの継承、カスタム) 今回は第2回目「SimpleItemAnimatorの継承、カスタム」編です。カスタム変更アニメーションの作り方を説明 ...
続きを読む
RecyclerViewへアイテムが表示されるとき、アニメーションはありません。。一瞬で表示されて終わりです。 「RecyclerViewへアイテムが表示される」ことを、ここでは「アイテムの出現」と言い表すことにします。 このアイテムの出現にアニメーションを付ける方法を紹介します。 アイテムの出現をアニメーションで演出することで、RecyclerViewに表示したい内容が際立つと思います。 ...
続きを読む
スポンサーリンク