转载请注明出处:http://blog.csdn.net/ym4189/article/details/77373379
前言
SnapHelper是Google发布的support v4包24.2.0版本出来的。
SnapHelper是对RecyclerView功能的一种拓展,使RecyclerView滑动行为类似ViewPager,无论怎么滑动最终停留在某页正中间。
ViewPager一次只能滑动一页,RecyclerView+SnapHelper方式可以一次滑动好几页,且最终都停留在某页正中间。非常实用和酷炫。
SnapHelper的实现原理是监听RecyclerView.OnFlingListener中的onFling接口。LinearSnapHelper是抽象类SnapHelper的具体实现。
实现效果
1.LinearSnapHelper是自带的实现效果
类似ViewPager,将某页居中显示,实现也是很简单,只要下面的两行代码:
LinearSnapHelper mLinearSnapHelper = new LinearSnapHelper();
mLinearSnapHelper.attachToRecyclerView(recycleView);
我们来看下LinearSnapHelper是怎么实现SnapHelper的,其中主要实现3个方法:
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;
}
当拖拽或滑动结束时会回调该方法,返回一个out = int[2],out[0]x轴,out[1] y轴 ,这个值就是需要修正的你需要的位置的偏移量 。
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;
}
看方法名就知道,找到对齐视图,就是上个方法的targetView。
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;
}
滑动结束时,用于OnFling,返回目标对齐项position 。
2.自定义SnapHelper实现左对齐或右对齐
其实通过上面的分析,就会发现最主要的就是 calculateDistanceToFinalSnap 和 findSnapView 这两个函数。
在寻找目标View的时候,不像findCenterView那么简单。
以为需要考虑到最后item的边界情况。判断的不好就会出现,无论怎么滑动都会出现最后一个item无法完整显示的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 {
// 左对齐
public static final int TYPE_SNAP_START = 2;
// 右对齐
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;
}
/**
* 注意判断最后一个item时,应通过判断距离右侧的位置
*
* @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;
}
}
最后只要用上我们自己的SnapHelper,就可以轻松搞定了。
MySnapHelper mySnapHelper = new MySnapHelper(2);
mySnapHelper.attachToRecyclerView(recycleView);
ps:
上面代码中如果使用分隔线,在居中对齐和右对齐时,位移会有误差。
原因是:在计算偏移量时targetView包含item和分隔线。所以我们在计算偏移量时需要把分隔线宽度减掉,
以右对齐为例:在distanceToEnd()中把
private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
//无分隔线
return helper.getDecoratedEnd(targetView) - helper.getEndAfterPadding();
}
改为
private int distanceToEnd(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView, OrientationHelper helper) {
//有分割线
return helper.getDecoratedStart(targetView) - helper.getEndAfterPadding() + targetView.getWidth();
}
如果使用的是居中对齐+分隔线,由于自带LinearSnapHelper无法更改,我们可以新建类继承SnapHelper,把LinearSnapHelper中代码全部copy过来,只需更改distanceToCenter()方法即可。
好了,基本就没问题了。
最后,个人建议使用此效果最好不要用分隔线……