From 0f87874ede997e91f6b214d40d79f7ee769cdc39 Mon Sep 17 00:00:00 2001 From: aleien Date: Sat, 12 Oct 2019 18:36:58 +0300 Subject: [PATCH] Fixed problem with RealmRecyclerViewAdapter wanting to autoupdate data and clashing with already applied view positioning from drag'n'drop --- .../RealmBaseTasksRecyclerViewAdapter.kt | 101 +++++++++++++----- .../tasks/TaskRecyclerViewFragment.kt | 69 ++++++------ .../viewHolders/tasks/BaseTaskViewHolder.kt | 1 + 3 files changed, 111 insertions(+), 60 deletions(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.kt index f91fe1375..c594461c1 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/tasks/RealmBaseTasksRecyclerViewAdapter.kt @@ -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(private var unfilteredData: OrderedRealmCollection?, private val hasAutoUpdates: Boolean, private val layoutResource: Int, private val taskFilterHelper: TaskFilterHelper?) : RealmRecyclerViewAdapter(null, true), TaskRecyclerViewAdapter { +abstract class RealmBaseTasksRecyclerViewAdapter( + private var unfilteredData: OrderedRealmCollection?, + private val hasAutoUpdates: Boolean, + private val layoutResource: Int, + private val taskFilterHelper: TaskFilterHelper? +) : RealmRecyclerViewAdapter(null, false), TaskRecyclerViewAdapter { private var updateOnModification: Boolean = false override var ignoreUpdates: Boolean = false - private val listener: OrderedRealmCollectionChangeListener> by lazy { - OrderedRealmCollectionChangeListener> { _, 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> by lazy { + OrderedRealmCollectionChangeListener> { _, changeSet -> + buildChangeSet(changeSet) + } + } - if (!updateOnModification) { - return@OrderedRealmCollectionChangeListener - } + private val listListener: OrderedRealmCollectionChangeListener> by lazy { + OrderedRealmCollectionChangeListener> { _, 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?) { + data?.takeIf { it.isValid }?.removeListener() + tasks?.takeIf { it.isValid }?.addListener() + super.updateData(tasks) + } + + private fun OrderedRealmCollection.addListener() { + when (this) { + is RealmResults -> addChangeListener(resultsListener) + is RealmList -> addChangeListener(listListener) + else -> throw IllegalArgumentException("RealmCollection not supported: $javaClass") + } + } + + private fun OrderedRealmCollection.removeListener() { + when (this) { + is RealmResults -> removeChangeListener(resultsListener) + is RealmList -> removeChangeListener(listListener) + else -> throw IllegalArgumentException("RealmCollection not supported: $javaClass") } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TaskRecyclerViewFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TaskRecyclerViewFragment.kt index 893e4a1c9..a935a46e0 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TaskRecyclerViewFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/tasks/TaskRecyclerViewFragment.kt @@ -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 } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt index ff9df4a15..6d0bd333e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewHolders/tasks/BaseTaskViewHolder.kt @@ -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)