Skip to content

Instantly share code, notes, and snippets.

@JakeSteam
Last active April 22, 2024 09:56
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JakeSteam/b5739b3fbdd367a9fb624b85196d8fcc to your computer and use it in GitHub Desktop.
Save JakeSteam/b5739b3fbdd367a9fb624b85196d8fcc to your computer and use it in GitHub Desktop.
Creating a grid RecyclerView with quick drag and drop item swapping, Room / LiveData support, and more!
class ItemAdapter(
private val itemClickListener: (OwnedItem) -> Unit,
private val itemSaver: (List<OwnedItem>) -> Unit
) : RecyclerView.Adapter<ItemViewHolder>() {
val items = ArrayList<OwnedItem>()
fun setItems(newItems: List<OwnedItem>) {
val result = calculateDiff(newItems)
items.clear()
items.addAll(newItems)
result.dispatchUpdatesTo(this)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val binding: BoardItemBinding =
BoardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ItemViewHolder(binding, itemClickListener, itemTouchHelper::startDrag)
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) =
holder.bind(items[position])
private fun calculateDiff(newItems: List<OwnedItem>) = DiffUtil.calculateDiff(object :
DiffUtil.Callback() {
override fun getOldListSize() = items.size
override fun getNewListSize() = newItems.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return items[oldItemPosition] == newItems[newItemPosition]
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val newProduct = newItems[newItemPosition]
val oldProduct = items[oldItemPosition]
return newProduct.id == oldProduct.id
&& newProduct.item == oldProduct.item
&& newProduct.board == oldProduct.board
&& newProduct.position == oldProduct.position
}
})
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN or
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0
) {
var oldPosition = -1
var newPosition = -1
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
newPosition = target.adapterPosition
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
when (actionState) {
ItemTouchHelper.ACTION_STATE_DRAG -> {
viewHolder?.adapterPosition?.let { oldPosition = it }
}
ItemTouchHelper.ACTION_STATE_IDLE -> {
if (oldPosition != -1 && newPosition != -1 && oldPosition != newPosition) {
val old = items[oldPosition]
val new = items[newPosition]
old.position = newPosition
new.position = oldPosition
items[oldPosition] = new
items[newPosition] = old
itemSaver.invoke(listOf(old, new))
notifyDataSetChanged()
oldPosition = -1
newPosition = -1
}
}
}
}
})
}
class ItemViewHolder(
private val itemBinding: BoardItemBinding,
private val itemClickListener: (OwnedItem) -> Unit,
private val itemTouchListener: (ItemViewHolder) -> Unit
) : RecyclerView.ViewHolder(itemBinding.root) {
private lateinit var ownedItem: OwnedItem
fun bind(ownedItem: OwnedItem) {
this.ownedItem = ownedItem
Glide.with(itemBinding.root)
.load(ownedItem.item.image)
.into(itemBinding.image)
if (ownedItem.item != Item.NONE) {
itemBinding.root.setOnTouchListener { v, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
itemTouchListener.invoke(this)
}
false
}
itemBinding.root.setOnClickListener {
itemClickListener.invoke(ownedItem)
}
itemBinding.tier.text = "Pos: ${ownedItem.position}"
} else {
itemBinding.tier.text = ""
}
}
}
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/itemGrid"
android:layout_width="0dp"
android:layout_height="0dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toTopOf="@id/testButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/reputation"
app:spanCount="5"
tools:listitem="@layout/board_item" />
@AndroidEntryPoint
class TableFragment : Fragment() {
private var binding: TableFragmentBinding by autoCleared()
private val viewModel: TableViewModel by viewModels()
private lateinit var adapter: ItemAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = TableFragmentBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreafted(view, savedInstanceState)
setupRecyclerView()
viewModel.items.observe(viewLifecycleOwner) {
adapter.setItems(it)
}
}
private fun setupRecyclerView() {
adapter = ItemAdapter(
viewModel::handleItemClick,
viewModel::saveItems
)
adapter.itemTouchHelper.attachToRecyclerView(binding.itemGrid)
binding.itemGrid.setHasFixedSize(true)
binding.itemGrid.adapter = adapter
}
}
fun handleItemClick(ownedItem: OwnedItem) {
_textToShow.postValue(String.format("That's a %s at position %d!", ownedItem.item.name, ownedItem.position))
}
fun saveItems(items: List<OwnedItem>) {
viewModelScope.launch(Dispatchers.IO) {
itemRepository.insertItems(items)
}
}
@Hassenforder
Copy link

Great gist. I am also looking on the merge algorithm shown in the video but not here in the gist. Can you release it, I try to figure out how to play with ItemTouchHelper tp implement it but I do not find a simple way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment