RecyclerView に ObservableArrayList を DataBinding して変更通知で更新するで対応した変更通知に対応したRecyclerAdapterのお話。
ドラッグ&ドロップで並べ替え - 期待値…
ItemTouchHelper使ってドラッグ&ドロップで並べ替えを実装していた時に以下のような流れを想定していました。
ドラッグ
↓
ItemTouchHelperのコールバックにonMove()イベント
↓
ViewModelで保持している表示用のObservableArrayListを並び替え
↓
変更通知
↓
RecyclerAdapterを更新して描画
しかし、、、
タッチ操作でViewModelに保持しているObservableArrayListを更新 → 変更通知でAdapter側を更新するとうまく描画が更新されない。
Adapter側で保持しているListを直接更新して描画をかけるとうまくいく。まあ、間に変更通知挟んでAdapter更新したらリアルタイムに更新されるわけじゃないので上手くいかないのは当然といえば当然なのですが。
Adapter側を先に更新することにした
リアルタイムにAdapter側を更新して描画に追随させる。その結果をObservableArrayListに逆に変更通知するしかないということですね(たぶん
Adapter側の対応
そうと決まればもうあとは実装するだけ。
callbackと対象のObservableArrayListを保持するように変更
private val callback: ObservableList.OnListChangedCallback<ObservableList<CustomRecyclerItem>>
init {
items.addAll(recyclerItems)
callback = object : ObservableList.OnListChangedCallback<ObservableList<CustomRecyclerItem>>() {
〜〜〜 省略 〜〜〜
}
recyclerItems.addOnListChangedCallback(callback)
}
アイテムの入れ替え
表示用にAdapter側に保持しているListをCollection.swap()でまず入れ替えます。
その後、自身(Adapter)で変更通知を受け取りたくないので一旦callbackを外します。
ObservableArrayListを更新してViewModelへ通知。
またcallbackをaddして元どおり変更通知を受け取れるようにしています。
fun swapItem(from: Int, to: Int) {
if (from < 0 && to < 0) {
return
}
// アイテムの入れ替えはadapter側で操作しないとうまく表示されない
Collections.swap(items, from, to)
notifyItemMoved(from, to)
// adapter側で操作すると2重に表示が更新されてしまうので
// 一旦callbackを外して更新して再度セットする
recyclerItems.removeOnListChangedCallback(callback)
Collections.swap(recyclerItems, from, to)
recyclerItems.addOnListChangedCallback(callback)
}
Activity(Fragment)側の対応
あとはもう並べ替えのリスナー内でAdapterのswap()を呼び出すだけですね。
ドラッグ&ドロップで並べ替え
val helper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return if (viewHolder.itemViewType != TargetType) {
0
} else ItemTouchHelper.Callback.makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return if (target.itemViewType == TargetType) {
// 追加したswapメソッドを呼び出す
adapter.swapItem(viewHolder.adapterPosition, target.adapterPosition)
true
} else {
false
}
}
// swipeは使わないので何も設定しない
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
})
// helperをrecyclerViewに適用
helper.attachToRecyclerView(binding.recyclerView)
並べ替え操作などはqiitaで別の方が投稿してらっしゃったのでそちらを参考にされるのがいいかと思います。
(見失って参照できなかった)
懸念点
データの流れが単一方向じゃなくなるので、それを想定して作らないと予期せぬ不具合を生むかも。
終わり
ItemTouchHelper クラスという便利なクラスがあるのを今更知りました。ただ、私はコードを見て並べ替え機能があることを初めて知りました。そもそもパっと見て並べ替えができるという見た目にしておかないと、せっかく実装しても全然使われない機能になりそうです。
いろいろと機能を実装してもそれがユーザーに使われるかどうかはデザイン次第だなと改めて感じました。デザイナーとの連携を密にするためにも少しデザインの勉強をすることにします。
更新分も合わせて念のためAdapterの実装をまるっとアップしておこうか…
以上です。