diff --git a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadDragHelper.kt b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadDragHelper.kt index 283620eb..a5d0b2fd 100644 --- a/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadDragHelper.kt +++ b/dpadrecyclerview/src/main/java/com/rubensousa/dpadrecyclerview/DpadDragHelper.kt @@ -19,8 +19,16 @@ package com.rubensousa.dpadrecyclerview import android.view.KeyEvent import android.view.View import androidx.recyclerview.widget.RecyclerView +import java.util.Collections -class DpadDragHelper( +/** + * A helper class for re-ordering the contents of a [DpadRecyclerView]. + * + * To use this, your adapter needs to implement [DpadDragHelper.DragAdapter] + * and expose the mutable collection via [DpadDragHelper.DragAdapter.getMutableItems]. + */ +class DpadDragHelper( + private val adapter: DragAdapter, private val callback: DragCallback, private val cancelKeyCodes: Set = setOf( KeyEvent.KEYCODE_DPAD_CENTER, @@ -48,6 +56,10 @@ class DpadDragHelper( } } + /** + * Attaches the [DpadRecyclerView] that will be dragged. + * This is required before calling [startDrag] + */ fun attachRecyclerView(recyclerView: DpadRecyclerView) { if (currentRecyclerView === recyclerView) { return @@ -56,11 +68,15 @@ class DpadDragHelper( currentRecyclerView = recyclerView } + /** + * Detaches the previously attached [DpadRecyclerView] and stops dragging + */ fun detachFromRecyclerView() { stopDrag() currentRecyclerView = null } + /** * Starts the dragging action for the ViewHolder at [position]. * @@ -125,24 +141,39 @@ class DpadDragHelper( } isDragging = false callback.onDragStopped() - currentRecyclerView?.requestLayout() } private fun onKeyEvent(event: KeyEvent): Boolean { val recyclerView = currentRecyclerView ?: return false val direction = getFocusDirection(event) ?: return false val view = recyclerView.focusSearch(direction) ?: return false - val viewHolder = recyclerView.findContainingViewHolder(view) ?: return false + val targetViewHolder = recyclerView.findContainingViewHolder(view) ?: return false val selectedViewHolder = recyclerView.findViewHolderForAdapterPosition( recyclerView.getSelectedPosition() ) ?: return false if (selectedViewHolder.absoluteAdapterPosition == RecyclerView.NO_POSITION - || viewHolder.absoluteAdapterPosition == RecyclerView.NO_POSITION + || targetViewHolder.absoluteAdapterPosition == RecyclerView.NO_POSITION ) { return false } - return callback.move(src = selectedViewHolder, target = viewHolder) + val currentAdapter = recyclerView.adapter ?: return false + if (recyclerView.getSpanCount() == 1) { + moveLinear( + adapter = currentAdapter, + items = adapter.getMutableItems(), + srcIndex = selectedViewHolder.bindingAdapterPosition, + targetIndex = targetViewHolder.bindingAdapterPosition + ) + } else { + moveGrid( + adapter = currentAdapter, + items = adapter.getMutableItems(), + srcIndex = selectedViewHolder.bindingAdapterPosition, + targetIndex = targetViewHolder.bindingAdapterPosition + ) + } + return true } private fun getFocusDirection(event: KeyEvent): Int? { @@ -155,28 +186,64 @@ class DpadDragHelper( } } - interface DragCallback { + /** + * A linear move just swaps positions + */ + private fun moveLinear( + adapter: RecyclerView.Adapter<*>, + items: MutableList, + srcIndex: Int, + targetIndex: Int + ) { + Collections.swap(items, srcIndex, targetIndex) + adapter.notifyItemMoved(srcIndex, targetIndex) + } + + /** + * A grid move needs to remove the element at the given position + * and insert it in the new position, otherwise order is not kept + */ + private fun moveGrid( + adapter: RecyclerView.Adapter<*>, + items: MutableList, + srcIndex: Int, + targetIndex: Int + ) { + val item = items.removeAt(srcIndex) + items.add(targetIndex, item) + adapter.notifyItemMoved(srcIndex, targetIndex) + /** + * Now notify the range that was affected + * If src < target -> notify all indexes from src until target + * If src > target -> notify all indexes from target until src + */ + if (srcIndex < targetIndex) { + adapter.notifyItemRangeChanged(srcIndex, targetIndex - srcIndex) + } else { + adapter.notifyItemRangeChanged(targetIndex, srcIndex - targetIndex) + } + } + interface DragAdapter { /** - * @return true if [src] should be moved to [target]'s position, - * false to keep the original positions + * @return the mutable collection of items backing the adapter */ - fun move( - src: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean + fun getMutableItems(): MutableList + } + + interface DragCallback { /** * Indicates that the dragging action has started. [DpadRecyclerView] will receive focus * * @param viewHolder the view holder that is being dragged */ - fun onDragStarted(viewHolder: RecyclerView.ViewHolder) {} + fun onDragStarted(viewHolder: RecyclerView.ViewHolder) /** * Indicates that the dragging action has stopped */ - fun onDragStopped() {} + fun onDragStopped() } } diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAdapter.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAdapter.kt index f6dd45ad..a58f4023 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAdapter.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAdapter.kt @@ -19,6 +19,7 @@ package com.rubensousa.dpadrecyclerview.sample.ui.screen.drag import android.view.ViewGroup import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.recyclerview.widget.RecyclerView +import com.rubensousa.dpadrecyclerview.DpadDragHelper import com.rubensousa.dpadrecyclerview.compose.DpadComposeFocusViewHolder import com.rubensousa.dpadrecyclerview.sample.ui.model.ListTypes import com.rubensousa.dpadrecyclerview.sample.ui.widgets.common.MutableListAdapter @@ -29,7 +30,10 @@ class DragAdapter( private val dragState: StateFlow, private val onDragStart: (viewHolder: RecyclerView.ViewHolder) -> Unit, private val gridLayout: Boolean = false -) : MutableListAdapter>(MutableGridAdapter.DIFF_CALLBACK) { +) : MutableListAdapter>(MutableGridAdapter.DIFF_CALLBACK), + DpadDragHelper.DragAdapter { + + override fun getMutableItems(): MutableList = items override fun onCreateViewHolder( parent: ViewGroup, @@ -64,8 +68,22 @@ class DragAdapter( holder.itemView.contentDescription = item.toString() } + override fun getItemViewType(position: Int): Int { return ListTypes.ITEM } + override fun toString(): String { + val builder = StringBuilder() + builder.append("[") + for (i in 0 until itemCount) { + builder.append(getItem(i)) + if (i < itemCount - 1) { + builder.append(", ") + } + } + builder.append("]") + return builder.toString() + } + } \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAndDropFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAndDropFragment.kt index 76ddae76..84e881d5 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAndDropFragment.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAndDropFragment.kt @@ -44,28 +44,17 @@ class DragAndDropFragment : Fragment(R.layout.screen_drag_drop) { startDrag(viewHolder) } ) - private val dragHelper = DpadDragHelper(object : DpadDragHelper.DragCallback { - - override fun move( - src: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - dragAdapter.move( - from = src.bindingAdapterPosition, - to = target.bindingAdapterPosition - ) - return true - } - - override fun onDragStarted(viewHolder: RecyclerView.ViewHolder) { - dragState.value = dragAdapter.getItem(viewHolder.bindingAdapterPosition) - } - - override fun onDragStopped() { - dragState.value = null + private val dragHelper = DpadDragHelper( + adapter = dragAdapter, + callback = object : DpadDragHelper.DragCallback { + override fun onDragStarted(viewHolder: RecyclerView.ViewHolder) { + dragState.value = dragAdapter.getItem(viewHolder.bindingAdapterPosition) + } + override fun onDragStopped() { + dragState.value = null + } } - - }) + ) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAndDropGridFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAndDropGridFragment.kt index 4f409bd1..ab834b76 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAndDropGridFragment.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/drag/DragAndDropGridFragment.kt @@ -45,34 +45,23 @@ class DragAndDropGridFragment : Fragment(R.layout.screen_drag_drop_grid) { }, gridLayout = true ) - private val dragHelper = DpadDragHelper(object : DpadDragHelper.DragCallback { - - override fun move( - src: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - dragAdapter.move( - from = src.absoluteAdapterPosition, - to = target.absoluteAdapterPosition - ) - return true - } - - override fun onDragStarted(viewHolder: RecyclerView.ViewHolder) { - dragState.value = dragAdapter.getItem(viewHolder.absoluteAdapterPosition) - } - - override fun onDragStopped() { - dragState.value = null + private val dragHelper = DpadDragHelper( + adapter = dragAdapter, + callback = object : DpadDragHelper.DragCallback { + override fun onDragStarted(viewHolder: RecyclerView.ViewHolder) { + dragState.value = dragAdapter.getItem(viewHolder.bindingAdapterPosition) + } + override fun onDragStopped() { + dragState.value = null + } } - - }) + ) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupRecyclerView() setDragButtonContent() - dragAdapter.submitList(List(20) { it }.toMutableList()) + dragAdapter.submitList(List(15) { it }.toMutableList()) } private fun setupRecyclerView() { diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/common/MutableListAdapter.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/common/MutableListAdapter.kt index 7ffa8f49..19b2639f 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/common/MutableListAdapter.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/common/MutableListAdapter.kt @@ -34,7 +34,7 @@ abstract class MutableListAdapter( private val MAIN_THREAD_HANDLER = Handler(Looper.getMainLooper()) } - private var items: MutableList = Collections.emptyList() + protected var items: MutableList = Collections.emptyList() private var currentVersion = 0 @SuppressLint("NotifyDataSetChanged") @@ -128,12 +128,6 @@ abstract class MutableListAdapter( } - fun move(from: Int, to: Int) { - currentVersion++ - Collections.swap(items, from, to) - notifyItemMoved(from, to) - } - fun addAt(index: Int, item: T) { currentVersion++ items.add(index, item)