RecyclerView SnapHelper fails to show first/last items - android

I have a RecyclerView which is attached to a LinearSnapHelper to snap to center item. When I scroll to the first or last items, these items are not fully visible anymore. This problem is shown in the following image. How to solve it?

This issue happens when center of item which is next to the first/last is closer to the center of container. So, we should make some changes on snapping functionality to ignore this case. Since we need some fields in LinearSnapHelper class, we can copy its source code and make change on findCenterView method as following:
MyLinearSnapHelper.kt
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.aminography.view.component
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.OrientationHelper
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.SnapHelper
import android.view.View
/**
* Implementation of the [SnapHelper] supporting snapping in either vertical or horizontal
* orientation.
*
*
* The implementation will snap the center of the target child view to the center of
* the attached [RecyclerView]. If you intend to change this behavior then override
* [SnapHelper.calculateDistanceToFinalSnap].
*/
class MyLinearSnapHelper : SnapHelper() {
// Orientation helpers are lazily created per LayoutManager.
private var mVerticalHelper: OrientationHelper? = null
private var mHorizontalHelper: OrientationHelper? = null
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
}
override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int,
velocityY: Int): Int {
if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) {
return RecyclerView.NO_POSITION
}
val itemCount = layoutManager.itemCount
if (itemCount == 0) {
return RecyclerView.NO_POSITION
}
val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
val currentPosition = layoutManager.getPosition(currentView)
if (currentPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION
}
val vectorProvider = layoutManager as RecyclerView.SmoothScroller.ScrollVectorProvider
// deltaJumps sign comes from the velocity which may not match the order of children in
// the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
// get the direction.
val vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1)
?: // cannot get a vector for the given position.
return RecyclerView.NO_POSITION
var vDeltaJump: Int
var hDeltaJump: Int
if (layoutManager.canScrollHorizontally()) {
hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getHorizontalHelper(layoutManager), velocityX, 0)
if (vectorForEnd.x < 0) {
hDeltaJump = -hDeltaJump
}
} else {
hDeltaJump = 0
}
if (layoutManager.canScrollVertically()) {
vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getVerticalHelper(layoutManager), 0, velocityY)
if (vectorForEnd.y < 0) {
vDeltaJump = -vDeltaJump
}
} else {
vDeltaJump = 0
}
val deltaJump = if (layoutManager.canScrollVertically()) vDeltaJump else hDeltaJump
if (deltaJump == 0) {
return RecyclerView.NO_POSITION
}
var targetPos = currentPosition + deltaJump
if (targetPos < 0) {
targetPos = 0
}
if (targetPos >= itemCount) {
targetPos = itemCount - 1
}
return targetPos
}
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager))
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
}
return null
}
private fun distanceToCenter(layoutManager: RecyclerView.LayoutManager,
targetView: View, helper: OrientationHelper): Int {
val childCenter = helper.getDecoratedStart(targetView) + helper.getDecoratedMeasurement(targetView) / 2
val containerCenter: Int = if (layoutManager.clipToPadding) {
helper.startAfterPadding + helper.totalSpace / 2
} else {
helper.end / 2
}
return childCenter - containerCenter
}
/**
* Estimates a position to which SnapHelper will try to scroll to in response to a fling.
*
* #param layoutManager The [RecyclerView.LayoutManager] associated with the attached
* [RecyclerView].
* #param helper The [OrientationHelper] that is created from the LayoutManager.
* #param velocityX The velocity on the x axis.
* #param velocityY The velocity on the y axis.
*
* #return The diff between the target scroll position and the current position.
*/
private fun estimateNextPositionDiffForFling(layoutManager: RecyclerView.LayoutManager,
helper: OrientationHelper, velocityX: Int, velocityY: Int): Int {
val distances = calculateScrollDistance(velocityX, velocityY)
val distancePerChild = computeDistancePerChild(layoutManager, helper)
if (distancePerChild <= 0) {
return 0
}
val distance = if (Math.abs(distances[0]) > Math.abs(distances[1])) distances[0] else distances[1]
return Math.round(distance / distancePerChild)
}
/**
* Return the child view that is currently closest to the center of this parent.
*
* #param layoutManager The [RecyclerView.LayoutManager] associated with the attached
* [RecyclerView].
* #param helper The relevant [OrientationHelper] for the attached [RecyclerView].
*
* #return the child view that is currently closest to the center of this parent.
*/
private fun findCenterView(layoutManager: RecyclerView.LayoutManager,
helper: OrientationHelper): View? {
// ----- Added by aminography
if (layoutManager is LinearLayoutManager) {
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
return layoutManager.getChildAt(0)
} else if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.itemCount - 1) {
return layoutManager.getChildAt(layoutManager.itemCount - 1)
}
}
// -----
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 = helper.getDecoratedStart(child) + helper.getDecoratedMeasurement(child) / 2
val absDistance = Math.abs(childCenter - center)
/** if child center is closer than previous closest, set it as closest */
if (absDistance < absClosest) {
absClosest = absDistance
closestChild = child
}
}
return closestChild
}
/**
* Computes an average pixel value to pass a single child.
*
*
* Returns a negative value if it cannot be calculated.
*
* #param layoutManager The [RecyclerView.LayoutManager] associated with the attached
* [RecyclerView].
* #param helper The relevant [OrientationHelper] for the attached
* [RecyclerView.LayoutManager].
*
* #return A float value that is the average number of pixels needed to scroll by one view in
* the relevant direction.
*/
private fun computeDistancePerChild(layoutManager: RecyclerView.LayoutManager,
helper: OrientationHelper): Float {
var minPosView: View? = null
var maxPosView: View? = null
var minPos = Integer.MAX_VALUE
var maxPos = Integer.MIN_VALUE
val childCount = layoutManager.childCount
if (childCount == 0) {
return INVALID_DISTANCE
}
for (i in 0 until childCount) {
val child = layoutManager.getChildAt(i)
val pos = layoutManager.getPosition(child!!)
if (pos == RecyclerView.NO_POSITION) {
continue
}
if (pos < minPos) {
minPos = pos
minPosView = child
}
if (pos > maxPos) {
maxPos = pos
maxPosView = child
}
}
if (minPosView == null || maxPosView == null) {
return INVALID_DISTANCE
}
val start = Math.min(helper.getDecoratedStart(minPosView),
helper.getDecoratedStart(maxPosView))
val end = Math.max(helper.getDecoratedEnd(minPosView),
helper.getDecoratedEnd(maxPosView))
val distance = end - start
return if (distance == 0) {
INVALID_DISTANCE
} else 1f * distance / (maxPos - minPos + 1)
}
private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
if (mVerticalHelper == null || mVerticalHelper!!.layoutManager !== layoutManager) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
}
return mVerticalHelper!!
}
private fun getHorizontalHelper(
layoutManager: RecyclerView.LayoutManager): OrientationHelper {
if (mHorizontalHelper == null || mHorizontalHelper!!.layoutManager !== layoutManager) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
}
return mHorizontalHelper!!
}
companion object {
private const val INVALID_DISTANCE = 1f
}
}

I know I am late but I want to suggest an simple solution written in Java code:
Create CustomSnapHelper class:
public class CustomSnapHelper extends LinearSnapHelper {
#Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if(layoutManager instanceof LinearLayoutManager){
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
if(needToDoSnap(linearLayoutManager)==false){
return null;
}
}
return super.findSnapView(layoutManager);
}
public boolean needToDoSnap(LinearLayoutManager linearLayoutManager){
return linearLayoutManager.findFirstCompletelyVisibleItemPosition()!=0&&linearLayoutManager.findLastCompletelyVisibleItemPosition()!=linearLayoutManager.getItemCount()-1;
}
}
Attach an object of CustomSnapHelper to the recycler view:
CustomSnapHelper mSnapHelper = new CustomSnapHelper();
mSnapHelper.attachToRecyclerView(mRecyclerView);

I tried to implement a simple solution. Basically I checked if the first/last items are completely visible. If so, we don't need to perform the snap. See the solution below:
class CarouselSnapHelper : LinearSnapHelper() {
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
val linearLayoutManager = layoutManager as? LinearLayoutManager
?: return super.findSnapView(layoutManager)
return linearLayoutManager
.takeIf { isValidSnap(it) }
?.run { super.findSnapView(layoutManager) }
}
private fun isValidSnap(linearLayoutManager: LinearLayoutManager) =
linearLayoutManager.findFirstCompletelyVisibleItemPosition() != 0 &&
linearLayoutManager.findLastCompletelyVisibleItemPosition() != linearLayoutManager.itemCount - 1
}

I found a less invasive answer:
private class PagerSelectSnapHelper : LinearSnapHelper() {
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
// Use existing LinearSnapHelper but override when the itemDecoration calculations are off
val snapView = super.findSnapView(layoutManager)
return if (!snapView.isViewInCenterOfParent(layoutManager.width)) {
val endView = layoutManager.findViewByPosition(layoutManager.itemCount - 1)
val startView = layoutManager.findViewByPosition(0)
when {
endView.isViewInCenterOfParent(layoutManager.width) -> endView
startView.isViewInCenterOfParent(layoutManager.width) -> startView
else -> snapView
}
} else {
snapView
}
}
private fun View?.isViewInCenterOfParent(parentWidth: Int): Boolean {
if (this == null || width == 0) {
return false
}
val parentCenter = parentWidth / 2
return left < parentCenter && parentCenter < right
}
}

Related

Add animation to custom SnapHelper in recycler view when items are scrolled

I'm using custom SnapHelper which extends LinearSnapHelper to automatically scroll in recycler view.Code can be found below. The scrolling however is way too fast and i want to add a slide or fade animation. Please guideLink to scrolling effect in which animation needs to be added
Auto scroll code(Fragment1.kt)-
private fun autoScrollRecyclerView() {
val snapHelper = GravitySnapHelper(Gravity.START)
snapHelper.attachToRecyclerView(binding.rcvSuccessMetric)
val timer = Timer()
timer.schedule(object : TimerTask() {
override fun run() {
if (layoutManager.findLastCompletelyVisibleItemPosition() < (adapter.itemCount) - 1)
layoutManager.smoothScrollToPosition(
binding.rcvSuccessMetric,
RecyclerView.State(),
layoutManager.findLastCompletelyVisibleItemPosition() + 2
)
else
layoutManager.smoothScrollToPosition(
binding.rcvSuccessMetric,
RecyclerView.State(),
0
)
}
}, 0, 6000)
}
GravitySnapHelper.java (Custom class)-
public class GravitySnapHelper extends LinearSnapHelper {
public static final int FLING_DISTANCE_DISABLE = -1;
public static final float FLING_SIZE_FRACTION_DISABLE = -1f;
private int gravity;
private boolean isRtl;
private boolean snapLastItem;
private int nextSnapPosition;
private boolean isScrolling = false;
private boolean snapToPadding = false;
private float scrollMsPerInch = 100f;
private int maxFlingDistance = FLING_DISTANCE_DISABLE;
private float maxFlingSizeFraction = FLING_SIZE_FRACTION_DISABLE;
private OrientationHelper verticalHelper;
private OrientationHelper horizontalHelper;
private GravitySnapHelper.SnapListener listener;
private RecyclerView recyclerView;
private RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
GravitySnapHelper.this.onScrollStateChanged(newState);
}
};
public GravitySnapHelper(int gravity) {
this(gravity, false, null);
}
public GravitySnapHelper(int gravity, #NonNull SnapListener snapListener) {
this(gravity, false, snapListener);
}
public GravitySnapHelper(int gravity, boolean enableSnapLastItem) {
this(gravity, enableSnapLastItem, null);
}
public GravitySnapHelper(int gravity, boolean enableSnapLastItem,
#Nullable SnapListener snapListener) {
if (gravity != Gravity.START
&& gravity != Gravity.END
&& gravity != Gravity.BOTTOM
&& gravity != Gravity.TOP
&& gravity != Gravity.CENTER) {
throw new IllegalArgumentException("Invalid gravity value. Use START " +
"| END | BOTTOM | TOP | CENTER constants");
}
this.snapLastItem = enableSnapLastItem;
this.gravity = gravity;
this.listener = snapListener;
}
#Override
public void attachToRecyclerView(#Nullable RecyclerView recyclerView)
throws IllegalStateException {
if (this.recyclerView != null) {
this.recyclerView.removeOnScrollListener(scrollListener);
}
if (recyclerView != null) {
recyclerView.setOnFlingListener(null);
if (gravity == Gravity.START || gravity == Gravity.END) {
isRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault())
== ViewCompat.LAYOUT_DIRECTION_RTL;
}
recyclerView.addOnScrollListener(scrollListener);
this.recyclerView = recyclerView;
} else {
this.recyclerView = null;
}
super.attachToRecyclerView(recyclerView);
}
#Override
#Nullable
public View findSnapView(#NonNull RecyclerView.LayoutManager lm) {
return findSnapView(lm, true);
}
#Nullable
public View findSnapView(#NonNull RecyclerView.LayoutManager lm, boolean checkEdgeOfList) {
View snapView = null;
switch (gravity) {
case Gravity.START:
snapView = findView(lm, getHorizontalHelper(lm), Gravity.START, checkEdgeOfList);
break;
case Gravity.END:
snapView = findView(lm, getHorizontalHelper(lm), Gravity.END, checkEdgeOfList);
break;
case Gravity.TOP:
snapView = findView(lm, getVerticalHelper(lm), Gravity.START, checkEdgeOfList);
break;
case Gravity.BOTTOM:
snapView = findView(lm, getVerticalHelper(lm), Gravity.END, checkEdgeOfList);
break;
case Gravity.CENTER:
if (lm.canScrollHorizontally()) {
snapView = findView(lm, getHorizontalHelper(lm), Gravity.CENTER,
checkEdgeOfList);
} else {
snapView = findView(lm, getVerticalHelper(lm), Gravity.CENTER,
checkEdgeOfList);
}
break;
}
if (snapView != null) {
nextSnapPosition = recyclerView.getChildAdapterPosition(snapView);
} else {
nextSnapPosition = RecyclerView.NO_POSITION;
}
return snapView;
}
#Override
#NonNull
public int[] calculateDistanceToFinalSnap(#NonNull RecyclerView.LayoutManager layoutManager,
#NonNull View targetView) {
if (gravity == Gravity.CENTER) {
//noinspection ConstantConditions
return super.calculateDistanceToFinalSnap(layoutManager, targetView);
}
int[] out = new int[2];
if (!(layoutManager instanceof LinearLayoutManager)) {
return out;
}
LinearLayoutManager lm = (LinearLayoutManager) layoutManager;
if (lm.canScrollHorizontally()) {
if ((isRtl && gravity == Gravity.END) || (!isRtl && gravity == Gravity.START)) {
out[0] = getDistanceToStart(targetView, getHorizontalHelper(lm));
} else {
out[0] = getDistanceToEnd(targetView, getHorizontalHelper(lm));
}
} else if (lm.canScrollVertically()) {
if (gravity == Gravity.TOP) {
out[1] = getDistanceToStart(targetView, getVerticalHelper(lm));
} else {
out[1] = getDistanceToEnd(targetView, getVerticalHelper(lm));
}
}
return out;
}
#Override
#NonNull
public int[] calculateScrollDistance(int velocityX, int velocityY) {
if (recyclerView == null
|| (verticalHelper == null && horizontalHelper == null)
|| (maxFlingDistance == FLING_DISTANCE_DISABLE
&& maxFlingSizeFraction == FLING_SIZE_FRACTION_DISABLE)) {
return super.calculateScrollDistance(velocityX, velocityY);
}
final int[] out = new int[2];
Scroller scroller = new Scroller(recyclerView.getContext(),
new DecelerateInterpolator());
int maxDistance = getFlingDistance();
scroller.fling(0, 0, velocityX, velocityY,
-maxDistance, maxDistance,
-maxDistance, maxDistance);
out[0] = scroller.getFinalX();
out[1] = scroller.getFinalY();
return out;
}
#Nullable
#Override
public RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)
|| recyclerView == null) {
return null;
}
return new LinearSmoothScroller(recyclerView.getContext()) {
#Override
protected void onTargetFound(View targetView,
RecyclerView.State state,
RecyclerView.SmoothScroller.Action action) {
if (recyclerView == null || recyclerView.getLayoutManager() == null) {
// The associated RecyclerView has been removed so there is no action to take.
return;
}
int[] snapDistances = calculateDistanceToFinalSnap(recyclerView.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, mDecelerateInterpolator);
}
}
#Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return scrollMsPerInch / displayMetrics.densityDpi;
}
};
}
/**
* Sets a {#link SnapListener} to listen for snap events
*
* #param listener a {#link SnapListener} that'll receive snap events or null to clear it
*/
public void setSnapListener(#Nullable SnapListener listener) {
this.listener = listener;
}
/**
* Changes the gravity of this {#link GravitySnapHelper}
* and dispatches a smooth scroll for the new snap position.
*
* #param newGravity one of the following: {#link Gravity#START}, {#link Gravity#TOP},
* {#link Gravity#END}, {#link Gravity#BOTTOM}, {#link Gravity#CENTER}
* #param smooth true if we should smooth scroll to new edge, false otherwise
*/
public void setGravity(int newGravity, Boolean smooth) {
if (this.gravity != newGravity) {
this.gravity = newGravity;
updateSnap(smooth, false);
}
}
/**
* Updates the current view to be snapped
*
* #param smooth true if we should smooth scroll, false otherwise
* #param checkEdgeOfList true if we should check if we're at an edge of the list
* and snap according to {#link GravitySnapHelper#getSnapLastItem()},
* or false to force snapping to the nearest view
*/
public void updateSnap(Boolean smooth, Boolean checkEdgeOfList) {
if (recyclerView == null || recyclerView.getLayoutManager() == null) {
return;
}
final RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
View snapView = findSnapView(lm, checkEdgeOfList);
if (snapView != null) {
int[] out = calculateDistanceToFinalSnap(lm, snapView);
if (smooth) {
recyclerView.smoothScrollBy(out[0], out[1]);
} else {
recyclerView.scrollBy(out[0], out[1]);
}
}
}
/**
* This method will only work if there's a ViewHolder for the given position.
*
* #return true if scroll was successful, false otherwise
*/
public boolean scrollToPosition(int position) {
if (position == RecyclerView.NO_POSITION) {
return false;
}
return scrollTo(position, false);
}
/**
* Unlike {#link GravitySnapHelper#scrollToPosition(int)},
* this method will generally always find a snap view if the position is valid.
* <p>
* The smooth scroller from {#link GravitySnapHelper#createScroller(RecyclerView.LayoutManager)}
* will be used, and so will {#link GravitySnapHelper#scrollMsPerInch} for the scroll velocity
*
* #return true if scroll was successful, false otherwise
*/
public boolean smoothScrollToPosition(int position) {
if (position == RecyclerView.NO_POSITION) {
return false;
}
return scrollTo(position, true);
}
/**
* Get the current gravity being applied
*
* #return one of the following: {#link Gravity#START}, {#link Gravity#TOP}, {#link Gravity#END},
* {#link Gravity#BOTTOM}, {#link Gravity#CENTER}
*/
public int getGravity() {
return this.gravity;
}
/**
* Changes the gravity of this {#link GravitySnapHelper}
* and dispatches a smooth scroll for the new snap position.
*
* #param newGravity one of the following: {#link Gravity#START}, {#link Gravity#TOP},
* {#link Gravity#END}, {#link Gravity#BOTTOM}, {#link Gravity#CENTER}
*/
public void setGravity(int newGravity) {
setGravity(newGravity, true);
}
/**
* #return true if this SnapHelper should snap to the last item
*/
public boolean getSnapLastItem() {
return snapLastItem;
}
/**
* Enable snapping of the last item that's snappable.
* The default value is false, because you can't see the last item completely
* if this is enabled.
*
* #param snap true if you want to enable snapping of the last snappable item
*/
public void setSnapLastItem(boolean snap) {
snapLastItem = snap;
}
/**
* #return last distance set through {#link GravitySnapHelper#setMaxFlingDistance(int)}
* or {#link GravitySnapHelper#FLING_DISTANCE_DISABLE} if we're not limiting the fling distance
*/
public int getMaxFlingDistance() {
return maxFlingDistance;
}
/**
* Changes the max fling distance in absolute values.
*
* #param distance max fling distance in pixels
* or {#link GravitySnapHelper#FLING_DISTANCE_DISABLE}
* to disable fling limits
*/
public void setMaxFlingDistance(#Px int distance) {
maxFlingDistance = distance;
maxFlingSizeFraction = FLING_SIZE_FRACTION_DISABLE;
}
/**
* #return last distance set through {#link GravitySnapHelper#setMaxFlingSizeFraction(float)}
* or {#link GravitySnapHelper#FLING_SIZE_FRACTION_DISABLE}
* if we're not limiting the fling distance
*/
public float getMaxFlingSizeFraction() {
return maxFlingSizeFraction;
}
/**
* Changes the max fling distance depending on the available size of the RecyclerView.
* <p>
* Example: if you pass 0.5f and the RecyclerView measures 600dp,
* the max fling distance will be 300dp.
*
* #param fraction size fraction to be used for the max fling distance
* or {#link GravitySnapHelper#FLING_SIZE_FRACTION_DISABLE}
* to disable fling limits
*/
public void setMaxFlingSizeFraction(float fraction) {
maxFlingDistance = FLING_DISTANCE_DISABLE;
maxFlingSizeFraction = fraction;
}
/**
* #return last scroll speed set through {#link GravitySnapHelper#setScrollMsPerInch(float)}
* or 100f
*/
public float getScrollMsPerInch() {
return scrollMsPerInch;
}
/**
* Sets the scroll duration in ms per inch.
* <p>
* Default value is 100.0f
* <p>
* This value will be used in
* {#link GravitySnapHelper#createScroller(RecyclerView.LayoutManager)}
*
* #param ms scroll duration in ms per inch
*/
public void setScrollMsPerInch(float ms) {
scrollMsPerInch = ms;
}
/**
* #return true if this SnapHelper should snap to the padding. Defaults to false.
*/
public boolean getSnapToPadding() {
return snapToPadding;
}
/**
* If true, GravitySnapHelper will snap to the gravity edge
* plus any amount of padding that was set in the RecyclerView.
* <p>
* The default value is false.
*
* #param snapToPadding true if you want to snap to the padding
*/
public void setSnapToPadding(boolean snapToPadding) {
this.snapToPadding = snapToPadding;
}
/**
* #return the position of the current view that's snapped
* or {#link RecyclerView#NO_POSITION} in case there's none.
*/
public int getCurrentSnappedPosition() {
if (recyclerView != null && recyclerView.getLayoutManager() != null) {
View snappedView = findSnapView(recyclerView.getLayoutManager());
if (snappedView != null) {
return recyclerView.getChildAdapterPosition(snappedView);
}
}
return RecyclerView.NO_POSITION;
}
private int getFlingDistance() {
if (maxFlingSizeFraction != FLING_SIZE_FRACTION_DISABLE) {
if (verticalHelper != null) {
return (int) (recyclerView.getHeight() * maxFlingSizeFraction);
} else if (horizontalHelper != null) {
return (int) (recyclerView.getWidth() * maxFlingSizeFraction);
} else {
return Integer.MAX_VALUE;
}
} else if (maxFlingDistance != FLING_DISTANCE_DISABLE) {
return maxFlingDistance;
} else {
return Integer.MAX_VALUE;
}
}
/**
* #return true if the scroll will snap to a view, false otherwise
*/
private boolean scrollTo(int position, boolean smooth) {
if (recyclerView.getLayoutManager() != null) {
if (smooth) {
RecyclerView.SmoothScroller smoothScroller
= createScroller(recyclerView.getLayoutManager());
if (smoothScroller != null) {
smoothScroller.setTargetPosition(position);
recyclerView.getLayoutManager().startSmoothScroll(smoothScroller);
return true;
}
} else {
RecyclerView.ViewHolder viewHolder
= recyclerView.findViewHolderForAdapterPosition(position);
if (viewHolder != null) {
int[] distances = calculateDistanceToFinalSnap(recyclerView.getLayoutManager(),
viewHolder.itemView);
recyclerView.scrollBy(distances[0], distances[1]);
return true;
}
}
}
return false;
}
private int getDistanceToStart(View targetView, #NonNull OrientationHelper helper) {
int distance;
// If we don't care about padding, just snap to the start of the view
if (!snapToPadding) {
int childStart = helper.getDecoratedStart(targetView);
if (childStart >= helper.getStartAfterPadding() / 2) {
distance = childStart - helper.getStartAfterPadding();
} else {
distance = childStart;
}
} else {
distance = helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
}
return distance;
}
private int getDistanceToEnd(View targetView, #NonNull OrientationHelper helper) {
int distance;
if (!snapToPadding) {
int childEnd = helper.getDecoratedEnd(targetView);
if (childEnd >= helper.getEnd() - (helper.getEnd() - helper.getEndAfterPadding()) / 2) {
distance = helper.getDecoratedEnd(targetView) - helper.getEnd();
} else {
distance = childEnd - helper.getEndAfterPadding();
}
} else {
distance = helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();
}
return distance;
}
/**
* Returns the first view that we should snap to.
*
* #param layoutManager the RecyclerView's LayoutManager
* #param helper orientation helper to calculate view sizes
* #param gravity gravity to find the closest view
* #return the first view in the LayoutManager to snap to, or null if we shouldn't snap to any
*/
#Nullable
private View findView(#NonNull RecyclerView.LayoutManager layoutManager,
#NonNull OrientationHelper helper,
int gravity,
boolean checkEdgeOfList) {
if (layoutManager.getChildCount() == 0 || !(layoutManager instanceof LinearLayoutManager)) {
return null;
}
final LinearLayoutManager lm = (LinearLayoutManager) layoutManager;
// If we're at an edge of the list, we shouldn't snap
// to avoid having the last item not completely visible.
if (checkEdgeOfList && (isAtEdgeOfList(lm) && !snapLastItem)) {
return null;
}
View edgeView = null;
int distanceToTarget = Integer.MAX_VALUE;
final int center;
if (layoutManager.getClipToPadding()) {
center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
} else {
center = helper.getEnd() / 2;
}
final boolean snapToStart = (gravity == Gravity.START && !isRtl)
|| (gravity == Gravity.END && isRtl);
final boolean snapToEnd = (gravity == Gravity.START && isRtl)
|| (gravity == Gravity.END && !isRtl);
for (int i = 0; i < lm.getChildCount(); i++) {
View currentView = lm.getChildAt(i);
int currentViewDistance;
if (snapToStart) {
if (!snapToPadding) {
currentViewDistance = Math.abs(helper.getDecoratedStart(currentView));
} else {
currentViewDistance = Math.abs(helper.getStartAfterPadding()
- helper.getDecoratedStart(currentView));
}
} else if (snapToEnd) {
if (!snapToPadding) {
currentViewDistance = Math.abs(helper.getDecoratedEnd(currentView)
- helper.getEnd());
} else {
currentViewDistance = Math.abs(helper.getEndAfterPadding()
- helper.getDecoratedEnd(currentView));
}
} else {
currentViewDistance = Math.abs(helper.getDecoratedStart(currentView)
+ (helper.getDecoratedMeasurement(currentView) / 2) - center);
}
if (currentViewDistance < distanceToTarget) {
distanceToTarget = currentViewDistance;
edgeView = currentView;
}
}
return edgeView;
}
private boolean isAtEdgeOfList(LinearLayoutManager lm) {
if ((!lm.getReverseLayout() && gravity == Gravity.START)
|| (lm.getReverseLayout() && gravity == Gravity.END)
|| (!lm.getReverseLayout() && gravity == Gravity.TOP)
|| (lm.getReverseLayout() && gravity == Gravity.BOTTOM)) {
return lm.findLastCompletelyVisibleItemPosition() == lm.getItemCount() - 1;
} else if (gravity == Gravity.CENTER) {
return lm.findFirstCompletelyVisibleItemPosition() == 0
|| lm.findLastCompletelyVisibleItemPosition() == lm.getItemCount() - 1;
} else {
return lm.findFirstCompletelyVisibleItemPosition() == 0;
}
}
/**
* Dispatches a {#link SnapListener#onSnap(int)} event if the snapped position
* is different than {#link RecyclerView#NO_POSITION}.
* <p>
* When {#link GravitySnapHelper#findSnapView(RecyclerView.LayoutManager)} returns null,
* {#link GravitySnapHelper#dispatchSnapChangeWhenPositionIsUnknown()} is called
*
* #param newState the new RecyclerView scroll state
*/
private void onScrollStateChanged(int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE && listener != null) {
if (isScrolling) {
if (nextSnapPosition != RecyclerView.NO_POSITION) {
listener.onSnap(nextSnapPosition);
} else {
dispatchSnapChangeWhenPositionIsUnknown();
}
}
}
isScrolling = newState != RecyclerView.SCROLL_STATE_IDLE;
}
/**
* Calls {#link GravitySnapHelper#findSnapView(RecyclerView.LayoutManager, boolean)}
* without the check for the edge of the list.
* <p>
* This makes sure that a position is reported in {#link SnapListener#onSnap(int)}
*/
private void dispatchSnapChangeWhenPositionIsUnknown() {
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
View snapView = findSnapView(layoutManager, false);
if (snapView == null) {
return;
}
int snapPosition = recyclerView.getChildAdapterPosition(snapView);
if (snapPosition != RecyclerView.NO_POSITION) {
listener.onSnap(snapPosition);
}
}
private OrientationHelper getVerticalHelper(RecyclerView.LayoutManager layoutManager) {
if (verticalHelper == null || verticalHelper.getLayoutManager() != layoutManager) {
verticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return verticalHelper;
}
private OrientationHelper getHorizontalHelper(RecyclerView.LayoutManager layoutManager) {
if (horizontalHelper == null || horizontalHelper.getLayoutManager() != layoutManager) {
horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return horizontalHelper;
}
/**
* A listener that's called when the {#link RecyclerView} used by {#link GravitySnapHelper}
* changes its scroll state to {#link RecyclerView#SCROLL_STATE_IDLE}
* and there's a valid snap position.
*/
public interface SnapListener {
/**
* #param position last position snapped to
*/
void onSnap(int position);
}
}

How to snap to the left side of the item in RecyclerView

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!!
}
}

