Search code examples
androidandroid-jetpackandroid-viewpager2

How to reset the view state of a ViewHolder in ViewPager2, once it is not visible anymore?


I'm using a ViewPager2 and a ListAdapter to scroll horizontally through Recycler.ViewHolder items, showing one item at a time. Each can be scrolled vertically when the content doesn't fit on the screen.

I would like to reset the scroll state of the items once they are not visible anymore, because the view holder is likely to be reused for another item, and because it's tidier.

I know how to reset the scroll state in the ViewHolder, but where can I put this code?

I first thought it could be in the fragment hosting the ViewPager2, where I could register a callback ViewPager2.OnPageChangeCallback with the viewpager:

private var lastPosition = -1
private var callback = object: ViewPager2.OnPageChangeCallback() {
    override fun onPageSelected(position: Int) {
        if (lastPosition >= 0) {
            // reset scroll here
            val v: MyViewHolder = ???(position)
            v.resetScrollState()
        }
        lastPosition = position
    }
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    viewpager = binding.itemPager
    viewpager.adapter = [...]
    viewpager.registerOnPageChangeCallback(callback)
}

(where binding.itemPager points to the ViewPager2)

But I don't know how to easily get to the view holder, except by storing it (in an collection for ex.) so I can retrieve it from position. Then I would still have to correctly set lastPosition for the first time but it's secondary.

My question: Is there any way to

  • set a listener to a change of page, with access to the view holder?
  • or alternatively, retrieve the view holder in function of position?

Or is there a better way to handle this?


Solution

  • I found it eventually.

    For a given lastPosition, the view holder can be obtained with:

    (viewpager.get(0) as RecyclerView).findViewHolderForAdapterPosition(lastPosition)
        as? MyViewHolder
    

    So the callback becomes:

    private var callback = object: ViewPager2.OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            if (lastPosition >= 0) {
                // reset scroll here
                val holder = (viewpager.get(0) as RecyclerView).findViewHolderForAdapterPosition(lastPosition) as? TodoEventAdapter.MyViewHolder
                if (holder != null)
                    holder.resetScroll()
            }
            lastPosition = position
        }
    }
    

    I can set the initial value of lastPosition and register the callback, for example in the fragment code with ViewPager2.currentItem:

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewpager = binding.itemPager
        [...]
        lastPosition = viewpager.currentItem
        viewpager.registerOnPageChangeCallback(callback)
    }
    
    override fun onDestroyView() {
        [...]
        viewpager.unregisterOnPageChangeCallback(callback)
        super.onDestroyView()
    }
    

    Where binding.itemPager uses viewBinding, and corresponds to the ViewPager2 instance of the layout.

    Note that the first position to show is likely to come from a parameter, for example an activity that needs to display a specific item at a starting position. The initial value of lastPosition may be obtained from this parameter, or from the ViewPager2.currentItem value once/if it has been set, as shown above.