何も考えずに実装した場合に発生する現象
マルチタップをRecyclerViewに実装しようと考えているときに発生した現象でした.クリックされたときに色を変更する処理を以下のようにRecyclerView.AdapterクラスのonBindViewHolderで仮実装しました.
aHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { int color = Color.CYAN; view.setBackgroundColor(color); return false; } });
まぁこれでよしなにやってくれるんだろうと思っていたら,全然思い通りの挙動になっていませんでした.確かにロングクリックするとそのViewの背景色はシアンに変更されますが,スクロールしてみるとなぜか一部のViewまで背景色が変更されていました.しかも元の場所にスクロールし直すとその背景色が元に戻るという訳分からん状態に・・・.調べた結果以下に行きつきました.
Recycler ViewのViewは再利用される
今更何を言っているんだと言われそうですが,Recycler Viewはその名の通り,View部分を再利用する実装となっています.したがって,Recycler Viewをスクロールした際に画面外に出たViewはRecyclerView.Holderをクリアして,新しいRecyclerView.Holderをセットして再利用されます.ViewのtoStringを呼ぶことでhashCodeを含めた情報を見ることが出来るので確認してみます.
//はじめのViewのtoString androidx.cardview.widget.CardView{f90079b VFE…CL. …P…. 0,1201-540,1392}
//スクロールした先のViewのtoString androidx.cardview.widget.CardView{f90079b VFE…CL. …P…. 0,0-540,191}
CardViewで実装しているためCardViewのtoStringが表示されていますが,太文字部分がhashCodeに対応しています.見事にhashCodeが同一のviewが存在していました.すなわち裏側では以下のような流れができていた訳です.
- クリック
- view①の背景色を変更
- view①が画面外にスクロールされた時点でリサイクルされる
- 新しいviewにview①が利用され,2でセットした背景色がそのまま保持されている
ここまで分かってしまえばこちらのもので,対策としては2通り考えられます.
Recycleされないようにする
RecyclerViewの良さを消しているので非推奨ですが,Viewの数が多くない場合はこれで簡便化してもよいと思います.以下を各holderに対して呼び出すことで再利用しないように設定できます.
aHolder.setIsRecyclable(false);
onBindViewHolderで毎回背景色をセットする
これが一番楽ですね.なんで初めからこうしなかったのか.背景色のリストやnotify形式でも良いですがとにかく表示されるタイミングでセットするのが安全です.
@Override public void onBindViewHolder(RecordsViewHolder aHolder, int aPosition){ aHolder.itemView.setBackgroundColor(oColorList.get(aPosition)); }
最後に
こうして実際に体感してみるとRecyclerViewはよくできた仕組みですね.Viewのインスタンス数をかなり抑制できるので,相当軽くなっていると思います.どの閾値でViewを再利用しているのか調べていませんが,せいぜい画面内のView+上下1つずつぐらいでしょうから,高々10個程度のインスタンス数で済みます.