fix height of scrollbar in nested recyclerview android

I have a horizontal Recyclerview inside another horizontal Recyclerview. Now I want to show the scrollbar. But the height of the scrollbar is changing as I scroll. I want to have a fixed height of the scrollbar as I scroll. How can I achieve that? I have a custom LinearLayoutManager. But it is not working properly. Any better solution to this problem?
class CoolLayoutManager(context: Context,orientation: Int,
reverseLayout: Boolean) : LinearLayoutManager(context,orientation,
reverseLayout) {
init {
isSmoothScrollbarEnabled = true
}
override fun computeHorizontalScrollExtent(state: State): Int {
val count = childCount
return if (count > 0) {
SMOOTH_VALUE * 3
} else 0
}
override fun computeHorizontalScrollRange(state: State): Int {
return Math.max((itemCount - 1) * SMOOTH_VALUE, 0)
}
override fun computeHorizontalScrollOffset(state: State): Int {
val count = childCount
if (count <= 0) {
return 0
}
if (findLastCompletelyVisibleItemPosition() == itemCount - 1) {
return Math.max((itemCount - 1) * SMOOTH_VALUE, 0)
}
val widthOfScreen: Int
val firstPos = findFirstVisibleItemPosition()
if (firstPos == RecyclerView.NO_POSITION) {
return 0
}
val view = findViewByPosition(firstPos) ?: return 0
// Top of the view in pixels
val top = getDecoratedTop(view)
val width = getDecoratedMeasuredWidth(view)
widthOfScreen = if (width <= 0) {
0
} else {
Math.abs(SMOOTH_VALUE * top / width)
}
return if (widthOfScreen == 0 && firstPos > 0) {
SMOOTH_VALUE * firstPos - 1
} else SMOOTH_VALUE * firstPos + widthOfScreen
}
companion object {
private const val SMOOTH_VALUE = 100
}
}
Here is the screenshot of the layout:

