title: How to snap RecyclerView items so that every X items would be considered like a single unit to snap to?
body: <h2>Background</h2>
  <p>It's possible to snap a RecyclerView to its center using :</p>
  <pre><code>LinearSnapHelper().attachToRecyclerView(recyclerView)
  </code></pre>
  <p>Example:</p>
  <p><strong>MainActivity.kt</strong></p>
  <pre><code>class MainActivity : AppCompatActivity() {
  
      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_main)
          val inflater = LayoutInflater.from(this)
  
          recyclerView.adapter = object : RecyclerView.Adapter&lt;RecyclerView.ViewHolder&gt;() {
              override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                  val textView = holder.itemView as TextView
                  textView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
                  textView.text = position.toString()
              }
  
              override fun getItemCount(): Int {
                  return 100
              }
  
              override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
                  val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
                  val cellSize = recyclerView.width / 3
                  view.layoutParams.height = cellSize
                  view.layoutParams.width = cellSize
                  view.gravity = Gravity.CENTER
                  return object : RecyclerView.ViewHolder(view) {}
              }
          }
          LinearSnapHelper().attachToRecyclerView(recyclerView)
      }
  }
  </code></pre>
  <p><strong>activity_main.xml</strong></p>
  <pre><code>&lt;android.support.v7.widget.RecyclerView
      android:id=&quot;@+id/recyclerView&quot; xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
      xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot; xmlns:tools=&quot;http://schemas.android.com/tools&quot;
      android:layout_width=&quot;match_parent&quot; android:layout_height=&quot;match_parent&quot; android:orientation=&quot;horizontal&quot;
      app:layoutManager=&quot;android.support.v7.widget.LinearLayoutManager&quot;/&gt;
  </code></pre>
  <p>It's also possible to snap it to other sides, as was done in some libraries, such as <a href="https://github.com/DevExchanges/SnappingRecyclerview" rel="noreferrer"><strong>here</strong></a>.</p>
  <p>There are also libraries that allow to have a RecyclerView that can work like a ViewPager, such as <a href="https://github.com/lsjwzh/RecyclerViewPager" rel="noreferrer"><strong>here</strong></a>.</p>
  <h2>The problem</h2>
  <p>Supposed I have a RecyclerView (horizontal in my case) with many items, and I want that it will treat every X items (X is constant) as a single unit, and snap to each of those units.</p>
  <p>For example, if I scroll a bit, it could snap to either the 0-item, or the X-item, but not to something in between them.</p>
  <p>In a way, it's similar in its behavior to a case of a normal ViewPager, just that each page would have X items in it.</p>
  <p>For example, if we continue from the sample code I wrote above,suppose X==3 , the snapping would be from this idle state:</p>
  <p><a href="https://i.stack.imgur.com/fH5eC.png" rel="noreferrer"><img src="https://i.stack.imgur.com/fH5eC.png" alt="enter image description here" /></a></p>
  <p>to this idle state (in case we scrolled enough, otherwise would stay in previous state) :</p>
  <p><a href="https://i.stack.imgur.com/5TOBY.png" rel="noreferrer"><img src="https://i.stack.imgur.com/5TOBY.png" alt="enter image description here" /></a></p>
  <p>Flinging or scrolling more should be handled like on ViewPager, just like the library I've mentioned above.</p>
  <p>Scrolling more (in the same direction) to the next snapping point would be to reach item &quot;6&quot; , &quot;9&quot;, and so on...</p>
  <h2>What I tried</h2>
  <p>I tried to search for alternative libraries, and I also tried to read the docs regarding this, but I didn't find anything that might be useful.</p>
  <p>It might also be possible by using a ViewPager, but I think that's not the best way, because ViewPager doesn't recycle its items well, and I think it's less flexible than RecyclerView in terms of how to snap.</p>
  <h2>The questions</h2>
  <ol>
  <li><p>Is it possible to set RecyclerView to snap every X items, to treat each X items as a single page to snap to?</p>
  <p>Of course, the items will take enough space for the whole RecyclerView, evenly.</p>
  </li>
  <li><p>Supposed it is possible, how would I get a callback when the RecyclerView is about to snap to a certain item, including having this item, before it got snapped? I ask this because it's related to the same question I asked <a href="https://stackoverflow.com/q/47534838/878126"><strong>here</strong></a>.</p>
  </li>
  </ol>
  <hr />
  <h2>Kotlin solution</h2>
  <p>A working Kotlin solution based on &quot;Cheticamp&quot; answer (<a href="https://github.com/DevExchanges/SnappingRecyclerview" rel="noreferrer"><strong>here</strong></a>), without the need to verify that you have the RecyclerView size, and with the choice of having a grid instead of a list, in the sample:</p>
  <p><strong>MainActivity.kt</strong></p>
  <pre><code>class MainActivity : AppCompatActivity() {
      val USE_GRID = false
      //        val USE_GRID = true
      val ITEMS_PER_PAGE = 4
      var selectedItemPos = 0
  
      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_main)
          val inflater = LayoutInflater.from(this)
  
          recyclerView.adapter = object : RecyclerView.Adapter&lt;RecyclerView.ViewHolder&gt;() {
              override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                  val textView = holder.itemView as TextView
                  textView.setBackgroundColor(if (position % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
                  textView.text = if (selectedItemPos == position) &quot;selected: $position&quot; else position.toString()
              }
  
              override fun getItemCount(): Int {
                  return 100
              }
  
              override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
                  val view = inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
                  view.layoutParams.width = if (USE_GRID)
                      recyclerView.width / (ITEMS_PER_PAGE / 2)
                  else
                      recyclerView.width / 4
                  view.layoutParams.height = recyclerView.height / (ITEMS_PER_PAGE / 2)
                  view.gravity = Gravity.CENTER
                  return object : RecyclerView.ViewHolder(view) {
                  }
              }
          }
          recyclerView.layoutManager = if (USE_GRID)
              GridLayoutManager(this, ITEMS_PER_PAGE / 2, GridLayoutManager.HORIZONTAL, false)
          else
              LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
          val snapToBlock = SnapToBlock(recyclerView, ITEMS_PER_PAGE)
          snapToBlock.attachToRecyclerView(recyclerView)
          snapToBlock.setSnapBlockCallback(object : SnapToBlock.SnapBlockCallback {
              override fun onBlockSnap(snapPosition: Int) {
                  if (selectedItemPos == snapPosition)
                      return
                  selectedItemPos = snapPosition
                  recyclerView.adapter.notifyDataSetChanged()
              }
  
              override fun onBlockSnapped(snapPosition: Int) {
                  if (selectedItemPos == snapPosition)
                      return
                  selectedItemPos = snapPosition
                  recyclerView.adapter.notifyDataSetChanged()
              }
  
          })
      }
  
  }
  </code></pre>
  <p><strong>SnapToBlock.kt</strong></p>
  <pre><code>/**@param maxFlingBlocks Maxim blocks to move during most vigorous fling*/
  class SnapToBlock constructor(private val maxFlingBlocks: Int) : SnapHelper() {
      private var recyclerView: RecyclerView? = null
      // Total number of items in a block of view in the RecyclerView
      private var blocksize: Int = 0
      // Maximum number of positions to move on a fling.
      private var maxPositionsToMove: Int = 0
      // Width of a RecyclerView item if orientation is horizonal; height of the item if vertical
      private var itemDimension: Int = 0
      // Callback interface when blocks are snapped.
      private var snapBlockCallback: SnapBlockCallback? = null
      // When snapping, used to determine direction of snap.
      private var priorFirstPosition = RecyclerView.NO_POSITION
      // Our private scroller
      private var scroller: Scroller? = null
      // Horizontal/vertical layout helper
      private var orientationHelper: OrientationHelper? = null
      // LTR/RTL helper
      private var layoutDirectionHelper: LayoutDirectionHelper? = null
  
      @Throws(IllegalStateException::class)
      override fun attachToRecyclerView(recyclerView: RecyclerView?) {
          if (recyclerView != null) {
              this.recyclerView = recyclerView
              val layoutManager = recyclerView.layoutManager as LinearLayoutManager
              orientationHelper = when {
                  layoutManager.canScrollHorizontally() -&gt; OrientationHelper.createHorizontalHelper(layoutManager)
                  layoutManager.canScrollVertically() -&gt; OrientationHelper.createVerticalHelper(layoutManager)
                  else -&gt; throw IllegalStateException(&quot;RecyclerView must be scrollable&quot;)
              }
              scroller = Scroller(this.recyclerView!!.context, sInterpolator)
              initItemDimensionIfNeeded(layoutManager)
          }
          super.attachToRecyclerView(recyclerView)
      }
  
      // Called when the target view is available and we need to know how much more
      // to scroll to get it lined up with the side of the RecyclerView.
      override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray {
          val out = IntArray(2)
          initLayoutDirectionHelperIfNeeded(layoutManager)
          if (layoutManager.canScrollHorizontally())
              out[0] = layoutDirectionHelper!!.getScrollToAlignView(targetView)
          if (layoutManager.canScrollVertically())
              out[1] = layoutDirectionHelper!!.getScrollToAlignView(targetView)
          if (snapBlockCallback != null)
              if (out[0] == 0 &amp;&amp; out[1] == 0)
                  snapBlockCallback!!.onBlockSnapped(layoutManager.getPosition(targetView))
              else
                  snapBlockCallback!!.onBlockSnap(layoutManager.getPosition(targetView))
          return out
      }
  
      private fun initLayoutDirectionHelperIfNeeded(layoutManager: RecyclerView.LayoutManager) {
          if (layoutDirectionHelper == null)
              if (layoutManager.canScrollHorizontally())
                  layoutDirectionHelper = LayoutDirectionHelper()
              else if (layoutManager.canScrollVertically())
              // RTL doesn't matter for vertical scrolling for this class.
                  layoutDirectionHelper = LayoutDirectionHelper(false)
      }
  
      // We are flinging and need to know where we are heading.
      override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int): Int {
          initLayoutDirectionHelperIfNeeded(layoutManager)
          val lm = layoutManager as LinearLayoutManager
          initItemDimensionIfNeeded(layoutManager)
          scroller!!.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE)
          return when {
              velocityX != 0 -&gt; layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalX, itemDimension)
              else -&gt; if (velocityY != 0)
                  layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalY, itemDimension)
              else RecyclerView.NO_POSITION
          }
      }
  
      // We have scrolled to the neighborhood where we will snap. Determine the snap position.
      override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
          // Snap to a view that is either 1) toward the bottom of the data and therefore on screen,
          // or, 2) toward the top of the data and may be off-screen.
          val snapPos = calcTargetPosition(layoutManager as LinearLayoutManager)
          val snapView = if (snapPos == RecyclerView.NO_POSITION)
              null
          else
              layoutManager.findViewByPosition(snapPos)
          if (snapView == null)
              Log.d(TAG, &quot;&lt;&lt;&lt;&lt;findSnapView is returning null!&quot;)
          Log.d(TAG, &quot;&lt;&lt;&lt;&lt;findSnapView snapos=&quot; + snapPos)
          return snapView
      }
  
      // Does the heavy lifting for findSnapView.
      private fun calcTargetPosition(layoutManager: LinearLayoutManager): Int {
          val snapPos: Int
          initLayoutDirectionHelperIfNeeded(layoutManager)
          val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
          if (firstVisiblePos == RecyclerView.NO_POSITION)
              return RecyclerView.NO_POSITION
          initItemDimensionIfNeeded(layoutManager)
          if (firstVisiblePos &gt;= priorFirstPosition) {
              // Scrolling toward bottom of data
              val firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition()
              snapPos = if (firstCompletePosition != RecyclerView.NO_POSITION &amp;&amp; firstCompletePosition % blocksize == 0)
                  firstCompletePosition
              else
                  roundDownToBlockSize(firstVisiblePos + blocksize)
          } else {
              // Scrolling toward top of data
              snapPos = roundDownToBlockSize(firstVisiblePos)
              // Check to see if target view exists. If it doesn't, force a smooth scroll.
              // SnapHelper only snaps to existing views and will not scroll to a non-existant one.
              // If limiting fling to single block, then the following is not needed since the
              // views are likely to be in the RecyclerView pool.
              if (layoutManager.findViewByPosition(snapPos) == null) {
                  val toScroll = layoutDirectionHelper!!.calculateDistanceToScroll(layoutManager, snapPos)
                  recyclerView!!.smoothScrollBy(toScroll[0], toScroll[1], sInterpolator)
              }
          }
          priorFirstPosition = firstVisiblePos
          return snapPos
      }
  
      private fun initItemDimensionIfNeeded(layoutManager: RecyclerView.LayoutManager) {
          if (itemDimension != 0)
              return
          val child = layoutManager.getChildAt(0) ?: return
          if (layoutManager.canScrollHorizontally()) {
              itemDimension = child.width
              blocksize = getSpanCount(layoutManager) * (recyclerView!!.width / itemDimension)
          } else if (layoutManager.canScrollVertically()) {
              itemDimension = child.height
              blocksize = getSpanCount(layoutManager) * (recyclerView!!.height / itemDimension)
          }
          maxPositionsToMove = blocksize * maxFlingBlocks
      }
  
      private fun getSpanCount(layoutManager: RecyclerView.LayoutManager): Int = (layoutManager as? GridLayoutManager)?.spanCount ?: 1
  
      private fun roundDownToBlockSize(trialPosition: Int): Int = trialPosition - trialPosition % blocksize
  
      private fun roundUpToBlockSize(trialPosition: Int): Int = roundDownToBlockSize(trialPosition + blocksize - 1)
  
      override fun createScroller(layoutManager: RecyclerView.LayoutManager): LinearSmoothScroller? {
          return if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider)
              null
          else object : LinearSmoothScroller(recyclerView!!.context) {
              override fun onTargetFound(targetView: View, state: RecyclerView.State?, action: RecyclerView.SmoothScroller.Action) {
                  val snapDistances = calculateDistanceToFinalSnap(recyclerView!!.layoutManager, targetView)
                  val dx = snapDistances[0]
                  val dy = snapDistances[1]
                  val time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)))
                  if (time &gt; 0)
                      action.update(dx, dy, time, sInterpolator)
              }
  
              override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float = MILLISECONDS_PER_INCH / displayMetrics.densityDpi
          }
      }
  
      fun setSnapBlockCallback(callback: SnapBlockCallback?) {
          snapBlockCallback = callback
      }
  
      /*
          Helper class that handles calculations for LTR and RTL layouts.
       */
      private inner class LayoutDirectionHelper {
          // Is the layout an RTL one?
          private val mIsRTL: Boolean
  
          constructor() {
              mIsRTL = ViewCompat.getLayoutDirection(recyclerView) == ViewCompat.LAYOUT_DIRECTION_RTL
          }
  
          constructor(isRTL: Boolean) {
              mIsRTL = isRTL
          }
  
          /*
              Calculate the amount of scroll needed to align the target view with the layout edge.
           */
          fun getScrollToAlignView(targetView: View): Int = if (mIsRTL)
              orientationHelper!!.getDecoratedEnd(targetView) - recyclerView!!.width
          else
              orientationHelper!!.getDecoratedStart(targetView)
  
          /**
           * Calculate the distance to final snap position when the view corresponding to the snap
           * position is not currently available.
           *
           * @param layoutManager LinearLayoutManager or descendent class
           * @param targetPos     - Adapter position to snap to
           * @return int[2] {x-distance in pixels, y-distance in pixels}
           */
          fun calculateDistanceToScroll(layoutManager: LinearLayoutManager, targetPos: Int): IntArray {
              val out = IntArray(2)
              val firstVisiblePos = layoutManager.findFirstVisibleItemPosition()
              if (layoutManager.canScrollHorizontally()) {
                  if (targetPos &lt;= firstVisiblePos)  // scrolling toward top of data
                      if (mIsRTL) {
                          val lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition())
                          out[0] = orientationHelper!!.getDecoratedEnd(lastView) + (firstVisiblePos - targetPos) * itemDimension
                      } else {
                          val firstView = layoutManager.findViewByPosition(firstVisiblePos)
                          out[0] = orientationHelper!!.getDecoratedStart(firstView) - (firstVisiblePos - targetPos) * itemDimension
                      }
              }
              if (layoutManager.canScrollVertically() &amp;&amp; targetPos &lt;= firstVisiblePos) { // scrolling toward top of data
                  val firstView = layoutManager.findViewByPosition(firstVisiblePos)
                  out[1] = firstView.top - (firstVisiblePos - targetPos) * itemDimension
              }
              return out
          }
  
          /*
              Calculate the number of positions to move in the RecyclerView given a scroll amount
              and the size of the items to be scrolled. Return integral multiple of mBlockSize not
              equal to zero.
           */
          fun getPositionsToMove(llm: LinearLayoutManager, scroll: Int, itemSize: Int): Int {
              var positionsToMove: Int
              positionsToMove = roundUpToBlockSize(Math.abs(scroll) / itemSize)
              if (positionsToMove &lt; blocksize)
              // Must move at least one block
                  positionsToMove = blocksize
              else if (positionsToMove &gt; maxPositionsToMove)
              // Clamp number of positions to move so we don't get wild flinging.
                  positionsToMove = maxPositionsToMove
              if (scroll &lt; 0)
                  positionsToMove *= -1
              if (mIsRTL)
                  positionsToMove *= -1
              return if (layoutDirectionHelper!!.isDirectionToBottom(scroll &lt; 0)) {
                  // Scrolling toward the bottom of data.
                  roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionsToMove
              } else roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove
              // Scrolling toward the top of the data.
          }
  
          fun isDirectionToBottom(velocityNegative: Boolean): Boolean = if (mIsRTL) velocityNegative else !velocityNegative
      }
  
      interface SnapBlockCallback {
          fun onBlockSnap(snapPosition: Int)
          fun onBlockSnapped(snapPosition: Int)
      }
  
      companion object {
          // Borrowed from ViewPager.java
          private val sInterpolator = Interpolator { input -&gt;
              var t = input
              // _o(t) = t * t * ((tension + 1) * t + tension)
              // o(t) = _o(t - 1) + 1
              t -= 1.0f
              t * t * t + 1.0f
          }
  
          private val MILLISECONDS_PER_INCH = 100f
          private val TAG = &quot;SnapToBlock&quot;
      }
  }
  </code></pre>
  <hr />
  <h2>Update</h2>
  <p>Even though I've marked <a href="https://stackoverflow.com/a/47580753/878126"><strong>an answer</strong></a> as accepted, as it works fine, I've noticed it has serious issues:</p>
  <ol>
  <li><p>Smooth scrolling doesn't seem to work fine (doesn't scroll to correct place). Only scrolling that work is as such (but with the &quot;smearing&quot; effect) :</p>
  <pre><code>(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPos,0)
  </code></pre>
  </li>
  <li><p>When switching to RTL (Right to left) locale such as Hebrew (&quot;עברית&quot;), it doesn't let me scroll at all.</p>
  </li>
  <li><p>I've noticed that <code>onCreateViewHolder</code> is called a lot. In fact it is called every time I scroll, even for times it should have recycled the ViewHolders. This means there is an excessive creation of views, and it might also mean there is a memory leak.</p>
  </li>
  </ol>
  <p>I've tried to fix those myself, but failed so far.</p>
  <p>If anyone here knows how to fix it, I will grant the extra, new bounty</p>
  <hr />
  <p>Update: as we got a fix for RTL/LTR, I've updated the Kotlin solution within this post.</p>
  <hr />
  <p>Update: about point #3 , this seems to be because there is a pool of views for the recyclerView, which gets filled too soon. To handle this, we can simply enlarge the pool size, by using <code>recyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType, Integer.MAX_VALUE) </code> for each view type we have in it. Weird thing that this is really needed. I've posted about it to Google (<a href="https://issuetracker.google.com/issues/71784689" rel="noreferrer"><strong>here</strong></a> and <a href="https://issuetracker.google.com/issues/72301192" rel="noreferrer"><strong>here</strong></a>) but was rejected that the pool should be unlimited by default. In the end, I decided to at least request to have a more convinient function to do it for all view types (<a href="https://issuetracker.google.com/issues/110979418" rel="noreferrer"><strong>here</strong></a>).</p>