RecyclerView scrolls to top when dragging item with ItemTouchHelper

786 views Asked by At

I have this weird bug where my RecyclerView scrolls back to top position whenever I start dragging an item in it. It's inside ViewPager if that has any difference. You can see the behavior in .gif attached.

EDIT:

It seems that RecyclerView view scrolls to top when notifyItemMoved is called and it scrolls just as much for the first view to be at least partially displayed on screen.

enter image description here

View

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/accounts_recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scrollbarStyle="outsideOverlay"
    android:scrollbars="none"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    tools:listitem="@layout/view_account_list_item" />

Adapter

class AccountListAdapter(
private val onAccountClickListener: OnAccountClickListener) :
ListAdapter<Account, AccountListAdapter.ViewHolder>(
    AccountDiffCallback()
) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val inflater = LayoutInflater.from(parent.context)
    return ViewHolder(
        inflater.inflate(
            R.layout.view_account_list_item,
            parent,
            false
        )
    )
}

override fun getItemId(position: Int): Long {
    return getItem(position).accountId.toLong()
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(getItem(position), onAccountClickListener)
}

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), OnItemDragged {

    fun bind(account: Account, onAccountClickListener: OnAccountClickListener) {
        itemView.account_name.text = account.name
       
        itemView.setOnClickListener {
            onAccountClickListener.onAccountClick(account)
        }

    }

    override fun onItemSelected() {
        itemView.setBackgroundColor(
            ContextCompat.getColor(
                itemView.context,
                R.color.background_contrast
            )
        )
    }

    override fun onItemClear() {
        itemView.setBackgroundColor(
            ContextCompat.getColor(
                itemView.context,
                R.color.background
            )
        )
    }

}

class AccountDiffCallback : DiffUtil.ItemCallback<Account>() {
    override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean {
        return oldItem.accountId == newItem.accountId
    }

    override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean {
        return (oldItem.balance == newItem.balance
                && oldItem.annualReturn == newItem.annualReturn
                && oldItem.name == newItem.name)
    }
}

interface OnAccountClickListener {
    fun onAccountClick(account: Account)
}

interface OnItemDragged {
    fun onItemSelected()
    fun onItemClear()
}}

ItemTouchHelper

 private fun setupListAdapter() {

    accountListAdapter = AccountListAdapter(this)
    accountListAdapter.setHasStableIds(true)

    accounts_recycler_view.adapter = accountListAdapter
    accounts_recycler_view.addItemDecoration(
        DividerItemDecoration(
            requireContext(),
            DividerItemDecoration.VERTICAL
        )
    )


    val accountTouchHelper = ItemTouchHelper(
        object : ItemTouchHelper.SimpleCallback(
            ItemTouchHelper.UP or ItemTouchHelper.DOWN,
            0
        ) {

            override fun onSelectedChanged(
                viewHolder: RecyclerView.ViewHolder?,
                actionState: Int
            ) {
                if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
                    val accountViewHolder = viewHolder as AccountListAdapter.ViewHolder
                    accountViewHolder.onItemSelected()
                }
                super.onSelectedChanged(viewHolder, actionState)
            }

            override fun clearView(
                recyclerView: RecyclerView,
                viewHolder: RecyclerView.ViewHolder
            ) {
                val accountViewHolder = viewHolder as AccountListAdapter.ViewHolder
                accountViewHolder.onItemClear()
                super.clearView(recyclerView, viewHolder)
            }


            override fun onMove(
                recyclerView: RecyclerView,
                viewHolder: RecyclerView.ViewHolder,
                target: RecyclerView.ViewHolder
            ): Boolean {
                val fromPos: Int = viewHolder.adapterPosition
                val toPos: Int = target.adapterPosition
                Collections.swap(_accountList, fromPos, toPos)
                accountListAdapter.notifyItemMoved(fromPos, toPos)
                return true
            }

            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {}

        })

    accountTouchHelper.attachToRecyclerView(accounts_recycler_view)

}
2

There are 2 answers

0
MakinTosH On BEST ANSWER

So the issues was that my Recyclerview was inside ViewPager which was inside a ConstraintLayout . View pager was constrained vertically with height set to 0dp but width was set to match_parent. All I needed was to constrain it horizontally with width set to 0dp and setHasFixedSize = true to RecyclerView

When calling notifyItemMoved for adapter, if RecyclerView is flexible, all items are redrawn and by default it focuses on the first item.

0
protaiyabali On

If you are using NestedScrollView just remove and instead of using other layouts use ConstraintLayout

Steps:

Use ConstrainLayout and RecylerView like this:

<androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rvDraggable"
            android:layout_width="match_parent"
            android:layout_height="@dimen/dp_0"
            android:layout_marginStart="@dimen/dp_16"
            android:layout_marginEnd="@dimen/dp_16"
            android:scrollbarStyle="outsideOverlay"
            android:scrollbars="none"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>

Note- you can also try to use:

  • viewBinding.recycler.setHasFixedSize(true)

  • If you are using notifyDataSetChanged() so just replace it by notifyItemMoved() in onMove() method