SnapHelper issue with first and Last item

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) }
}

RecyclerView with offsets and PagerSnapHelper not clipping correctly

I´ve two RecyclerViews with two ItemDecoration each that adds a fixed offset to the first and last items.
StartOffsetItemDecoration.kt
class StartOffsetItemDecoration : RecyclerView.ItemDecoration {
private var mOffsetPx: Int = 0
private var mOffsetDrawable: Drawable? = null
private var mOrientation: Int = 0
/**
* Constructor that takes in the size of the offset to be added to the
* start of the RecyclerView.
*
* #param offsetPx The size of the offset to be added to the start of the
* RecyclerView in pixels
*/
constructor(offsetPx: Int) {
mOffsetPx = offsetPx
}
/**
* Constructor that takes in a {#link Drawable} to be drawn at the start of
* the RecyclerView.
*
* #param offsetDrawable The {#code Drawable} to be added to the start of
* the RecyclerView
*/
constructor(offsetDrawable: Drawable) {
mOffsetDrawable = offsetDrawable
}
/**
* Determines the size and location of the offset to be added to the start
* of the RecyclerView.
*
* #param outRect The [Rect] of offsets to be added around the child view
* #param view The child view to be decorated with an offset
* #param parent The RecyclerView onto which dividers are being added
* #param state The current RecyclerView.State of the RecyclerView
*/
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
if (parent.getChildAdapterPosition(view) > 0) {
return
}
mOrientation = (parent.layoutManager as LinearLayoutManager).orientation
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
if (mOffsetPx > 0) {
outRect.left = mOffsetPx
} else if (mOffsetDrawable != null) {
outRect.left = mOffsetDrawable!!.intrinsicWidth
}
} else if (mOrientation == LinearLayoutManager.VERTICAL) {
if (mOffsetPx > 0) {
outRect.top = mOffsetPx
} else if (mOffsetDrawable != null) {
outRect.top = mOffsetDrawable!!.intrinsicHeight
}
}
}
/**
* Draws horizontal or vertical offset onto the start of the parent
* RecyclerView.
*
* #param c The [Canvas] onto which an offset will be drawn
* #param parent The RecyclerView onto which an offset is being added
* #param state The current RecyclerView.State of the RecyclerView
*/
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
if (mOffsetDrawable == null) {
return
}
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
drawOffsetHorizontal(c, parent)
} else if (mOrientation == LinearLayoutManager.VERTICAL) {
drawOffsetVertical(c, parent)
}
}
private fun drawOffsetHorizontal(canvas: Canvas, parent: RecyclerView) {
val parentTop = parent.paddingTop
val parentBottom = parent.height - parent.paddingBottom
val parentLeft = parent.paddingLeft
val offsetDrawableRight = parentLeft + mOffsetDrawable!!.intrinsicWidth
mOffsetDrawable?.setBounds(parentLeft, parentTop, offsetDrawableRight, parentBottom)
mOffsetDrawable?.draw(canvas)
}
private fun drawOffsetVertical(canvas: Canvas, parent: RecyclerView) {
val parentLeft = parent.paddingLeft
val parentRight = parent.width - parent.paddingRight
val parentTop = parent.paddingTop
val offsetDrawableBottom = parentTop + mOffsetDrawable!!.intrinsicHeight
mOffsetDrawable?.setBounds(parentLeft, parentTop, parentRight, offsetDrawableBottom)
mOffsetDrawable?.draw(canvas)
}
}
EndOffsetItemDecoration.kt
class EndOffsetItemDecoration: RecyclerView.ItemDecoration {
private var mOffsetPx: Int = 0
private var mOffsetDrawable: Drawable? = null
private var mOrientation: Int = 0
/**
* Constructor that takes in the size of the offset to be added to the
* start of the RecyclerView.
*
* #param offsetPx The size of the offset to be added to the start of the
* RecyclerView in pixels
*/
constructor(offsetPx: Int) {
mOffsetPx = offsetPx
}
/**
* Constructor that takes in a {#link Drawable} to be drawn at the start of
* the RecyclerView.
*
* #param offsetDrawable The {#code Drawable} to be added to the start of
* the RecyclerView
*/
constructor(offsetDrawable: Drawable) {
mOffsetDrawable = offsetDrawable
}
/**
* Determines the size and location of the offset to be added to the end
* of the RecyclerView.
*
* #param outRect The [Rect] of offsets to be added around the child view
* #param view The child view to be decorated with an offset
* #param parent The RecyclerView onto which dividers are being added
* #param state The current RecyclerView.State of the RecyclerView
*/
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val itemCount = state.itemCount
if (parent.getChildAdapterPosition(view) != itemCount - 1) {
return
}
mOrientation = (parent.layoutManager as LinearLayoutManager).orientation
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
if (mOffsetPx > 0) {
outRect.right = mOffsetPx
} else if (mOffsetDrawable != null) {
outRect.right = mOffsetDrawable!!.intrinsicWidth
}
} else if (mOrientation == LinearLayoutManager.VERTICAL) {
if (mOffsetPx > 0) {
outRect.bottom = mOffsetPx
} else if (mOffsetDrawable != null) {
outRect.bottom = mOffsetDrawable!!.intrinsicHeight
}
}
}
/**
* Draws horizontal or vertical offset onto the end of the parent
* RecyclerView.
*
* #param c The [Canvas] onto which an offset will be drawn
* #param parent The RecyclerView onto which an offset is being added
* #param state The current RecyclerView.State of the RecyclerView
*/
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
if (mOffsetDrawable == null) {
return
}
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
drawOffsetHorizontal(c, parent)
} else if (mOrientation == LinearLayoutManager.VERTICAL) {
drawOffsetVertical(c, parent)
}
}
private fun drawOffsetHorizontal(canvas: Canvas, parent: RecyclerView) {
val parentTop = parent.paddingTop
val parentBottom = parent.height - parent.paddingBottom
val lastChild = parent.getChildAt(parent.childCount - 1)
val lastChildLayoutParams = lastChild.layoutParams as RecyclerView.LayoutParams
val offsetDrawableLeft = lastChild.right + lastChildLayoutParams.rightMargin
val offsetDrawableRight = offsetDrawableLeft + mOffsetDrawable!!.intrinsicWidth
mOffsetDrawable?.setBounds(offsetDrawableLeft, parentTop, offsetDrawableRight, parentBottom)
mOffsetDrawable?.draw(canvas)
}
private fun drawOffsetVertical(canvas: Canvas, parent: RecyclerView) {
val parentLeft = parent.paddingLeft
val parentRight = parent.width - parent.paddingRight
val lastChild = parent.getChildAt(parent.childCount - 1)
val lastChildLayoutParams = lastChild.layoutParams as RecyclerView.LayoutParams
val offsetDrawableTop = lastChild.bottom + lastChildLayoutParams.bottomMargin
val offsetDrawableBottom = offsetDrawableTop + mOffsetDrawable!!.intrinsicHeight
mOffsetDrawable?.setBounds(parentLeft, offsetDrawableTop, parentRight, offsetDrawableBottom)
mOffsetDrawable?.draw(canvas)
}
}
I´ve also added a PagerSnapHelper on both RecyclerViews but they don't seem to work well with the offset given. I think I may be missing some configuration or maybe making the PageSnapHelper aware of the offset so it can calculate the right snapping for the first and the last elements.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_anomaly_solution_hours)
hours.layoutManager = LinearLayoutManager(
this#AnomalySolutionHoursActivity, LinearLayoutManager.VERTICAL, false
)
minutes.layoutManager = LinearLayoutManager(
this#AnomalySolutionHoursActivity, LinearLayoutManager.VERTICAL, false
)
hours.setHasFixedSize(true)
minutes.setHasFixedSize(true)
hours.addItemDecoration(StartOffsetItemDecoration(calculateOffset().toInt()))
hours.addItemDecoration(EndOffsetItemDecoration(calculateOffset().toInt()))
minutes.addItemDecoration(StartOffsetItemDecoration(calculateOffset().toInt()))
minutes.addItemDecoration(EndOffsetItemDecoration(calculateOffset().toInt()))
val hoursSnapHelper = PagerSnapHelper()
val minutesSnapHelper = PagerSnapHelper()
hours.adapter = TimeAdapter((0..10).toList().toTypedArray(), this#AnomalySolutionHoursActivity,
object: TimeAdapter.OnItemClickListener {
override fun onItemClick(unit: Int) {
}
})
hoursSnapHelper.attachToRecyclerView(hours)
minutes.adapter = TimeAdapter((0..60).toList().toTypedArray(), this#AnomalySolutionHoursActivity,
object: TimeAdapter.OnItemClickListener {
override fun onItemClick(unit: Int) {
}
})
minutesSnapHelper.attachToRecyclerView(minutes)
}
private fun calculateOffset(): Float {
val listHalfHeight = (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 250f, resources.displayMetrics)) / 2
val listItemHalfHeight = (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48f, resources.displayMetrics)) / 2
return listHalfHeight - listItemHalfHeight
}
}
Has someone been able to make a PagerSnapHelper work with a RecyclerView containing offsets? LinearSnapHelper seems to work better than PagerSnapHelper but still can't snap the first and the last items.
Thanks!
UPDATE
Made both list height up to 500dp but nothing changed.
UPDATE 2
Added an empty item to the first and last positions and now it work pretty good. Just have to keep on mind those items when calculating positions with sizes.

Categories

Resources