Preface
SnapHelper is a 24.2.0 version of support v4 package released by Google.
SnapHelper is an extension of RecyclerView functionality, making RecyclerView slide like ViewPager, no matter how slip ends up in the middle of a page.
ViewPager can only slide one page at a time, RecyclerView+SnapHelper can slide several pages at a time, and ultimately stay in the middle of a page. Very practical and cool.
SnapHelper is implemented by listening to the onFling interface in RecyclerView.OnFlingListener. LinearSnapHelper is a concrete implementation of the abstract class SnapHelper.
Realization effect
1.LinearSnapHelper is a self-contained implementation effect
Similar to ViewPager, it is easy to display a page in the middle, as long as the following two lines of code:
LinearSnapHelper mLinearSnapHelper = new LinearSnapHelper();
mLinearSnapHelper.attachToRecyclerView(recycleView);
Let's see how Linear SnapHelper implements SnapHelper. There are three main implementations:
1.calculateDistanceToFinalSnap()
@Override
public int[] calculateDistanceToFinalSnap(
@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out = new int[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;
}
When the drag or slide ends, the method is called back, returning an out = int[2], out[0]x axis, out[1] y axis, which is the offset of the position you need to correct.
2.findSnapView()
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}
As you can see from the method name, finding the aligned view is the targetView of the previous method.
3.findTargetSnapPosition()
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return RecyclerView.NO_POSITION;
}
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
final View currentView = findSnapView(layoutManager);
if (currentView == null) {
return RecyclerView.NO_POSITION;
}
final int currentPosition = layoutManager.getPosition(currentView);
if (currentPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}
RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
// 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.
PointF vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1);
if (vectorForEnd == null) {
// cannot get a vector for the given position.
return RecyclerView.NO_POSITION;
}
int vDeltaJump, hDeltaJump;
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;
}
int deltaJump = layoutManager.canScrollVertically() ? vDeltaJump : hDeltaJump;
if (deltaJump == 0) {
return RecyclerView.NO_POSITION;
}
int targetPos = currentPosition + deltaJump;
if (targetPos < 0) {
targetPos = 0;
}
if (targetPos >= itemCount) {
targetPos = itemCount - 1;
}
return targetPos;
}
At the end of the slide, for OnFling, return the target alignment item position.
2. Customizing SnapHelper to Realize Left Alignment or Right Alignment
In fact, through the above analysis, you will find that the most important are the calculateDistance ToFinalSnap and findSnapView functions.
When looking for the target view, it's not as simple as Find Center View.
I think we need to consider the boundary condition of the final item. Bad judgment will occur, no matter how slippery the last item will not be able to fully display the bug.
package com.example.myapplication.com.example;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearSnapHelper;
import android.support.v7.widget.OrientationHelper;
import android.support.v7.widget.RecyclerView;
import android.view.View;
public class MySnapHelper extends LinearSnapHelper {
// Left alignment
public static final int TYPE_SNAP_START = 2;
// Right alignment
public static final int TYPE_SNAP_END = 3;
// default
private int type = TYPE_SNAP_START;
@Nullable
private OrientationHelper mVerticalHelper;
@Nullable
private OrientationHelper mHorizontalHelper;
public MySnapHelper(int type) {
this.type = type;
}
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
if (type == TYPE_SNAP_START) {
return calculateDisOnStart(layoutManager, targetView);
} else if (type == TYPE_SNAP_END) {
return calculateDisOnEnd(layoutManager, targetView);
} else {
return super.calculateDistanceToFinalSnap(layoutManager, targetView);
}
}
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (type == TYPE_SNAP_START) {
return findStartSnapView(layoutManager);
} else if (type == TYPE_SNAP_END) {
return findEndSnapView(layoutManager);
} else {
return super.findSnapView(layoutManager);
}
}
/**
* TYPE_SNAP_START
*
* @param layoutManager
* @param targetView
* @return
*/
private int[] calculateDisOnStart(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToStart(layoutManager, targetView,
getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToStart(layoutManager, targetView,
getVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}
/**
* TYPE_SNAP_END
*
* @param layoutManager
* @param targetView
* @return
*/
private int[] calculateDisOnEnd(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToEnd(layoutManager, targetView,
getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToEnd(layoutManager, targetView,
getVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}
/**
* calculate distance to start
*
* @param layoutManager
* @param targetView
* @param helper
* @return
*/
private int distanceToStart(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
return helper.getDecoratedStart(targetView) - helper.getStartAfterPadding();
}
/**
* calculate distance to end
*
* @param layoutManager
* @param targetView
* @param helper
* @return
*/
private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();
}
/**
* find the start view
*
* @param layoutManager
* @return
*/
private View findStartSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return findStartView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
return findStartView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}
/**
* Attention should be paid to judging the last item by judging the position on the right side of the distance.
*
* @param layoutManager
* @param helper
* @return
*/
private View findStartView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager
return null;
}
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
final int start = helper.getStartAfterPadding();
int absClosest = Integer.MAX_VALUE;
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
int childStart = helper.getDecoratedStart(child);
int absDistance = Math.abs(childStart - start);
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
View firstVisibleChild = layoutManager.getChildAt(0);
if (firstVisibleChild != closestChild) {
return closestChild;
}
int firstChildStart = helper.getDecoratedStart(firstVisibleChild);
int lastChildPos = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
View lastChild = layoutManager.getChildAt(childCount - 1);
int lastChildCenter = helper.getDecoratedStart(lastChild) + (helper.getDecoratedMeasurement(lastChild) / 2);
boolean isEndItem = lastChildPos == layoutManager.getItemCount() - 1;
if (isEndItem && firstChildStart < 0 && lastChildCenter < helper.getEnd()) {
return lastChild;
}
return closestChild;
}
/**
* find the end view
*
* @param layoutManager
* @return
*/
private View findEndSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager.canScrollVertically()) {
return findEndView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
return findEndView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}
private View findEndView(RecyclerView.LayoutManager layoutManager, OrientationHelper helper) {
if (!(layoutManager instanceof LinearLayoutManager)) { // only for LinearLayoutManager
return null;
}
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
if (((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition() == 0) {
return null;
}
View closestChild = null;
final int end = helper.getEndAfterPadding();
int absClosest = Integer.MAX_VALUE;
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
int childStart = helper.getDecoratedEnd(child);
int absDistance = Math.abs(childStart - end);
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
if (lastVisibleChild != closestChild) {
return closestChild;
}
if (layoutManager.getPosition(closestChild) == ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition()) {
return closestChild;
}
View firstChild = layoutManager.getChildAt(0);
int firstChildStart = helper.getDecoratedStart(firstChild);
int firstChildPos = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
boolean isFirstItem = firstChildPos == 0;
int firstChildCenter = helper.getDecoratedStart(firstChild) + (helper.getDecoratedMeasurement(firstChild) / 2);
if (isFirstItem && firstChildStart < 0 && firstChildCenter > helper.getStartAfterPadding()) {
return firstChild;
}
return closestChild;
}
@NonNull
private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
if (mVerticalHelper == null) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
}
return mVerticalHelper;
}
@NonNull
private OrientationHelper getHorizontalHelper(
@NonNull RecyclerView.LayoutManager layoutManager) {
if (mHorizontalHelper == null) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return mHorizontalHelper;
}
}
Finally, just use our own SnapHelper, and you can do it easily.
MySnapHelper mySnapHelper = new MySnapHelper(2);
mySnapHelper.attachToRecyclerView(recycleView);
ps:
If the separator is used in the above code, the displacement will be erroneous when aligning in the center and right.
The reason is that the targetView contains item s and separators when calculating offsets. So we need to subtract the width of the divider when calculating the offset.
Take right alignment as an example: in distanceToEnd ()
private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
//No dividing line
return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();
}
Change to
private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
//Split line
return helper.getDecoratedStart(targetView) - helper.getEndAfterPadding() + targetView.getWidth();
}
If the alignment + separator is used, because the linearSnapHelper can not be changed, we can create a new class to inherit SnapHelper, copy all the code in LinearSnapHelper, just change the distanceToCenter () method.
Okay, that's basically all right.
Finally, I suggest that the best way to use this effect is not to use separators...