Disable vertical scrolling in view pager 2 - android

I'm using viewPager2 along with tab layout. I only have 2 fragments.
There is a RecyclerView in my second fragment and a swipe refresh layout.
My problem is when I try to swipe down to refresh , it conflicts with the Viewpager's vertical swipe and I'm not able to swipe down correctly.
So is there a way to disable just the vertical swipe of view Pager ? I still want it to be able to swipe horizontally.

You can disable viewpager2 scroll by setting isUserInputEnabled to false
viewPager2.isUserInputEnabled = false
This will disable all events, but in your case i think you need to disable move down action only, you need to intercept touch listener and call requestDisallowInterceptTouchEvent(true) on view pager.
You can use this wrapper from google samples
/**
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
*
* This solution has limitations when using multiple levels of nested scrollable elements
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
*/
class NestedScrollableHost : FrameLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
while (v != null && v !is ViewPager2) {
v = v.parent as? View
}
return v as? ViewPager2
}
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
val direction = -delta.sign.toInt()
return when (orientation) {
0 -> child?.canScrollHorizontally(direction) ?: false
1 -> child?.canScrollVertically(direction) ?: false
else -> throw IllegalArgumentException()
}
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
}
Then wrap your view pager contents -recyclerView- ,
<androidx.viewpager2.integration.testapp.NestedScrollableHost
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/first_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF" />
</androidx.viewpager2.integration.testapp.NestedScrollableHost>

ViewPager2 scrolling Disable
viewPager2 user input scrolling disable.
import androidx.viewpager2.widget.ViewPager2;
private ViewPager2 viewPager;
viewPager = findViewById(R.id.view_pager);
viewPager.setUserInputEnabled(false);

Related

how to disable scrolling "parent.requestDisallowInterceptTouchEvent(false) " in tabview2 if isUserInputEnabled = false?

Error horizontal ViewPager2 setScrollEnabled = false in a horizontal ViewPager2 . parent pageview2 cant scroll . I want to check when isUserInputEnabled = false then set parent.requestDisallowInterceptTouchEvent(false) . Can you provide the code?
this is func in react-native-pager-view/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
if (scaledDx > touchSlop || scaledDy > touchSlop) {
if (isVpHorizontal == (scaledDy > scaledDx)) {
// Gesture is perpendicular, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
} else {
// Gesture is parallel, query child if movement in that direction is possible
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
// Child can scroll, disallow all parents to intercept
parent.requestDisallowInterceptTouchEvent(true)
} else {
// Child cannot scroll, allow all parents to intercept
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
}
}
I tried but I can't finish , I don't know kotlin

ViewPager2: set a different padding for first and last page

I have implemented a "page peek" feature for my ViewPager2:
private fun setViewPager() {
inventoryVp?.apply {
clipToPadding = false // allow full width shown with padding
clipChildren = false // allow left/right item is not clipped
offscreenPageLimit = 2 // make sure left/right item is rendered
}
inventoryVp?.setPadding(Utility.dpToPx(25), 0, Utility.dpToPx(25), 0)
val pageMarginPx = Utility.dpToPx(6)
val marginTransformer = MarginPageTransformer(pageMarginPx)
inventoryVp?.setPageTransformer(marginTransformer)
}
Doing this I am able to view a portion of the previous and next page. But first and last page show a bigger white space because there's no other page in this direction to show.
How can I set a different padding for the first and last page?
I solved it using ItemDecoration.
class CartOOSVPItemDecoration(val marginStart: Int,
val marginEnd: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (parent.getChildAdapterPosition(view) == 0) {
outRect.left = marginStart
}
else if(parent.getChildAdapterPosition(view) == ((parent.adapter?.itemCount ?: 0) - 1)) {
outRect.right = marginEnd
}
}
}
inventoryVp?.addItemDecoration( CartOOSVPItemDecoration(Utility.dpToPx(-9), Utility.dpToPx(-9)))

Simple way to expand/collapse bottom frame by pulling it up/down in Android

