Drag and drop issue with RecyclerView, ItemTouchHelper and GridLayoutManager - android

I have found an issue when dragging an item that doesn't trigger a move on a GridLayoutManager with 2 columns.
When dragging one item from the first column to the top of the list (which scrolls the list upwards) and then back to its original position, the recyclerView creates an empty space next to the original position, pushing the item that was next to the dragged item to the next row.
I have coded the simplest app to show you what I mean. On the first screenshot I start dragging one item towards the top of the list. On the second screenshot I reach the top of the screen. Then I move it back to its position and you can already see how the items have not been laid down properly.
Has anyone encountered this as well? I have not found any solution to this issue.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myAdapter = MyAdapter()
with(findViewById<RecyclerView>(R.id.recyclerView)) {
adapter = myAdapter
layoutManager = GridLayoutManager(this#MainActivity, 2)
ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
return makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, DOWN or UP or START or END)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun canDropOver(recyclerView: RecyclerView, current: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
TODO("Not yet implemented")
}
}).attachToRecyclerView(this)
}
myAdapter.submitList(data)
}
data class ListItem(val id: String, val name: String)
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
class MyAdapter : ListAdapter<ListItem, MyViewHolder>(MyAdapterDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false)
return MyViewHolder(view)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
}
class MyAdapterDiffCallback : DiffUtil.ItemCallback<ListItem>() {
override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: ListItem, newItem: ListItem) = oldItem == newItem
}
}

Related

Drag and Drop to reorder and move from one to another recycling view

