I have recyclerview with horizontal scroll and it snaps by default with LinearSnapHelper or PagerSnapHelper to the center of item. I want to snap to the left side of the each item. Is it possible?
You can easily extend PagerSnapHelper to align items by left side. There is one trick only needed with last item.
My solution is:
class AlignLeftPagerSnapHelper : PagerSnapHelper() {
private var horizontalHelper: OrientationHelper? = null
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
return getStartView(layoutManager as LinearLayoutManager, getHorizontalHelper(layoutManager))
}
private fun getStartView(layoutManager: LinearLayoutManager, helper: OrientationHelper): View? {
val firstVisibleChildPosition = layoutManager.findFirstVisibleItemPosition()
val lastCompletelyVisibleChildPosition = layoutManager.findLastCompletelyVisibleItemPosition()
val lastChildPosition = layoutManager.itemCount - 1
if (firstVisibleChildPosition != RecyclerView.NO_POSITION) {
var childView = layoutManager.findViewByPosition(firstVisibleChildPosition)
if (helper.getDecoratedEnd(childView) < helper.getDecoratedMeasurement(childView) / 2) {
childView = layoutManager.findViewByPosition(firstVisibleChildPosition + 1)
} else if (lastCompletelyVisibleChildPosition == lastChildPosition) {
childView = layoutManager.findViewByPosition(lastChildPosition)
}
return childView
}
return null
}
override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray =
intArrayOf(distanceToStart(targetView, getHorizontalHelper(layoutManager)), 0)
override fun findTargetSnapPosition(
layoutManager: RecyclerView.LayoutManager,
velocityX: Int,
velocityY: Int
): Int {
val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
val currentPosition = layoutManager.getPosition(currentView)
return if (velocityX < 0) {
(currentPosition - 1).coerceAtLeast(0)
} else {
(currentPosition + 1).coerceAtMost(layoutManager.itemCount - 1)
}
}
private fun distanceToStart(targetView: View, helper: OrientationHelper): Int =
helper.getDecoratedStart(targetView) - helper.startAfterPadding
private fun getHorizontalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
if (horizontalHelper == null) {
horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
}
return horizontalHelper!!
}
}
Related
I am unable to used pagination in HeterogeneousRecyclerview.it contents horizontal and vertical recycleview. I want to use pagination in VerticalRecycleview endless. When I try to do that it will not satisfy conditon.I will return visibleItemCount,totalItemCount and firstVisibleItemPosition that will not help to load more data. Please suggest me.
Recycle view:
val recyclerView = findViewById<RecyclerView>(R.id.rvMain)
val adapter = MainAdapter(this, getObject())
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
recycleViewScroll(recyclerView)
private fun getObject(): ArrayList<Any> {
jsonHelper?.readHorizontalItemsJSON()?.get(0)?.let { objects.add(it) }
jsonHelper?.readVerticalItemsJSON()?.get(0)?.let { objects.add(it) }
jsonHelper?.readHorizontalItemsJSON()?.get(0)?.let { objects.add(it) }
return objects as ArrayList<Any>
}
private fun recycleViewScroll(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(object :
RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = recyclerView.layoutManager!!.childCount
val totalItemCount = recyclerView.layoutManager!!.itemCount
val firstVisibleItemPosition =
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val lastpositionView = (recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
//val lastpositionView = (recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
println("dsfdsfdsfdfd----->" + visibleItemCount + "," + totalItemCount + "," + firstVisibleItemPosition!! +","+lastpositionView)
if (!isLoading && !isLastPage) {//
if (visibleItemCount + firstVisibleItemPosition!! >= totalItemCount && firstVisibleItemPosition!! >= 0 && totalItemCount >= PAGE_SIZE) {
loadMoreItems()
}
}
}
})
}
}`
Adapter:
class MainAdapter(private val context: Context, private val items: ArrayList<Any>?=null) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val jsonHelper = JsonHelper(context)
//this method returns the number according to the Vertical/Horizontal object
override
fun getItemViewType(position: Int): Int {
if (items?.get(position) is HorizontalModel)
return HORIZONTAL
return if (items?.get(position) is VerticalModel) VERTICAL else -1
}
override
fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(context)
val view: View
val holder: RecyclerView.ViewHolder
when (viewType) {
HORIZONTAL -> {
view = inflater.inflate(R.layout.layout_horizontal_recycler_view, parent, false)
holder = HorizontalViewHolder(view)
}
VERTICAL -> {
view = inflater.inflate(R.layout.layout_vertical_recycler_view, parent, false)
holder = VerticalViewHolder(view)
}
else -> {
view = inflater.inflate(R.layout.layout_horizontal_recycler_view, parent, false)
holder = HorizontalViewHolder(view)
}
}
return holder
}
override
fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder.itemViewType == HORIZONTAL)
horizontalView(holder as HorizontalViewHolder)
else if (holder.itemViewType == VERTICAL)
verticalView(holder as VerticalViewHolder)
}
private fun horizontalView(holder: HorizontalViewHolder) {
val adapter = HorizontalAdapter(context, jsonHelper.readHorizontalItemsJSON()!!)
holder.recyclerView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
holder.recyclerView.adapter = adapter
}
private fun verticalView(holder: VerticalViewHolder) {
val adapter = VerticalAdapter(context, jsonHelper.readVerticalItemsJSON()!!)
holder.recyclerView.layoutManager = LinearLayoutManager(context)
holder.recyclerView.adapter = adapter
}
override
fun getItemCount(): Int {
return items?.size ?:0
}
inner class HorizontalViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
internal var recyclerView: RecyclerView = itemView.findViewById(R.id.rvHorizontal)
}
inner class VerticalViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
internal var recyclerView: RecyclerView = itemView.findViewById(R.id.rvVertical)
}
}
I had a issue with PositionalDataSource, were loadRange not getting called once i invalidate the data source, this issue happens only if i add the item decoration for my recyclerview for sticky header
Without item decoration for recyclerview works fine
Here is my datasource
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<SomeData>) {
getData(0, params.requestedLoadSize)
.doOnSubscribe {
loading.set(true)
}
.map {
callback.onResult(it.data, 0)
}
.doOnComplete {
loading.set(false)
}
.subscribe()
.addTo(disposal)
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<SomeData>) {
getData(params.startPosition, params.loadSize)
.doOnSubscribe {
loading.set(true)
}
.map {
callback.onResult(it.data)
}
.doOnComplete {
loading.set(false)
}
.subscribe()
.addTo(disposal)
}
I invalidate the datasource from getData(int,int)
Here is my sticky header item decoration class
class StickyHeaderItemDecoration constructor(listener : StickyHeader) : RecyclerView.ItemDecoration() {
private val mListener: StickyHeader = listener
private var mStickyHeaderHeight = 0
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(canvas, parent, state)
val topChild = parent.getChildAt(0) ?: return
val topChildPosition = parent.getChildAdapterPosition(topChild)
if (topChildPosition == RecyclerView.NO_POSITION) {
return
}
val headerPos = mListener.getHeaderPositionFor(topChildPosition)
val currentHeader = getHeaderViewForItem(headerPos, parent)
fixLayoutSize(parent, currentHeader)
val contactPoint = currentHeader.bottom
val childInContact = getChildInContact(parent, contactPoint, headerPos)
if (childInContact != null && mListener.isHeaderView(
parent.getChildAdapterPosition(
childInContact
)
)
) {
moveHeader(canvas, currentHeader, childInContact)
return
}
drawHeader(canvas, currentHeader)
}
private fun getHeaderViewForItem(headerPosition: Int, parent: RecyclerView): View {
val layoutResId = mListener.getHeaderLayoutIdFor(headerPosition)
val header =
LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
// mListener.bindHeaderData(header, headerPosition)
return header
}
private fun drawHeader(c: Canvas, header: View) {
c.save()
c.translate(0F, 0F)
header.draw(c)
c.restore()
}
private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View) {
c.save()
c.translate(0F, (nextHeader.top - currentHeader.height)*1.0F)
currentHeader.draw(c)
c.restore()
}
private fun getChildInContact(
parent: RecyclerView,
contactPoint: Int,
currentHeaderPos: Int
): View? {
var childInContact: View? = null
for (i in 0 until parent.childCount) {
var heightTolerance = 0
val child = parent.getChildAt(i)
//measure height tolerance with child if child is another header
if (currentHeaderPos != i) {
val isChildHeader =
mListener.isHeaderView(parent.getChildAdapterPosition(child))
if (isChildHeader) {
heightTolerance = mStickyHeaderHeight - child.height
}
}
//add heightTolerance if child top be in display area
var childBottomPosition: Int
childBottomPosition = if (child.top > 0) {
child.bottom + heightTolerance
} else {
child.bottom
}
if (childBottomPosition > contactPoint) {
if (child.top <= contactPoint) { // This child overlaps the contactPoint
childInContact = child
break
}
}
}
return childInContact
}
/**
* Properly measures and layouts the top sticky header.
* #param parent ViewGroup: RecyclerView in this case.
*/
private fun fixLayoutSize(
parent: ViewGroup,
view: View
) { // Specs for parent (RecyclerView)
val widthSpec =
View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec =
View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
// Specs for children (headers)
val childWidthSpec = ViewGroup.getChildMeasureSpec(
widthSpec,
parent.paddingLeft + parent.paddingRight,
view.layoutParams.width
)
val childHeightSpec = ViewGroup.getChildMeasureSpec(
heightSpec,
parent.paddingTop + parent.paddingBottom,
view.layoutParams.height
)
view.measure(childWidthSpec, childHeightSpec)
view.layout(
0,
0,
view.measuredWidth,
view.measuredHeight.also { mStickyHeaderHeight = it }
)
}
}
Can anyone guess, what may be the issue ?
I am using Recyclerview with PageSnapHelper to create an Image carousel.
First item - Not Centered
The first Item is not centered and Subsequent Items should be centered, I have achieved this using item decorator. RecyclerView is inside nested scrollview.
Issue:
Scrolling is not smooth, I have override findTargetSnapPosition, It is scrolling 2 items for the first fling.
override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int): Int {
if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) {
return RecyclerView.NO_POSITION
}
val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
val layoutManager = layoutManager as LinearLayoutManager
val position1 = layoutManager.findFirstVisibleItemPosition()
val position2 = layoutManager.findLastVisibleItemPosition()
var currentPosition = layoutManager.getPosition(currentView)
if (velocityX > 500) {
currentPosition = position2
} else if (velocityX < 500) {
currentPosition = position1
}
return if (currentPosition == RecyclerView.NO_POSITION) {
RecyclerView.NO_POSITION
} else currentPosition
}
If i got you right, you need to override LinearSnapHelper instead, cause your item views are not full screened. For achieving focusing on first/last items you need to override findSnapView next way(note that this snippet only applicable when RecyclerView.layoutmanager is LinearLayoutManager):
fun RecyclerView.setLinearSnapHelper(isReversed: Boolean = false) {
object : LinearSnapHelper() {
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
val firstVisiblePosition = (layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
val lastVisiblePosition = layoutManager.findLastCompletelyVisibleItemPosition()
val firstItem = 0
val lastItem = layoutManager.itemCount - 1
return when {
firstItem == firstVisiblePosition -> layoutManager.findViewByPosition(firstVisiblePosition)
lastItem == lastVisiblePosition -> layoutManager.findViewByPosition(lastVisiblePosition)
else -> super.findSnapView(layoutManager)
}
}
}.apply { attachToRecyclerView(this#setLinearSnapHelper) }
}
I'd like to headers in a list using the android.arch.paging components.
Usually this would easy enough, just add different types for a RecyclerView adapter to handle and create a new list from items including headers.
But with the paging components I'm essentially handing the PagedList a PositionalDataSource from an SQL query. Is it possible to interrupt this and add header types?
How I ended up solving this for anyone interested.
when binding the data I pass in the previous item as well
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val previousItem = if (position == 0) null else getItem(position - 1)
holder.bind(item, previousItem)
}
Every view then sets a header, which is only made visible if the previous item doesn't have the same header.
val previousHeader = previousItem?.name?.capitalize().first()
val header = item?.name?.capitalize()?.first()
view.cachedContactHeader.text = header
view.cachedContactHeader.isVisible = previousHeader != header
Update 24/01/20
Since answering this I have changed to using a custom ItemDecoration
class StickyHeaderDividerItem(
context: Context,
private val stickyHeaderCallbacks: StickyHeaderCallbacks
) : ItemDecoration() {
private val headerHeight = context.resources.getDimensionPixelOffset(R.dimen.sticky_header_height)
private var headerView: View? = null
private var headerTextView: TextView? = null
private var headerEndTextView: TextView? = null
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
if (position >= 0 && stickyHeaderCallbacks.isHeader(position)) {
outRect.top = headerHeight
}
}
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: State) {
super.onDrawOver(canvas, parent, state)
if (headerView == null) {
headerView = (parent.inflate(R.layout.list_item_sticky_header)).apply {
fixLayoutSize(this, parent)
}
headerTextView = headerView?.headerText
headerEndTextView = headerView?.headerEndText
}
var previousHeader = ""
parent.children.toList().forEach { child ->
val position = parent.getChildAdapterPosition(child)
val headerTitle = position.takeIf { it >= 0 }?.let(stickyHeaderCallbacks::headerTitle).orEmpty()
val headerEndTitle = position.takeIf { it >= 0 }?.let(stickyHeaderCallbacks::headerTitleEnd)
headerTextView?.text = headerTitle
headerEndTextView?.text = headerEndTitle
if ((previousHeader != headerTitle) || (position >= 0 && stickyHeaderCallbacks.isHeader(position))) {
headerView?.let { drawHeader(canvas, child, it) }
previousHeader = headerTitle
}
}
}
private fun drawHeader(canvas: Canvas, child: View, headerView: View) {
canvas.run {
save()
translate(0F, maxOf(0, child.top - headerView.height).toFloat())
headerView.draw(this)
restore()
}
}
private fun fixLayoutSize(view: View, parent: ViewGroup) {
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingStart + parent.paddingEnd, view.layoutParams.width)
val childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.layoutParams.height)
view.measure(childWidth, childHeight)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
}
interface StickyHeaderCallbacks {
fun isHeader(itemPosition: Int): Boolean
fun headerTitle(itemPosition: Int): String
fun headerTitleEnd(itemPosition: Int): String? = null
}
Background
I'm try to achieve something similar to what the Camera app has for its modes:
I might probably not need to have a ViewPager, as seems that it uses above the horizontal list, but could be nice to have it as an option.
The problem
While technically I succeeded to have the RecyclerView center its items, it doesn't let you actually have the all items to be able to be on the center (example is the first/last one). When you try to scroll to the first or last items, it doesn't let you , because you've reached the edge of the RecyclerView :
Not only that, but in the beginning it doesn't really center, and if I have the RecyclerView have few items, it becomes a problem because I want them to be centered, but having android:layout_width="match_parent" (because all of it should be touch-able) produces this:
while having android:layout_width="wrap_content" get me this:
On both cases, I can't scroll at all. When it's "wrap_content", it's a problem because I won't be able to scroll on the sides.
What I've tried
It's possible to snap the items so that there will always be an item in the center of RecyclerView as such :
val snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(categoriesRecyclerView)
We can also get which is the item in the center (as shown here), by having a scroll listener and using snapHelper.findSnapView(layoutManagaer).
But as I wrote, I can't really have the first/last item being selected this way, because I can't scroll to it so that it will be at the middle.
I tried to look at the docs of the related classes, but I can't find such a thing.
Here's the current code (sample available here) :
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val holder = object : RecyclerView.ViewHolder(
LayoutInflater.from(this#MainActivity).inflate(
R.layout.list_item,
parent,
false
)
) {}
holder.itemView.setOnClickListener {
}
return holder
}
override fun getItemCount(): Int = 20
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.textView.text = "pos:$position"
}
}
val snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(recyclerView)
}
}
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView android:background="#66000000"
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="#dimen/list_item_size"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="#layout/list_item"/>
</androidx.constraintlayout.widget.ConstraintLayout>
list_item.xml
<TextView
android:id="#+id/textView" 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="wrap_content" android:layout_height="#dimen/list_item_size"
android:background="?attr/selectableItemBackground" android:breakStrategy="balanced" android:clickable="true"
android:focusable="true" android:gravity="center" android:maxLines="1" android:padding="8dp"
android:shadowColor="#222" android:shadowDx="1" android:shadowDy="1" android:textColor="#fff"
app:autoSizeTextType="uniform" tools:targetApi="m" tools:text="#tools:sample/lorem"/>
The question
How can I let the user freely scroll inside, so that the edge will be determined by whether the first/last item is in the middle? How can I always have the items centered, including when I just started seeing the RecyclerView), and including when there are few of them?
I gave this a try
5 items: https://drive.google.com/open?id=1RPyiY9UndXcrbfBDWLB-UklxjPKMiR8-
2 items: https://drive.google.com/open?id=1HkG8NShxQ3illFupK-urSPwsUhag74WS
First, apply an item decoration to center the first and last items:
class CenterDecoration(#Px private val spacing: Int) : RecyclerView.ItemDecoration() {
private var firstViewWidth = -1
private var lastViewWidth = -1
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val adapterPosition = (view.layoutParams as RecyclerView.LayoutParams).viewAdapterPosition
val lm = parent.layoutManager as LinearLayoutManager
if (adapterPosition == 0) {
// Invalidate decorations when this view width has changed
if (view.width != firstViewWidth) {
view.doOnPreDraw { parent.invalidateItemDecorations() }
}
firstViewWidth = view.width
outRect.left = parent.width / 2 - view.width / 2
// If we have more items, use the spacing provided
if (lm.itemCount > 1) {
outRect.right = spacing / 2
} else {
// Otherwise, make sure this to fill the whole width with the decoration
outRect.right = outRect.left
}
} else if (adapterPosition == lm.itemCount - 1) {
// Invalidate decorations when this view width has changed
if (view.width != lastViewWidth) {
view.doOnPreDraw { parent.invalidateItemDecorations() }
}
lastViewWidth = view.width
outRect.right = parent.width / 2 - view.width / 2
outRect.left = spacing / 2
} else {
outRect.left = spacing / 2
outRect.right = spacing / 2
}
}
}
Now, LinearSnapHelper determines the center of a view and includes its decorations. You can create a custom one that excludes the decorations from the calculation to center the view only:
/**
* A LinearSnapHelper that ignores item decorations to determine a view's center
*/
class CenterSnapHelper : LinearSnapHelper() {
private var verticalHelper: OrientationHelper? = null
private var horizontalHelper: OrientationHelper? = null
private var scrolled = false
private var recyclerView: RecyclerView? = null
private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE && scrolled) {
if (recyclerView.layoutManager != null) {
val view = findSnapView(recyclerView.layoutManager)
if (view != null) {
val out = calculateDistanceToFinalSnap(recyclerView.layoutManager!!, view)
if (out != null) {
recyclerView.smoothScrollBy(out[0], out[1])
}
}
}
scrolled = false
} else {
scrolled = true
}
}
}
fun scrollTo(position: Int, smooth: Boolean) {
if (recyclerView?.layoutManager != null) {
val viewHolder = recyclerView!!.findViewHolderForAdapterPosition(position)
if (viewHolder != null) {
val distances = calculateDistanceToFinalSnap(recyclerView!!.layoutManager!!, viewHolder.itemView)
if (smooth) {
recyclerView!!.smoothScrollBy(distances!![0], distances[1])
} else {
recyclerView!!.scrollBy(distances!![0], distances[1])
}
} else {
if (smooth) {
recyclerView!!.smoothScrollToPosition(position)
} else {
recyclerView!!.scrollToPosition(position)
}
}
}
}
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
if (layoutManager == null) {
return null
}
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager))
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
}
return null
}
override fun attachToRecyclerView(recyclerView: RecyclerView?) {
this.recyclerView = recyclerView
recyclerView?.addOnScrollListener(scrollListener)
}
override fun calculateDistanceToFinalSnap(
layoutManager: RecyclerView.LayoutManager,
targetView: View
): IntArray? {
val out = IntArray(2)
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToCenter(layoutManager, targetView, getHorizontalHelper(layoutManager))
} else {
out[0] = 0
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToCenter(layoutManager, targetView, getVerticalHelper(layoutManager))
} else {
out[1] = 0
}
return out
}
private fun findCenterView(
layoutManager: RecyclerView.LayoutManager,
helper: OrientationHelper
): View? {
val childCount = layoutManager.childCount
if (childCount == 0) {
return null
}
var closestChild: View? = null
val center: Int = if (layoutManager.clipToPadding) {
helper.startAfterPadding + helper.totalSpace / 2
} else {
helper.end / 2
}
var absClosest = Integer.MAX_VALUE
for (i in 0 until childCount) {
val child = layoutManager.getChildAt(i)
val childCenter = if (helper == horizontalHelper) {
(child!!.x + child.width / 2).toInt()
} else {
(child!!.y + child.height / 2).toInt()
}
val absDistance = Math.abs(childCenter - center)
if (absDistance < absClosest) {
absClosest = absDistance
closestChild = child
}
}
return closestChild
}
private fun distanceToCenter(
layoutManager: RecyclerView.LayoutManager,
targetView: View,
helper: OrientationHelper
): Int {
val childCenter = if (helper == horizontalHelper) {
(targetView.x + targetView.width / 2).toInt()
} else {
(targetView.y + targetView.height / 2).toInt()
}
val containerCenter = if (layoutManager.clipToPadding) {
helper.startAfterPadding + helper.totalSpace / 2
} else {
helper.end / 2
}
return childCenter - containerCenter
}
private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
if (verticalHelper == null || verticalHelper!!.layoutManager !== layoutManager) {
verticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
}
return verticalHelper!!
}
private fun getHorizontalHelper(
layoutManager: RecyclerView.LayoutManager
): OrientationHelper {
if (horizontalHelper == null || horizontalHelper!!.layoutManager !== layoutManager) {
horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
}
return horizontalHelper!!
}
}
Usage:
class MainActivity : AppCompatActivity() {
private val snapHelper = CenterSnapHelper()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView.addItemDecoration(CenterDecoration(0))
snapHelper.attachToRecyclerView(recyclerView)
recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val holder = object : RecyclerView.ViewHolder(
LayoutInflater.from(this#MainActivity).inflate(
R.layout.list_item,
parent,
false
)
) {}
holder.itemView.setOnClickListener {
if (holder.adapterPosition != RecyclerView.NO_POSITION) {
snapHelper.scrollTo(holder.adapterPosition, true)
}
}
return holder
}
override fun getItemCount(): Int = 20
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.textView.text = "pos:$position"
}
}
}
}
Posting XML here in case someone wants to check this out:
MainActivity
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="4dp"
android:layout_height="0dp"
android:background="#color/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
list_item.xml
<TextView
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:id="#+id/textView"
android:layout_width="wrap_content" android:layout_height="#dimen/list_item_size"
android:background="?attr/selectableItemBackground" android:clickable="true"
android:focusable="true" android:gravity="center" android:maxLines="1" android:padding="8dp"
android:shadowColor="#222" android:shadowDx="1" android:shadowDy="1" android:textColor="#fff"
tools:targetApi="m" tools:text="#tools:sample/lorem"/>
EDIT: here's a sample of how to use this:
http://s000.tinyupload.com/?file_id=01184747175525079378
This is how i solved it. The problem on custom snaphelper and decorators is that they dont work with other libaries and custom Views. It also works with items with variable widths.
If you want to snap the items, just use the classic snaphelper on the recyclerview
public class CenterRecyclerView extends RecyclerView {
public CenterRecyclerView(#NonNull Context context) {
super(context);
}
public CenterRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public CenterRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void updatePadding() {
post(() -> {
final DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
final int screenWidth = displayMetrics.widthPixels;
final int screenHeight = displayMetrics.heightPixels;
ViewHolder firstViewHolder = findViewHolderForAdapterPosition(0);
if (firstViewHolder != null) {
firstViewHolder.itemView.measure(WRAP_CONTENT, WRAP_CONTENT);
int viewWidth = firstViewHolder.itemView.getMeasuredWidth();
int padding;
if (screenHeight > screenWidth) {
//Portrait
padding = screenWidth / 2 - viewWidth / 2;
} else {
//Landscape
padding = screenHeight / 2 - viewWidth / 2;
}
setPadding(padding, 0, padding, 0);
} else {
Log.e("CenterRecyclerView", "Could not get first ViewHolder");
}
});
}
#Override
protected void onFinishInflate() {
super.onFinishInflate();
updatePadding();
}
#Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updatePadding();
}
}