Last active
April 22, 2024 09:56
-
-
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!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = "" | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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" /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.