I am building an app to be able to drag an item from one recycle view to another and I still have to keep the option to re-order inside a single recycler view.
So I have defined a Reorder Callback already
class ReorderHelperCallback(val adapter : ItemTouchHelperAdapter): ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
return makeMovementFlags( dragFlags, 0)//swipeFlags )
}
override fun onMove(
recyclerView: RecyclerView,
source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
adapter.onItemMove(source.getAdapterPosition(),
target.adapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
//Not use for Drag N Drop
}
}
also an interface:
interface OnStartDragListener {
fun onStartDrag(viewHolder: RecyclerView.ViewHolder?)
}
and a touchhelper:
interface ItemTouchHelperAdapter {
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
fun onItemDismiss(position: Int)
}
to allow the reorder to work, I had to update the Recycler view adapter as below:
class Adapter(
private var context: Context?,
val dragStartListener : OnStartDragListener
): RecyclerView.Adapter<Adapter.ViewHolder>(), ItemTouchHelperAdapter {
var arrayItems : ArrayList<Data?> = ArrayList()
fun setData(array : MutableList<Data?>){
array.toCollection(arrayItems)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return arrayItems.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Adapter.ViewHolder {
val binding = DashboardTileLayoutBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding, dragStartListener)
}
override fun onBindViewHolder(holder: Adapter.ViewHolder, position: Int) {
holder.setData(arrayItems[position])
}
inner class ViewHolder(val binding: LayoutBinding,
val dragStartListener : OnStartDragListener? = null)
: RecyclerView.ViewHolder(binding.root) {
val tileLayout = binding.tileLayout
fun setData(data: Data?) {
....
tileLayout.setOnDragListener { view, dragEvent ->
when(dragEvent.action) {
ACTION_DRAG_STARTED -> {
dragStartListener?.onStartDrag(this)
true
}
else -> false
}
}
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
Collections.swap(arrayItems, fromPosition, toPosition)
notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onItemDismiss(position: Int) {
TODO("Not yet implemented")
}
}
and the fragment which contain the rv, I have updated the adapter init:
list1adapter?.let { adapter ->
adapter.setData(list)
val callback: ItemTouchHelper.Callback = ReorderHelperCallback(adapter)
mItemTouchHelperSelected = ItemTouchHelper(callback)
mItemTouchHelperSelected?.attachToRecyclerView(selectedLayout)
}
---
override fun onStartDrag(viewHolder: RecyclerView.ViewHolder?) {
viewHolder?.let {
mItemTouchHelperSelected?.startDrag(it)
}
}
But my fragment contain 2 recycler views. list1 is working fine to user drag and drop to re-order the item but now, I would like to also be able to move an item from my rv list1 to the list2 and vice versa
Any idea, how to make it Kotlin ? I tried an sample code, byt I am losing the re-ordering.
Thanks

RecyclerView only displaying data after restarting Activity

So i have this strange behaviour where my RecyclerView only Displays data when i start the App and then restart the activity via Android Studio. Or make a change in XML, then undo it and restart.
The then displayed data is correct and also updates when i add things, but it is not visible after starting the app.
Can anyone help me out? I am really clueless why that would be.
Fragment:
#AndroidEntryPoint
class HistoryFragment : Fragment(R.layout.fragment_history) {
private val viewModel: PurchaseViewmodel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentHistoryBinding.bind(view)
val exampleAdapter = ExampleAdapter()
binding.apply{
recyclerView.apply{
layoutManager = LinearLayoutManager(requireContext())
adapter = exampleAdapter
setHasFixedSize(true)
}
ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false /// We dont need any interaction with Drag&Drop we only want swipe left&right
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val receipt = exampleAdapter.currentList[viewHolder.adapterPosition]
viewModel.onSwipe(receipt)
}
}).attachToRecyclerView(recyclerView)
}
setFragmentResultListener("add_receipt_request"){_,bundle ->
val result = bundle.getInt("add_receipt_request")
viewModel.onAddResult(result)
}
viewModel.receipts.observe(viewLifecycleOwner){ /// New Items get passed to the List
exampleAdapter.submitList(it)
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted { //as soon as we close our app the events will be suspended, but not deleted and will remain after restart
viewModel.addTaskEvent.collect { event->
when(event){
is PurchaseViewmodel.TasksEvent.ShowUndoDelete -> {
Snackbar.make(requireView(),"Tasks deleted", Snackbar.LENGTH_LONG)
.setAction("UNDO"){
viewModel.unDoDeleteClick(event.receipts)
}.show()
}
}
}
}
}
}
Adapter:
class ExampleAdapter : ListAdapter<Receipts,ExampleAdapter.ExampleViewHolder>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleViewHolder {
val binding = ReceiptsBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return ExampleViewHolder(binding)
}
override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {
val currentItem = getItem(position)
holder.bind(currentItem)
}
override fun getItemCount(): Int {
return super.getItemCount()
}
class ExampleViewHolder(private val binding: ReceiptsBinding) : RecyclerView.ViewHolder(binding.root){ //Examples One Row in our list
fun bind (receipts: Receipts) {
binding.apply {
storeHistory.text = receipts.store
amountHistory.text = receipts.total
dateHistory.text = receipts.date
}
}
}
class DiffCallback : DiffUtil.ItemCallback<Receipts>() {
override fun areItemsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem == newItem
}
}
You should compare corresponding fields of old/new Receipts in areContentsTheSame() instead of comparing the entire old/new objects
override fun areContentsTheSame(oldItem: Receipts, newItem: Receipts) =
oldItem.store == newItem.store
&& oldItem.total == newItem.total
&& oldItem.date == newItem.date

Recycler view items disappear when I scroll my recycler view to top

The problem is that when I scroll up my recycler view some items gets disappear
Here is MyAdapter Code of RecyclerView
class MessageAdapter:androidx.recyclerview.widget.ListAdapter<MessageEntity,MessageAdapter.MessageViewHolder>(MessageDiffCallBack()){
inner class MessageViewHolder(val view: View):RecyclerView.ViewHolder(view)
class MessageDiffCallBack:DiffUtil.ItemCallback<MessageEntity>(){
override fun areItemsTheSame(oldItem: MessageEntity, newItem: MessageEntity): Boolean {
return oldItem.timeStamp==newItem.timeStamp
}
override fun areContentsTheSame(oldItem: MessageEntity, newItem: MessageEntity): Boolean {
return oldItem==newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
val view=LayoutInflater.from(parent.context).inflate(R.layout.chat_item,parent,false)
return MessageViewHolder(view)
}
override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
val messageEntity=getItem(position)
val ourTextView=holder.view.findViewById<TextView>(R.id.ourUserMessage)
val otherTextView=holder.view.findViewById<TextView>(R.id.otherUserMessage)
val ourCardView=holder.view.findViewById<CardView>(R.id.ourCardView)
val otherCardView=holder.view.findViewById<CardView>(R.id.otherCardView)
if(messageEntity.senderId==FirebaseAuth.getInstance().currentUser?.uid){
otherCardView.visibility=View.GONE
ourTextView.text=messageEntity.message
}
else{
ourCardView.visibility=View.GONE
otherTextView.text=messageEntity.message
}
}
}
Can anyone help me in this problem

