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);
}
}
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!!
}
}
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:
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´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.