I have layout like this:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context=".MainActivity"
tools:showIn="#layout/app_bar_main"
android:orientation="vertical">
<FrameLayout
android:id="#+id/folderContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<FrameLayout
android:id="#+id/playerControlsContainer"
android:layout_width="match_parent"
android:layout_height="#dimen/control_view_height_min"
android:visibility="visible">
<fragment
android:id="#+id/playerControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:name="eu.zderadicka.audioserve.fragments.ControllerFragment"
/>
</FrameLayout>
</LinearLayout>
And I want to achieve this:
by pulling (scrolling) up bottom frame up to expand it's height from control_view_height_min (80dp) to control_view_height_max (200dp)
when fram is expanded pull it back again to control_view_height_min by pulling (scrolling) down
while pulling change height as I pull
after stop pulling animate rest of the way
I've have been looking at "Android Sliding Up Panel", but it looks bit too complex for this case and it always pulls bottom frame to full screen.
So my question is what would be simplest way to do it in standard SDK? What to use and how? Are the some code samples close to my requirements.
Thanks for any tips.
Ok so I had to find it myself. Created subclass of FrameLayout, which can be expanded/collapsed by dragging. Much simpler then Sliding Up Panel and what is more important it works in my setup. If anyone is interested (source in kotlin):
package eu.zderadicka.audioserve.ui
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ViewConfiguration
import android.widget.FrameLayout
import eu.zderadicka.audioserve.R
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
private const val LOG_TAG = "ExpandableFrameLayout"
class ExpandableFrameLayout #JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0, defStyleRes: Int = 0)
: FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
var minHeight: Int = 0
var maxHeight: Int = 0
val gestureDetector: GestureDetector
var animator: ValueAnimator? = null
private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop
var midHeight: Int = 0
fun animate(toValue: Int) {
animator?.cancel()
animator = ValueAnimator.ofInt(height, toValue).apply {
this.addUpdateListener {
layoutParams.height = it.animatedValue as Int
requestLayout()
}
start()
}
}
inner class MyGestureListener: GestureDetector.SimpleOnGestureListener() {
var isDragging = false
var startHeight = 0
override fun onDown(e: MotionEvent?): Boolean {
startHeight = layoutParams.height
isDragging = false
return true
}
// override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
// Log.d(LOG_TAG, "Fling with velocity $velocityY")
// isDragging = true
// if (velocityY < 0 && height < maxHeight) {
// animate(maxHeight)
//
// } else if (velocityY > 0 && height > minHeight) {
// animate(minHeight)
// }
//
// return true
// }
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
val dist = e1.rawY - e2.rawY
Log.d(LOG_TAG, "Scroll in Y $dist")
isDragging = true
if (dist > touchSlop && height < maxHeight) {
layoutParams.height = min(startHeight + dist.roundToInt(), maxHeight)
requestLayout()
} else if (dist < -touchSlop && height > minHeight) {
layoutParams.height = max(startHeight + dist.roundToInt(), minHeight)
requestLayout()
}
return true
}
fun dragging() = isDragging
}
val gestureListener = MyGestureListener()
init {
val customAttrs = context.theme.obtainStyledAttributes(attrs, R.styleable.ExpandableFrameLayout, 0, 0)
minHeight = customAttrs.getLayoutDimension(R.styleable.ExpandableFrameLayout_minExpandableHeight, 0)
maxHeight = customAttrs.getLayoutDimension(R.styleable.ExpandableFrameLayout_maxExpandableHeight, 1024)
midHeight = minHeight+ (maxHeight - minHeight) / 2
gestureDetector = GestureDetector(context, this.gestureListener)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
Log.d(LOG_TAG, "Player touch $event")
if (event.actionMasked == MotionEvent.ACTION_UP) {
if (layoutParams.height >= midHeight && layoutParams.height < maxHeight)
animate(maxHeight)
else if (layoutParams.height < midHeight && layoutParams.height > minHeight)
animate(minHeight)
}
return gestureDetector.onTouchEvent(event)
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
//listen also to all child events in upper part
if (event.y <= minHeight) {
onTouchEvent(event)
// need to block up event, which were dragging
if (event.actionMasked == MotionEvent.ACTION_UP) {
return gestureListener.isDragging
}
}
return super.onInterceptTouchEvent(event)
}
// this should be called from containing frame/activity in onPause
fun onPause() {
if (layoutParams.height >= minHeight) {
layoutParams?.height = minHeight
}
}
}
Gotchas:
when changing height or other layout properties call requestLayout
use rawY for scrolling - ( y is related to the view and thus scrolling motion was shaky)
as child views (buttons in my case) are consuming motion events I had to send them also from onIntercentTouchEvent plus block then unwanted ACTION_UP events.
finally it was easier and gave better results to just use onScroll - it gave better experience (but for shorter bottom panel) - combination with onFling gave bit more complex movements.
I wish somebody told me about those things here, it could save me some hours. However it looks like SO is now more about down voting then providing useful hints.

