On Android, you can implement RecyclerView Snapping using a class called SnapHelper. Snapping? As some people may think, snapping is a behavior that stops at a specific item instead of fling as usual when scrolling. The Netflix home screen uses Snapping.
This time, I will describe how to realize Snapping on Android.
There are three states for scrolling in RecyclerView.
SCROLL_STATE_IDLE
(RecyclerView is not scrolling.)
SCROLL_STATE_DRAGGING
(RecyclerView is being dragged)
SCROLL_STATE_SETTLING
(RecyclerView is animating to the final position)
SnapHelper Next, let's take a look inside the main subject, SnapHelper. SnapHelper has three Abstract methods. You can achieve the intended Snapping by overriding this.
calculateDistanceToFinalSnap
calculateDistanceToFinalSnap returns the distance to the final position based on the View (targetView) you want to Snap.
/**
* Override this method to snap to a particular point within the target view or the container
* view on any axis.
* <p>
* This method is called when the {@link SnapHelper} has intercepted a fling and it needs
* to know the exact distance required to scroll by in order to snap to the target view.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
* @param targetView the target view that is chosen as the view to snap
*
* @return the output coordinates the put the result into. out[0] is the distance
* on horizontal axis and out[1] is the distance on vertical axis.
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView);
The targetView is the View you want to snap. To achieve the Netflix-like behavior above, the targetView is snapped to the center, so you just need to return the difference between the center of the targetView and the center of the RecyclerView itself.
@Override
int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = getDistance(layoutManager, targetView, OrientationHelper.createHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
if (layoutManager.canScrollVertically()) {
out[1] = getDistance(layoutManager, targetView, OrientationHelper.createVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}
int getDistance(RecyclerView.LayoutManager layoutManager, View targetView, OrientationHelper helper) {
final int childCenter = helper.getDecoratedStart(targetView) + (helper.getDecoratedMeasurement(targetView) / 2);
final int containerCenter = layoutManager.getClipToPadding()
? helper.getStartAfterPadding() + helper.getTotalSpace() / 2
: helper.getEnd() / 2;
return childCenter - containerCenter;
}
findSnapView
As the name suggests, findSnapView returns the View you want to Snap. The View returned here will be included as an argument of calculateDistanceToFinalSnap above.
This method is called when the scroll state is SCROLL_STATE_IDLE
and when SnapHelper is attached to the RecyclerView.
/**
* Override this method to provide a particular target view for snapping.
* <p>
* This method is called when the {@link SnapHelper} is ready to start snapping and requires
* a target view to snap to. It will be explicitly called when the scroll state becomes idle
* after a scroll. It will also be called when the {@link SnapHelper} is preparing to snap
* after a fling and requires a reference view from the current set of child views.
* <p>
* If this method returns {@code null}, SnapHelper will not snap to any view.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
*
* @return the target view to which to snap on fling or end of scroll
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public abstract View findSnapView(LayoutManager layoutManager);
To achieve the Netflix-like behavior above, items are scrolled together in groups of three, so if you return the View that is closest to the center with item positions 1, 4, 7 ... It will be good.
@Override
View findSnapView(RecyclerView.LayoutManager layoutManager) {
OrientationHelper helper = layoutManager.canScrollHorizontally()
? OrientationHelper.createHorizontalHelper(layoutManager)
: OrientationHelper.createVerticalHelper(layoutManager);
int childCount = layoutManager.getChildCount();
View closestChild = null;
int containerCenter = layoutManager.getClipToPadding()
? helper.getStartAfterPadding() + helper.getTotalSpace() / 2
: helper.getEnd() / 2;
int absClosest = Integer.MAX_VALUE;
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
if (child == null) continue;
if (getChildPosition(child, helper) % 3 != 1) continue;
int childCenter = helper.getDecoratedStart(child) + (helper.getDecoratedMeasurement(child) / 2);
int absDistance = Math.abs(childCenter - containerCenter);
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}
findTargetSnapPosition
findTargetSnapPosition returns the Position of targetView. At first glance, it looks the same as findSnapView, but the timing at which the method is called is different.
findTargetSnapPosition is called with the RecyclerView set to SCROLL_STATE_SETTLING
. Therefore, the View itself may not have been generated, so the Position of the View is returned instead of the View.
/**
* Override to provide a particular adapter target position for snapping.
*
* @param layoutManager the {@link RecyclerView.LayoutManager} associated with the attached
* {@link RecyclerView}
* @param velocityX fling velocity on the horizontal axis
* @param velocityY fling velocity on the vertical axis
*
* @return the target adapter position to you want to snap or {@link RecyclerView#NO_POSITION}
* if no snapping should happen
*/
public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY);
To achieve the Netflix-like behavior above, return the Positionw of the View that snaps according to the scroll direction.
@Override
int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
boolean forwardDirection = layoutManager.canScrollHorizontally() ? velocityX > 0 : velocityY > 0;
return forwardDirection ? previousClosestPosition + 3 : previousClosestPosition - 3;
}
The previousClosestPosition can be realized by holding the previous Snapped Position in a member variable etc. without findSnapView.
I wrote a lot about how to use SnapHelper, but I made a library using the contents I wrote this time.
You can specify Gravity
in which direction to Snap and the number of items to scroll SnapCount
.
I would appreciate it if you could take a look.
And if you like it, I would appreciate it if you could push the star!
https://github.com/TakuSemba/MultiSnapRecyclerView
feature1: Gravity | feature2: SnapCount |
---|---|