Roland Le Franc
Roland Le Franc

Reputation: 356

Scroll an Android RecyclerView and control the exact location on screen

I am looking for a way to programmatically slowly scroll a RecyclerView so as to bring a certain element targetPosition exactly in the middle of the screen. Note that all my items in the RecylerView have the same height by design. The RecyclerView is vertical. I am programming in Kotlin and AndroidX

I have tried:

smoothScrollToPosition(targetPosition)

It does the scrolling slowly (I can also control the speed by extending the LinearLayoutManager and overriding calculateSpeedPerPixel() - see How can i control the scrolling speed of recyclerView.smoothScrollToPosition(position)), however I can't control the exact location of the list item on the screen after the scrolling. All I know is it will be fully visible on the screen.

I have tried:

scrollToX(0, targetPosition * itemHeight - screenHeight / 2)

It gives me the error message: "W/RecyclerView: RecyclerView does not support scrolling to an absolute position. Use scrollToPosition instead"

I have also tried replacing the RecyclerView by a LinearLayoutManager which contains all the items as children, and translate the LinearLayoutManager in the view, but I don't get the children initially outside of the screen to draw.

Is there a solution to this issue?

Upvotes: 0

Views: 2157

Answers (2)

Roland Le Franc
Roland Le Franc

Reputation: 356

Thank you @Bob. I missed scrollBy(). Following your advice, here is the code that worked for me.

class RecyclerViewFixedItemSize : RecyclerView {
    var itemFixedSize : Int = 0

    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
    }

    constructor(context: Context) : super(context) {
    }

    fun smoothScrollToPositionCentered(position: Int) {
        // Find the fixed item size - once for all
        if (itemFixedSize == 0) {
            val ll = layoutManager as LinearLayoutManager
            val fp = ll.findFirstCompletelyVisibleItemPosition()
            val fv = ll.getChildAt(fp)
            itemFixedSize = fv!!.height
        }

        // find the recycler view position and screen coordinates of the first item (partially) visible on screen
        val ll = layoutManager as LinearLayoutManagerFixedItemSize
        val fp = ll.findFirstVisibleItemPosition()
        val fv = ll.getChildAt(0)

        // find the middle of the recycler view
        val dyrv = (top - bottom ) / 2

        // compute the number of pixels to scroll to get the view position in the center
        val dy = (position - fp) * itemFixedSize + dyrv + fv!!.top + itemFixedSize / 2

        smoothScrollBy(0, dy)
    }
}

enter image description here

Upvotes: 0

Bob
Bob

Reputation: 13865

You can calculate the smoothScrollToPosition's targetPosition based on your actual target position and the number of visible items.

A quick POC on how to do that:

  val scrollToPosition = 50

  val layoutManager = recyclerView.layoutManager as LinearLayoutManager
  val firstPosition = layoutManager.findFirstVisibleItemPosition()
  val lastPosition = layoutManager.findLastVisibleItemPosition()
  val visibleItems =  lastPosition - firstPosition + 1

  if (firstPosition < scrollToPosition) {
      recyclerView.smoothScrollToPosition(scrollToPosition + (visibleItems / 2))
  } else {
      recyclerView.smoothScrollToPosition(scrollToPosition - (visibleItems / 2))
  }

If you want more precise results that the item should be exactly at the middle of the screen, you can use the heights of the item (since its fixed) and the height of the RecyclerView, then calculate the offset to scroll. And call:

recyclerView.scrollBy(dx, dy)

Or:

recyclerView.smoothScrollBy(dx, dy)

Upvotes: 2

Related Questions