How to add a click listener to ItemDecoration in RecyclerView Android?

I have tried to add a click listener to itemdecoration like below but still no luck. Please help.
recyclerview.addOnItemTouchListener( object : RecyclerView.OnItemTouchListener{
override fun onTouchEvent(rv: RecyclerView?, e: MotionEvent?) {
}
override fun onInterceptTouchEvent(rv: RecyclerView?, e: MotionEvent?): Boolean {
val view = rv!!.findChildViewUnder(e!!.x, e!!.y)
if(view == null) return false
when(view.id){
R.id.list_item_section_text->{
Log.d("Clicked","Header")
}
}
return false
}
override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
}
})
ItemDecoration will help you draw something between items, but the View you're drawing in is actually the RecyclerView itself (according to Layout inspector), via its Canvas. So you won't be able to add an basic onClickListener on that decorations.
According to your code, I guess you have an item decoration for each of your items header?
What I would do for this would be not to consider my header as an ItemDecoration but as an item (with different type) I would feed my RecyclerView with.
sealed class RecyclerViewItem
object MainItem : RecyclerViewItem()
object Header : RecyclerViewItem()
In your adapter (with items: RecyclerViewItem) you override getItemViewType method using custom ids. Then in onBindViewHolder you can check the item view type and add your onClickListener on your view if it's a Header.
For more info, you can search for building RecyclerView with different item type.
You can customize a RecyclerView and check if touch event arrives to ItemDecorator or no:
class YourRecyclerView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
private val clicksFlow: MutableSharedFlow<ClickEvent> = MutableSharedFlow(
extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST
)
fun getClicksFlow() = clicksFlow as Flow<RecyclerClickEvent>
#SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(e: MotionEvent) : Boolean{
if (e.action == MotionEvent.ACTION_DOWN) {
for (i in 0 until itemDecorationCount) {
val decor = getItemDecorationAt(i)
if (decor is YourDecoration && decor.isOnTouched(this, e)) {
clicksFlow.tryEmit(ClickEvent(e.x, e.y))
return true
}
}
}
return super.onTouchEvent(e)
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
if (e.action == MotionEvent.ACTION_DOWN) {
for (i in 0 until itemDecorationCount) {
val decor = getItemDecorationAt(i)
if (decor is YourDecoration && decor.isOnTouched(this, e)) {
return true
}
}
}
return super.onInterceptTouchEvent(e)
}
}
class YourDecoration(/*some stuff*/) : RecyclerView.ItemDecoration() {
fun isOnTouched(parent: YourRecyclerView , e: MotionEvent): Boolean {
val w = abs(scaleX * width)
val h = abs(scaleY * height)
val top = topMargin - height / 2F - parent.paddingTop
return if (scaleX > 0) {
val side = parent.measuredWidth - sideMargin
e.y >= top && e.y <= (top + h) && side >= e.x && (side - w) <= e.x
} else {
val side = sideMargin
e.y >= top && e.y <= (top + h) && side <= e.x && e.x <= (side + w)
}
}
}

How to snap RecyclerView items so that every X items would be considered like a single unit to snap to?

