关于Android24.2.0支持库SnapHelper的使用

横向滑动RecyclerView一般都会要求类似Viewpager的效果就是要保证第一个item是全部展现的。我之前的处理方法就是添加滑动监听然后当滑动停止时计算显示的首个可见及完全可见的position然后再进行滑动达到这个效果。不过在实现时会发现滑动速度太快,这里有需要重写LinearSmoothScroller。然后还有一个重要的效果就是Fling时要保证滑动停止时直接是完全显示首个Item,这里之前的方法是预测Fling的滑动距离然后找到最近的position之后再用LayoutManager的scrolltoposition()滑动过去。不过这样就没有fling的速度效果了。因为smoothscroll都是匀速的,现在recyclerView 24.2.0的支持库提供了SnapHelper现在我们可以完全模拟实现googleplay的效果了。

SnapHelper

SnapHelper 是一个抽象类,需要实现三个方法:

1
2
3
4
5
6
//当拖拽或滑动结束时会回调该方法,返回一个out = int[2],out[0]x轴,out[1] y轴 ,这个值就是需要修正的你需要的位置的偏移量
public abstract int[] calculateDistanceToFinalSnap(@NonNull LayoutManager layoutManager, @NonNull View targetView);
//上面方法有一个targetView吧 就是这个方法返回的
public abstract View findSnapView(LayoutManager layoutManager);
//用于Fling,根据速度返回你要滑到的position
public abstract int findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY);

看这三个方法接收就会发现已经可以实现GooglePlay上的效果了。下面以横向LinearLayoutManager以首项对齐为基础我们来简单实现下。

创建MySnapHelper 继承 LinearSnapHelper

LinearSnapHelper 默认实现fling的相关操作,LinearSnapHelper 是居中对齐的,如果是这样的需求可以直接使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MySnapHelper extends LinearSnapHelper {
private int[] out = new int[2];

@Nullable
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) targetView.getLayoutParams();
int width = layoutManager.getDecoratedMeasuredWidth(targetView) + params.leftMargin + params.rightMargin;
int childLeft = layoutManager.getDecoratedLeft(targetView) - params.leftMargin;
int parentPaddingLeft = layoutManager.getPaddingLeft();
if (childLeft >= parentPaddingLeft) {
out[0] = childLeft - parentPaddingLeft;
} else if (parentPaddingLeft - childLeft > width / 2) {
out[0] = childLeft + width-parentPaddingLeft;
} else {
out[0] = childLeft - parentPaddingLeft;
}
return out;
}

@Nullable
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
int firstVizPosition = linearLayoutManager.findFirstVisibleItemPosition();
if (firstVizPosition >= 0) {
return layoutManager.findViewByPosition(firstVizPosition);
}
}
return null;
}

}

写RecyclerView那些东西(略)

使用我们定义好的MySnapHelper

1
2
3
4
5
ActivityMainBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_main);
binding.recycleView.setAdapter(new MyMockAdapter());
binding.recycleView.setLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));
MySnapHelper snapHelper = new MySnapHelper();
snapHelper.attachToRecyclerView(binding.recycleView);

看下效果吧

原理分析

最开始扯了一堆我之前实现的想法,那么肯定要分析下SnapHelper做了些什么吧

  • 添加Scroll及Fling的监听

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    mRecyclerView.addOnScrollListener(mScrollListener);
    mRecyclerView.setOnFlingListener(this);
    private final RecyclerView.OnScrollListener mScrollListener =
    new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
    super.onScrollStateChanged(recyclerView, newState);
    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
    snapToTargetExistingView();
    }
    }
    };
    @Override
    public boolean onFling(int velocityX, int velocityY) {
    LayoutManager layoutManager = mRecyclerView.getLayoutManager();
    if (layoutManager == null) {
    return false;
    }
    RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
    if (adapter == null) {
    return false;
    }
    int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
    return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
    && snapFromFling(layoutManager, velocityX, velocityY);
    }
  • snapToTargetExistingView,用了定义的两个抽象方法,然后执行滑动,这里的滑动用的是RecyclerView的scroller不是匀速的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
  • 对于fling使用了匀速的scroller,这里放慢了滑动的速度,这里重写了onTargetFind,因为默认是保证View的可见性,这里进行了扩展
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Nullable
private LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return null;
}
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
int[] snapDistances = calculateDistanceToFinalSnap(mRecyclerView.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 MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}

总结

  • 普通的滑动使用了正常的Scroller
  • Fling使用了匀速的Scroller