DiffUtil.ItemCallback doesn't update item position (after a deleting)

I use a binding for a ListAdapter with the definition of a DiffUtil.ItemCallback. When deleting items (at least 2) I have an IndexOutOfBoundsException.
The update of the list works (the number of elements is indeed N-1 after deletion) but not the position of the item, which is kept is the call. The exception's therefore thrown when calling getItem(position) (in the onBindViewHolder). NB: A log of getItemCount() just before the getItem(position) shows that the list contains N-1 elements.
I created a small repo: https://github.com/jeremy-giles/DiffListAdapterTest (with a same configuration to my project) which reproduces the problem.
ItemAdapter class
class ItemAdapter(
var listener: ListAdapterListener) : DataBindingAdapter<Item>(DiffCallback()) {
class DiffCallback : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
}
override fun getItemViewType(position: Int) = R.layout.recycler_item
override fun onBindViewHolder(holder: DataBindingViewHolder<Item>, position: Int) {
super.onBindViewHolder(holder, position)
holder.itemView.tv_position.text = "Pos: $position"
holder.itemView.setOnLongClickListener {
Timber.d("List item count: ${itemCount}, position: $position")
listener.onLongViewClick(getItem(position), position)
}
}
interface ListAdapterListener {
fun onLongViewClick(item: Item, position: Int) : Boolean
}
}
BindingUtils classes
abstract class DataBindingAdapter<T>(diffCallback: DiffUtil.ItemCallback<T>) :
ListAdapter<T, DataBindingViewHolder<T>>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataBindingViewHolder<T> {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ViewDataBinding>(layoutInflater, viewType, parent, false)
return DataBindingViewHolder(binding)
}
override fun onBindViewHolder(holder: DataBindingViewHolder<T>, position: Int) {
holder.bind(getItem(position))
}
}
class DataBindingViewHolder<T>(private val binding: ViewDataBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: T) {
binding.setVariable(BR.item, item)
binding.executePendingBindings()
}
}
And in my MainActivity class I use a LiveData to update the recyclerView
itemViewModel.getListObserver().observe(this, Observer {
Timber.d("List Observer, items count ${it.size}")
itemAdapter.submitList(it.toList())
})
In your onBindViewHolder update usage of 'position' to 'holder.getAdapterPosition()':
override fun onBindViewHolder(holder: DataBindingViewHolder<Item>, position: Int) {
super.onBindViewHolder(holder, position)
holder.itemView.tv_position.text = "Pos: $position"
holder.itemView.setOnLongClickListener {
Timber.d("List item count: ${itemCount}, position: $position")
listener.onLongViewClick(getItem(holder.getAdapterPosition()), holder.getAdapterPosition())
}
}
Currently holder.getAdapterPosition() is deprecated. As per documentation, you might want to use either one of the following two methods.
If you are calling this in the context of an Adapter, you probably
want to call getBindingAdapterPosition() or if you want the position
as RecyclerView sees it, you should call getAbsoluteAdapterPosition().

Remove drag and drop for selected cells

I've created drag and drop for my recycler view, but I want disable drag and drop option on pull of cells (it's a headers in my view). How to make them not available for drag and drop function?
Drag And drop helper
class SimpleItemTouchHelperCallback(private val mAdapter: ItemTouchHelperAdapter) : ItemTouchHelper.Callback() {
override fun isLongPressDragEnabled() = true
override fun isItemViewSwipeEnabled() = true
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
mAdapter.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
return true
}
}
Adapter for Recyclerview
interface ItemTouchHelperAdapter {
fun onItemMove(fromPosition: Int, toPosition: Int): Boolean
}
Code to pair adapter to rv
val drugAndDropHandler = SimpleItemTouchHelperCallback(adapter)
ItemTouchHelper(drugAndDropHandler).attachToRecyclerView(multiple_stores_list)
Just need to override getMovementFlags
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
if (viewHolder.adapterPosition in 0..NOT_DRAGABLE_ITEMS_MAX_POSITION) {
return 0
}
}

Categories

Resources