Background
It's possible to snap a RecyclerView to its center using :
LinearSnapHelper().attachToRecyclerView(recyclerView)
Example:
MainActivity.kt
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<RecyclerView.ViewHolder>() {
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)
}
}
activity_main.xml
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"/>
It's also possible to snap it to other sides, as was done in some libraries, such as here.
There are also libraries that allow to have a RecyclerView that can work like a ViewPager, such as here.
The problem
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.
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.
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.
For example, if we continue from the sample code I wrote above,suppose X==3 , the snapping would be from this idle state:
to this idle state (in case we scrolled enough, otherwise would stay in previous state) :
Flinging or scrolling more should be handled like on ViewPager, just like the library I've mentioned above.
Scrolling more (in the same direction) to the next snapping point would be to reach item "6" , "9", and so on...
What I tried
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.
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.
The questions
Is it possible to set RecyclerView to snap every X items, to treat each X items as a single page to snap to?
Of course, the items will take enough space for the whole RecyclerView, evenly.
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 here.
Kotlin solution
A working Kotlin solution based on "Cheticamp" answer (here), 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:
MainActivity.kt
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<RecyclerView.ViewHolder>() {
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) "selected: $position" 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()
}
})
}
}
SnapToBlock.kt
/**#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() -> OrientationHelper.createHorizontalHelper(layoutManager)
layoutManager.canScrollVertically() -> OrientationHelper.createVerticalHelper(layoutManager)
else -> throw IllegalStateException("RecyclerView must be scrollable")
}
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 && 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 -> layoutDirectionHelper!!.getPositionsToMove(lm, scroller!!.finalX, itemDimension)
else -> 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, "<<<<findSnapView is returning null!")
Log.d(TAG, "<<<<findSnapView snapos=" + 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 >= priorFirstPosition) {
// Scrolling toward bottom of data
val firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition()
snapPos = if (firstCompletePosition != RecyclerView.NO_POSITION && 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 > 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 <= 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() && targetPos <= 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 < blocksize)
// Must move at least one block
positionsToMove = blocksize
else if (positionsToMove > maxPositionsToMove)
// Clamp number of positions to move so we don't get wild flinging.
positionsToMove = maxPositionsToMove
if (scroll < 0)
positionsToMove *= -1
if (mIsRTL)
positionsToMove *= -1
return if (layoutDirectionHelper!!.isDirectionToBottom(scroll < 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 ->
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 = "SnapToBlock"
}
}
Update
Even though I've marked an answer as accepted, as it works fine, I've noticed it has serious issues:
Smooth scrolling doesn't seem to work fine (doesn't scroll to correct place). Only scrolling that work is as such (but with the "smearing" effect) :
(recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPos,0)
When switching to RTL (Right to left) locale such as Hebrew ("עברית"), it doesn't let me scroll at all.
I've noticed that onCreateViewHolder 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.
I've tried to fix those myself, but failed so far.
If anyone here knows how to fix it, I will grant the extra, new bounty
Update: as we got a fix for RTL/LTR, I've updated the Kotlin solution within this post.
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 recyclerView.getRecycledViewPool() .setMaxRecycledViews(viewType, Integer.MAX_VALUE) for each view type we have in it. Weird thing that this is really needed. I've posted about it to Google (here and here) 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 (here).
SnapHelper supplies the necessary framework for what you are attempting, but it needs to be extended to handle blocks of views. The class SnapToBlock below extends SnapHelper to snap to blocks of views. In the example, I have used four views to a block but it can be more or less.
Update: The code has been change to accommodate GridLayoutManager as well as LinearLayoutManager. Flinging is now inhibited so the snapping works more list a ViewPager. Horizontal and vertical scrolling is now supported as well as LTR and RTL layouts.
Update: Changed smooth scroll interpolator to be more like ViewPager.
Update: Adding callbacks for pre/post snapping.
Update: Adding support for RTL layouts.
Here is a quick video of the sample app:
Set up the layout manager as follows:
// For LinearLayoutManager horizontal orientation
recyclerView.setLayoutManager(new LinearLayoutManager(this, RecyclerView.HORIZONTAL, false));
// For GridLayoutManager vertical orientation
recyclerView.setLayoutManager(new GridLayoutManager(this, SPAN_COUNT, RecyclerView.VERTICAL, false));
Add the following to attach the SnapToBlock to the RecyclerView.
SnapToBlock snapToBlock = new SnapToBlock(mMaxFlingPages);
snapToBlock.attachToRecyclerView(recyclerView);
mMaxFlingPages is the maximum number of blocks (rowsCols * spans) to allow to be flung at one time.
For call backs when a snap is about to be made and has been completed, add the following:
snapToBlock.setSnapBlockCallback(new SnapToBlock.SnapBlockCallback() {
#Override
public void onBlockSnap(int snapPosition) {
...
}
#Override
public void onBlockSnapped(int snapPosition) {
...
}
});
SnapToBlock.java
/* The number of items in the RecyclerView should be a multiple of block size; otherwise, the
extra item views will not be positioned on a block boundary when the end of the data is reached.
Pad out with empty item views if needed.
Updated to accommodate RTL layouts.
*/
public class SnapToBlock extends SnapHelper {
private RecyclerView mRecyclerView;
// Total number of items in a block of view in the RecyclerView
private int mBlocksize;
// Maximum number of positions to move on a fling.
private int mMaxPositionsToMove;
// Width of a RecyclerView item if orientation is horizonal; height of the item if vertical
private int mItemDimension;
// Maxim blocks to move during most vigorous fling.
private final int mMaxFlingBlocks;
// Callback interface when blocks are snapped.
private SnapBlockCallback mSnapBlockCallback;
// When snapping, used to determine direction of snap.
private int mPriorFirstPosition = RecyclerView.NO_POSITION;
// Our private scroller
private Scroller mScroller;
// Horizontal/vertical layout helper
private OrientationHelper mOrientationHelper;
// LTR/RTL helper
private LayoutDirectionHelper mLayoutDirectionHelper;
// Borrowed from ViewPager.java
private static final Interpolator sInterpolator = new Interpolator() {
public float getInterpolation(float t) {
// _o(t) = t * t * ((tension + 1) * t + tension)
// o(t) = _o(t - 1) + 1
t -= 1.0f;
return t * t * t + 1.0f;
}
};
SnapToBlock(int maxFlingBlocks) {
super();
mMaxFlingBlocks = maxFlingBlocks;
}
#Override
public void attachToRecyclerView(#Nullable final RecyclerView recyclerView)
throws IllegalStateException {
if (recyclerView != null) {
mRecyclerView = recyclerView;
final LinearLayoutManager layoutManager =
(LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager.canScrollHorizontally()) {
mOrientationHelper = OrientationHelper.createHorizontalHelper(layoutManager);
mLayoutDirectionHelper =
new LayoutDirectionHelper(ViewCompat.getLayoutDirection(mRecyclerView));
} else if (layoutManager.canScrollVertically()) {
mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager);
// RTL doesn't matter for vertical scrolling for this class.
mLayoutDirectionHelper = new LayoutDirectionHelper(RecyclerView.LAYOUT_DIRECTION_LTR);
} else {
throw new IllegalStateException("RecyclerView must be scrollable");
}
mScroller = new Scroller(mRecyclerView.getContext(), 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.
#NonNull
#Override
public int[] calculateDistanceToFinalSnap(#NonNull RecyclerView.LayoutManager layoutManager,
#NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = mLayoutDirectionHelper.getScrollToAlignView(targetView);
}
if (layoutManager.canScrollVertically()) {
out[1] = mLayoutDirectionHelper.getScrollToAlignView(targetView);
}
if (mSnapBlockCallback != null) {
if (out[0] == 0 && out[1] == 0) {
mSnapBlockCallback.onBlockSnapped(layoutManager.getPosition(targetView));
} else {
mSnapBlockCallback.onBlockSnap(layoutManager.getPosition(targetView));
}
}
return out;
}
// We are flinging and need to know where we are heading.
#Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager,
int velocityX, int velocityY) {
LinearLayoutManager lm = (LinearLayoutManager) layoutManager;
initItemDimensionIfNeeded(layoutManager);
mScroller.fling(0, 0, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE,
Integer.MIN_VALUE, Integer.MAX_VALUE);
if (velocityX != 0) {
return mLayoutDirectionHelper
.getPositionsToMove(lm, mScroller.getFinalX(), mItemDimension);
}
if (velocityY != 0) {
return mLayoutDirectionHelper
.getPositionsToMove(lm, mScroller.getFinalY(), mItemDimension);
}
return RecyclerView.NO_POSITION;
}
// We have scrolled to the neighborhood where we will snap. Determine the snap position.
#Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
// 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.
int snapPos = calcTargetPosition((LinearLayoutManager) layoutManager);
View snapView = (snapPos == RecyclerView.NO_POSITION)
? null : layoutManager.findViewByPosition(snapPos);
if (snapView == null) {
Log.d(TAG, "<<<<findSnapView is returning null!");
}
Log.d(TAG, "<<<<findSnapView snapos=" + snapPos);
return snapView;
}
// Does the heavy lifting for findSnapView.
private int calcTargetPosition(LinearLayoutManager layoutManager) {
int snapPos;
int firstVisiblePos = layoutManager.findFirstVisibleItemPosition();
if (firstVisiblePos == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}
initItemDimensionIfNeeded(layoutManager);
if (firstVisiblePos >= mPriorFirstPosition) {
// Scrolling toward bottom of data
int firstCompletePosition = layoutManager.findFirstCompletelyVisibleItemPosition();
if (firstCompletePosition != RecyclerView.NO_POSITION
&& firstCompletePosition % mBlocksize == 0) {
snapPos = firstCompletePosition;
} else {
snapPos = roundDownToBlockSize(firstVisiblePos + mBlocksize);
}
} 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) {
int[] toScroll = mLayoutDirectionHelper.calculateDistanceToScroll(layoutManager, snapPos);
mRecyclerView.smoothScrollBy(toScroll[0], toScroll[1], sInterpolator);
}
}
mPriorFirstPosition = firstVisiblePos;
return snapPos;
}
private void initItemDimensionIfNeeded(final RecyclerView.LayoutManager layoutManager) {
if (mItemDimension != 0) {
return;
}
View child;
if ((child = layoutManager.getChildAt(0)) == null) {
return;
}
if (layoutManager.canScrollHorizontally()) {
mItemDimension = child.getWidth();
mBlocksize = getSpanCount(layoutManager) * (mRecyclerView.getWidth() / mItemDimension);
} else if (layoutManager.canScrollVertically()) {
mItemDimension = child.getHeight();
mBlocksize = getSpanCount(layoutManager) * (mRecyclerView.getHeight() / mItemDimension);
}
mMaxPositionsToMove = mBlocksize * mMaxFlingBlocks;
}
private int getSpanCount(RecyclerView.LayoutManager layoutManager) {
return (layoutManager instanceof GridLayoutManager)
? ((GridLayoutManager) layoutManager).getSpanCount()
: 1;
}
private int roundDownToBlockSize(int trialPosition) {
return trialPosition - trialPosition % mBlocksize;
}
private int roundUpToBlockSize(int trialPosition) {
return roundDownToBlockSize(trialPosition + mBlocksize - 1);
}
#Nullable
protected LinearSmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
#Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.getLayoutManager(),
targetView);
final int dx = snapDistances[0];
final int dy = snapDistances[1];
final int time = calculateTimeForDeceleration(Math.max(Math.abs(dx), Math.abs(dy)));
if (time > 0) {
action.update(dx, dy, time, sInterpolator);
}
}
#Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
public void setSnapBlockCallback(#Nullable SnapBlockCallback callback) {
mSnapBlockCallback = callback;
}
/*
Helper class that handles calculations for LTR and RTL layouts.
*/
private class LayoutDirectionHelper {
// Is the layout an RTL one?
private final boolean mIsRTL;
#TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
LayoutDirectionHelper(int direction) {
mIsRTL = direction == View.LAYOUT_DIRECTION_RTL;
}
/*
Calculate the amount of scroll needed to align the target view with the layout edge.
*/
int getScrollToAlignView(View targetView) {
return (mIsRTL)
? mOrientationHelper.getDecoratedEnd(targetView) - mRecyclerView.getWidth()
: mOrientationHelper.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}
*/
int[] calculateDistanceToScroll(LinearLayoutManager layoutManager, int targetPos) {
int[] out = new int[2];
int firstVisiblePos;
firstVisiblePos = layoutManager.findFirstVisibleItemPosition();
if (layoutManager.canScrollHorizontally()) {
if (targetPos <= firstVisiblePos) { // scrolling toward top of data
if (mIsRTL) {
View lastView = layoutManager.findViewByPosition(layoutManager.findLastVisibleItemPosition());
out[0] = mOrientationHelper.getDecoratedEnd(lastView)
+ (firstVisiblePos - targetPos) * mItemDimension;
} else {
View firstView = layoutManager.findViewByPosition(firstVisiblePos);
out[0] = mOrientationHelper.getDecoratedStart(firstView)
- (firstVisiblePos - targetPos) * mItemDimension;
}
}
}
if (layoutManager.canScrollVertically()) {
if (targetPos <= firstVisiblePos) { // scrolling toward top of data
View firstView = layoutManager.findViewByPosition(firstVisiblePos);
out[1] = firstView.getTop() - (firstVisiblePos - targetPos) * mItemDimension;
}
}
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.
*/
int getPositionsToMove(LinearLayoutManager llm, int scroll, int itemSize) {
int positionsToMove;
positionsToMove = roundUpToBlockSize(Math.abs(scroll) / itemSize);
if (positionsToMove < mBlocksize) {
// Must move at least one block
positionsToMove = mBlocksize;
} else if (positionsToMove > mMaxPositionsToMove) {
// Clamp number of positions to move so we don't get wild flinging.
positionsToMove = mMaxPositionsToMove;
}
if (scroll < 0) {
positionsToMove *= -1;
}
if (mIsRTL) {
positionsToMove *= -1;
}
if (mLayoutDirectionHelper.isDirectionToBottom(scroll < 0)) {
// Scrolling toward the bottom of data.
return roundDownToBlockSize(llm.findFirstVisibleItemPosition()) + positionsToMove;
}
// Scrolling toward the top of the data.
return roundDownToBlockSize(llm.findLastVisibleItemPosition()) + positionsToMove;
}
boolean isDirectionToBottom(boolean velocityNegative) {
//noinspection SimplifiableConditionalExpression
return mIsRTL ? velocityNegative : !velocityNegative;
}
}
public interface SnapBlockCallback {
void onBlockSnap(int snapPosition);
void onBlockSnapped(int snapPosition);
}
private static final float MILLISECONDS_PER_INCH = 100f;
#SuppressWarnings("unused")
private static final String TAG = "SnapToBlock";
}
The SnapBlockCallback interface defined above can be used to report the adapter position of the view at the start of the block to be snapped. The view associated with that position may not be instantiated when the call is made if the view is off screen.
This library it is useful https://github.com/TakuSemba/MultiSnapRecyclerView
//Adding multisnap to the recyclerview
val multiSnapHelper = MultiSnapHelper(MultiSnapHelper.DEFAULT_GRAVITY, 1, 200F)
multiSnapHelper.attachToRecyclerView(recyclerView)
this code above is for your activity via code
<com.takusemba.multisnaprecyclerview.MultiSnapRecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:msrv_gravity="start" or center, end
app:msrv_interval="2" items to scroll over
app:msrv_ms_per_inch="100" /> // speed of scrolling through.
and this is the same way but in xml it is your choice
all this information its from the documentation
I would do something like that
Block scrolling inside RecyclerView (e.g How to disable RecyclerView scrolling?)
Create Gesture Fling Detecor and attach it to RecyclerView
Inside Gesture Detector detect fling events events
On Fling event, detect side (left right)
Scroll RecyclerView to position (first Visible item + your const * (left?-1:1))
should work :)

Categories

Resources