Search code examples
androidandroid-recyclerview

How to prevent items in RecyclerView from jumping when swiping and returning an item?


I have a RecyclerView where I want to allow a user to swipe an item to the left or right for an action, but then have the item return. I implemented a solution found here, which works well. Currently, I have implemented just the swiping and returning, but no actions yet.

However, this has resulted in a visual oddity. When I swipe an item either left or right, the item below it will quickly slide down and back up. This only happens to the item below it and not the one above it, and the amount it slides seems random. Moreover, this "jump" from the item below only happens on the first swipe. I can swipe the same item again and again and it will not result in any more jumps from the item below. I can only replicate the jump by scrolling away from the item and returning to it.

Here is an example of this jump. The GIF isn't the best quality, but it was the best I could do... The GIF appears as if the item below is just disappearing & reappearing, but it is actually sliding down and back up really quickly. In this case, the jump is a bit extreme, but some jumps are much more subtle.

enter image description here

I use a MarginDecoration to separate my items. I thought that perhaps it had something to do with it being destroyed, redrawn, something of the sort. However, I removed it and the jump still occurs. Additionally, I suspected the ImageView, but removing it does not solve the issue.

To summarize:

  • Upon swipe, item below slides down a random distance and back up.
  • Only occurs upon first swipe (left or right).
  • Scrolling item off screen and back to it will result in a slide upon first swipe.
  • Removing MarginDecoration or ImageView does not prevent the issue.

Why does this occur and how would I go about fixing it? Alternatively, perhaps the implementation is flawed? If so, how might one go about swiping and returning an item while maintaining the ability to invoke onSwiped() for an action to occur & maintaining the position of the item above and below?

For reference, here is my RecyclerView code:

private fun initRecyclerView() {
    val swipeRefreshLayout: SwipeRefreshLayout = binding.articleSwipeRefreshLayout
    swipeRefreshLayout.setOnRefreshListener {
        viewModel.getNewsHeadlines()
        swipeRefreshLayout.isRefreshing = false
    }
    val recyclerView: RecyclerView = binding.articleRecyclerView
    val divider: Drawable? =
        AppCompatResources.getDrawable(requireContext(), R.drawable.divider_white)
    val decoration: MarginDecoration? = divider?.let { MarginDecoration(it) }

    if (decoration != null) {
        recyclerView.addItemDecoration(decoration)
    }
    recyclerView.layoutManager = LinearLayoutManager(requireContext())
    adapter = ArticleAdapter(requireActivity())
    recyclerView.adapter = adapter
    ItemTouchHelper(
        object : ItemTouchHelper.SimpleCallback(
            0,
            ItemTouchHelper.END or ItemTouchHelper.START
        ) {
            override fun onMove(
                recyclerView: RecyclerView,
                viewHolder: RecyclerView.ViewHolder,
                target: RecyclerView.ViewHolder
            ): Boolean {
                return false
            }

            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                adapter.notifyItemChanged(viewHolder.adapterPosition)
            }
        }
    ).attachToRecyclerView(recyclerView)
}

Solution

  • By default, the RecyclerView has a RecyclerView.ItemAnimator active on it. This will animate the ViewHolders in four circumstances: added, removed, changed, and moved.

    Here, two animations are happening:

    1. The changed animation is being performed on the swiped item when adapter.notifyItemChanged(viewHolder.adapterPosition) is called. This animation will return the item from the left or right of the screen back to its position in the RecyclerView.
    2. The moved animation is being performed on the item below the swiped item.

    I am not sure why the move animation is occurring, as the item is not being moved, but I know it occurs because if I set the moveDuration of the animator to 0, it no longer occurs. This would be the solution to prevent the item below the swiped one to bounce or jitter after calling adapter.notifyItemChanged(viewHolder.adapterPosition):

    recyclerView.itemAnimator?.moveDuration = 0
    

    However, if a move animation is desired in other use cases for this RecyclerView, then you might consider creating a custom implementation of RecyclerView.ItemAnimator that may fit the use case. Or, it may be possible to set moveDuration to 0 right before calling adapter.notifyItemChanged(viewHolder.adapterPosition) and then returning it to its default value, which is 250, so that the move animation is retained for other circumstances.