mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-05-20 04:39:04 +00:00
Fixed problem with RealmRecyclerViewAdapter wanting to autoupdate data and clashing with already applied view positioning from drag'n'drop
This commit is contained in:
parent
2e6b8a88f8
commit
0f87874ede
3 changed files with 111 additions and 60 deletions
|
|
@ -3,6 +3,7 @@ package com.habitrpg.android.habitica.ui.adapter.tasks
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
|
||||
import com.habitrpg.android.habitica.models.responses.TaskDirection
|
||||
import com.habitrpg.android.habitica.models.tasks.ChecklistItem
|
||||
|
|
@ -12,38 +13,86 @@ import io.reactivex.BackpressureStrategy
|
|||
import io.reactivex.Flowable
|
||||
import io.reactivex.functions.Action
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import io.realm.OrderedRealmCollection
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.RealmRecyclerViewAdapter
|
||||
import io.realm.*
|
||||
|
||||
abstract class RealmBaseTasksRecyclerViewAdapter<VH : BaseTaskViewHolder>(private var unfilteredData: OrderedRealmCollection<Task>?, private val hasAutoUpdates: Boolean, private val layoutResource: Int, private val taskFilterHelper: TaskFilterHelper?) : RealmRecyclerViewAdapter<Task, VH>(null, true), TaskRecyclerViewAdapter {
|
||||
abstract class RealmBaseTasksRecyclerViewAdapter<VH : BaseTaskViewHolder>(
|
||||
private var unfilteredData: OrderedRealmCollection<Task>?,
|
||||
private val hasAutoUpdates: Boolean,
|
||||
private val layoutResource: Int,
|
||||
private val taskFilterHelper: TaskFilterHelper?
|
||||
) : RealmRecyclerViewAdapter<Task, VH>(null, false), TaskRecyclerViewAdapter {
|
||||
private var updateOnModification: Boolean = false
|
||||
override var ignoreUpdates: Boolean = false
|
||||
private val listener: OrderedRealmCollectionChangeListener<OrderedRealmCollection<Task>> by lazy {
|
||||
OrderedRealmCollectionChangeListener<OrderedRealmCollection<Task>> { _, changeSet ->
|
||||
if (ignoreUpdates) {
|
||||
return@OrderedRealmCollectionChangeListener
|
||||
}
|
||||
// null Changes means the async query returns the first time.
|
||||
// For deletions, the adapter has to be notified in reverse order.
|
||||
val deletions = changeSet.deletionRanges
|
||||
deletions.indices.reversed()
|
||||
.map { deletions[it] }
|
||||
.forEach { notifyItemRangeRemoved(it.startIndex, it.length) }
|
||||
|
||||
val insertions = changeSet.insertionRanges
|
||||
for (range in insertions) {
|
||||
notifyItemRangeInserted(range.startIndex, range.length)
|
||||
}
|
||||
private val resultsListener: OrderedRealmCollectionChangeListener<RealmResults<Task>> by lazy {
|
||||
OrderedRealmCollectionChangeListener<RealmResults<Task>> { _, changeSet ->
|
||||
buildChangeSet(changeSet)
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateOnModification) {
|
||||
return@OrderedRealmCollectionChangeListener
|
||||
}
|
||||
private val listListener: OrderedRealmCollectionChangeListener<RealmList<Task>> by lazy {
|
||||
OrderedRealmCollectionChangeListener<RealmList<Task>> { _, changeSet ->
|
||||
buildChangeSet(changeSet)
|
||||
}
|
||||
}
|
||||
|
||||
val modifications = changeSet.changeRanges
|
||||
for (range in modifications) {
|
||||
notifyItemRangeChanged(range.startIndex, range.length)
|
||||
}
|
||||
private fun buildChangeSet(changeSet: OrderedCollectionChangeSet) {
|
||||
if (ignoreUpdates) return
|
||||
if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) {
|
||||
notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
// For deletions, the adapter has to be notified in reverse order.
|
||||
val deletions = changeSet.deletionRanges
|
||||
for (i in deletions.indices.reversed()) {
|
||||
val range = deletions[i]
|
||||
notifyItemRangeRemoved(range.startIndex + dataOffset(), range.length)
|
||||
}
|
||||
|
||||
val insertions = changeSet.insertionRanges
|
||||
for (range in insertions) {
|
||||
notifyItemRangeInserted(range.startIndex + dataOffset(), range.length)
|
||||
}
|
||||
|
||||
if (!updateOnModification) {
|
||||
return
|
||||
}
|
||||
|
||||
val modifications = changeSet.changeRanges
|
||||
for (range in modifications) {
|
||||
notifyItemRangeChanged(range.startIndex + dataOffset(), range.length)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
data?.takeIf { it.isValid }?.addListener()
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView)
|
||||
data?.takeIf { it.isValid }?.removeListener()
|
||||
}
|
||||
|
||||
override fun updateData(tasks: OrderedRealmCollection<Task>?) {
|
||||
data?.takeIf { it.isValid }?.removeListener()
|
||||
tasks?.takeIf { it.isValid }?.addListener()
|
||||
super.updateData(tasks)
|
||||
}
|
||||
|
||||
private fun OrderedRealmCollection<Task>.addListener() {
|
||||
when (this) {
|
||||
is RealmResults<Task> -> addChangeListener(resultsListener)
|
||||
is RealmList<Task> -> addChangeListener(listListener)
|
||||
else -> throw IllegalArgumentException("RealmCollection not supported: $javaClass")
|
||||
}
|
||||
}
|
||||
|
||||
private fun OrderedRealmCollection<Task>.removeListener() {
|
||||
when (this) {
|
||||
is RealmResults<Task> -> removeChangeListener(resultsListener)
|
||||
is RealmList<Task> -> removeChangeListener(listListener)
|
||||
else -> throw IllegalArgumentException("RealmCollection not supported: $javaClass")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.components.UserComponent
|
||||
import com.habitrpg.android.habitica.data.ApiClient
|
||||
|
|
@ -63,18 +65,18 @@ open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayou
|
|||
@Inject
|
||||
lateinit var configManager: AppConfigManager
|
||||
|
||||
internal var layoutManager: androidx.recyclerview.widget.RecyclerView.LayoutManager? = null
|
||||
internal var layoutManager: RecyclerView.LayoutManager? = null
|
||||
|
||||
internal var classType: String? = null
|
||||
internal var user: User? = null
|
||||
private var mItemTouchCallback: ItemTouchHelper.Callback? = null
|
||||
private var itemTouchCallback: ItemTouchHelper.Callback? = null
|
||||
|
||||
internal val className: String
|
||||
get() = this.classType ?: ""
|
||||
|
||||
// TODO needs a bit of cleanup
|
||||
private fun setInnerAdapter() {
|
||||
val adapter: androidx.recyclerview.widget.RecyclerView.Adapter<*>? = when (this.classType) {
|
||||
val adapter: RecyclerView.Adapter<*>? = when (this.classType) {
|
||||
Task.TYPE_HABIT -> {
|
||||
HabitsRecyclerViewAdapter(null, true, R.layout.habit_item_card, taskFilterHelper)
|
||||
}
|
||||
|
|
@ -127,7 +129,7 @@ open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayou
|
|||
}
|
||||
|
||||
private fun allowReordering() {
|
||||
val itemTouchHelper = mItemTouchCallback?.let { ItemTouchHelper(it) }
|
||||
val itemTouchHelper = itemTouchCallback?.let { ItemTouchHelper(it) }
|
||||
itemTouchHelper?.attachToRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
|
|
@ -142,33 +144,27 @@ open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayou
|
|||
taskFilterHelper.setActiveFilter(Task.TYPE_TODO, Task.FILTER_ACTIVE)
|
||||
}
|
||||
|
||||
mItemTouchCallback = object : ItemTouchHelper.Callback() {
|
||||
private var fromPosition: Int? = null
|
||||
private var movingTaskID: String? = null
|
||||
itemTouchCallback = object : ItemTouchHelper.Callback() {
|
||||
|
||||
override fun onSelectedChanged(viewHolder: androidx.recyclerview.widget.RecyclerView.ViewHolder?, actionState: Int) {
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
if (viewHolder != null) {
|
||||
if (fromPosition == null) {
|
||||
fromPosition = viewHolder.adapterPosition
|
||||
}
|
||||
if (movingTaskID == null && (viewHolder as? BaseTaskViewHolder)?.task?.isValid == true) {
|
||||
movingTaskID = (viewHolder as? BaseTaskViewHolder)?.task?.id
|
||||
}
|
||||
if (viewHolder == null || viewHolder.adapterPosition == NO_POSITION) return
|
||||
val taskViewHolder = viewHolder as? BaseTaskViewHolder
|
||||
if (taskViewHolder != null) {
|
||||
taskViewHolder.movingFromPosition = viewHolder.adapterPosition
|
||||
}
|
||||
refreshLayout.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onMove(recyclerView: androidx.recyclerview.widget.RecyclerView, viewHolder: androidx.recyclerview.widget.RecyclerView.ViewHolder, target: androidx.recyclerview.widget.RecyclerView.ViewHolder): Boolean {
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
recyclerAdapter?.notifyItemMoved(viewHolder.adapterPosition, target.adapterPosition)
|
||||
//taskRepository.swapTaskPosition(viewHolder.getAdapterPosition(), target.getAdapterPosition());
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: androidx.recyclerview.widget.RecyclerView.ViewHolder, direction: Int) { /* no-on */ }
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { /* no-on */ }
|
||||
|
||||
//defines the enabled move directions in each state (idle, swiping, dragging).
|
||||
override fun getMovementFlags(recyclerView: androidx.recyclerview.widget.RecyclerView, viewHolder: androidx.recyclerview.widget.RecyclerView.ViewHolder): Int {
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
return makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
|
||||
ItemTouchHelper.DOWN or ItemTouchHelper.UP)
|
||||
}
|
||||
|
|
@ -177,26 +173,31 @@ open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayou
|
|||
|
||||
override fun isLongPressDragEnabled(): Boolean = true
|
||||
|
||||
override fun clearView(recyclerView: androidx.recyclerview.widget.RecyclerView, viewHolder: androidx.recyclerview.widget.RecyclerView.ViewHolder) {
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
refreshLayout?.isEnabled = true
|
||||
|
||||
val fromPosition = fromPosition
|
||||
val movingTaskID = movingTaskID
|
||||
if (fromPosition != null && movingTaskID != null) {
|
||||
if (viewHolder.adapterPosition == NO_POSITION) return
|
||||
val taskViewHolder = viewHolder as? BaseTaskViewHolder
|
||||
val validTaskId = taskViewHolder?.task?.takeIf { it.isValid }?.id
|
||||
if (viewHolder.adapterPosition != taskViewHolder?.movingFromPosition) {
|
||||
taskViewHolder?.movingFromPosition = null
|
||||
updateTaskInRepository(validTaskId, viewHolder)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTaskInRepository(validTaskId: String?, viewHolder: RecyclerView.ViewHolder) {
|
||||
if (validTaskId != null) {
|
||||
recyclerAdapter?.ignoreUpdates = true
|
||||
itemAnimator.skipAnimations = true
|
||||
compositeSubscription.add(taskRepository.updateTaskPosition(classType ?: "", movingTaskID, viewHolder.adapterPosition)
|
||||
compositeSubscription.add(taskRepository.updateTaskPosition(
|
||||
classType ?: "", validTaskId, viewHolder.adapterPosition
|
||||
)
|
||||
.delay(1, TimeUnit.SECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(Consumer {
|
||||
recyclerAdapter?.ignoreUpdates = false
|
||||
recyclerAdapter?.notifyDataSetChanged()
|
||||
itemAnimator.skipAnimations = false
|
||||
}, RxErrorHandler.handleEmptyError()))
|
||||
}
|
||||
this.fromPosition = null
|
||||
this.movingTaskID = null
|
||||
}
|
||||
}
|
||||
if (savedInstanceState != null) {
|
||||
|
|
@ -212,7 +213,7 @@ open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayou
|
|||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
mItemTouchCallback = null
|
||||
itemTouchCallback = null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
@ -228,7 +229,7 @@ open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayou
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
recyclerView.setScaledPadding(context, 0, 0, 0, 48)
|
||||
recyclerView.adapter = recyclerAdapter as? androidx.recyclerview.widget.RecyclerView.Adapter<*>
|
||||
recyclerView.adapter = recyclerAdapter as? RecyclerView.Adapter<*>
|
||||
recyclerAdapter?.filter()
|
||||
|
||||
layoutManager = getLayoutManager(context)
|
||||
|
|
@ -264,10 +265,10 @@ open class TaskRecyclerViewFragment : BaseFragment(), androidx.swiperefreshlayou
|
|||
|
||||
refreshLayout.setOnRefreshListener(this)
|
||||
|
||||
recyclerView.addOnScrollListener(object : androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: androidx.recyclerview.widget.RecyclerView, newState: Int) {
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
super.onScrollStateChanged(recyclerView, newState)
|
||||
if (newState == androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE) {
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
refreshLayout?.isEnabled = (activity as? MainActivity)?.isAppBarExpanded ?: false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ abstract class BaseTaskViewHolder constructor(itemView: View, var scoreTaskFunc:
|
|||
|
||||
|
||||
var task: Task? = null
|
||||
var movingFromPosition: Int? = null
|
||||
var errorButtonClicked: Action? = null
|
||||
protected var context: Context
|
||||
private val titleTextView: EllipsisTextView by bindView(itemView, R.id.checkedTextView)
|
||||
|
|
|
|||
Loading…
Reference in a new issue