SnapHelper作用
SnapHelper:翻译过来为卡片帮助者,常见的有ViewPager2,Banner
的卡片滑动效果都是借助RecyclerView和SnapHeler
来实现。
SnapHelper
通过绑定RecyclerView
的onScrollListener
和onFlingListener
来监听RecyclerView的滑动过程,从而实现一个卡片滑动的效果。
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 {
// ....
public abstract static class OnScrollListener {
// RecyclerView滑动状态改变时调用,如开始、结束滑动时调用
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
}
// RecyclerView滑动时调用
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
}
}
public abstract static class OnFlingListener {
// RecyclerView在开始Fling时调用
public abstract boolean onFling(int velocityX, int velocityY);
}
}
SnapHeler
有三个抽象方法:calculateDistanceToFinalSnap、findSnapView、findTargetSnapPosition
抽象方法 | 作用 |
---|---|
findSnapView | scrolling时将要滑到的View |
calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager,View targetView) | 计算到目标View还需要滑动的距离 |
findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY) | fling时RecyclerView滑动的位置,返回RecyclerView.NO_POSITION 表示支持fling |
这里的三个抽象方法,是继承
SnapHelper
时必须继承实现的方法,下面讲解这三个方法分别代表的作用。
findSnapView
:RecyclerView正常滑动时,下一个达到的目标View。
calculateDistanceToFinalSnap
:RecyclerView正常滑动时,在达到目标View时,还要互动多少距离。
findTargetSnapPosition
:RecyclerView在Fling
滑动时,返回能滑动到的位置。
这个帮助类,是怎样通过设置RecyclerView的监听来达到卡片滑动的效果的呢?这就要分析其源码。
SnapHelper源码分析
入口attachToRecyclerView
public abstract class SnapHelper extends RecyclerView.OnFlingListener {
// 通过外界绑定RecyclerView,设置监听
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
// 如果和上一个设置的RecyclerView一样,则不重新设置监听
if (mRecyclerView == recyclerView) {
return;
}
// 将上一个的RecyclerView解除绑定
if (mRecyclerView != null) {
destroyCallbacks();
}
// 给现在的RecyclerView设置OnScrollListener和OnFlingListener监听
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks();
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
snapToTargetExistingView();
}
}
// 设置现在的RecyclerView的OnScrollListener和OnFlingListener监听
private void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
// 解除上一个RecyclerView的OnScrollListener和OnFlingListener监听
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
attachToRecyclerView
方法很简单,就是给RecyclerView设置OnScrollListener
和OnFlingListener
监听。接下我们看看两个监听实例,在RecyclerView滑动的时候做了什么。
SnapHelper的监听实例
public abstract class SnapHelper extends RecyclerView.OnFlingListener {
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
// RecyclerView是否在滑动变量
boolean mScrolled = false;
// 滑动状态发生改变调用
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// RecyclerView已经结束滑动
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView(); // 调用snapToTargetExistingView方法
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 滑动时,设置滑动变量为ture
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
// 结束滑动时调用
void snapToTargetExistingView() {
// 判空处理
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
// 根据layoutManager调用findSnapView拿到目标view
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
// 根据layoutManager和目标View计算还要滑动的距离
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
// RecyclerView继续滑动达到目标View的位置
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
@Override
public boolean onFling(int velocityX, int velocityY) {
// 判空处理
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return false;
}
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter == null) {
return false;
}
// 触发Fling的最小力度
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
// 如果达到触发Fling条件则,调用snapFromFling方法
// 这里返回true表示由自己处理Fling滑动,返回false则由RecyclerView的fling方法处理滑动。
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}
// 在Fling开始时调用
private boolean snapFromFling(@NonNull RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
// 判空处理,返回false由RecyclerView处理滑动
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return false;
}
RecyclerView.SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
// Fling滑动的位置
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
// 如果findTargetSnapPosition返回的位置是RecyclerView.NO_POSITION,则由RecyclerView处理Fling
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
// 自己根据positon处理Fling,并返回true
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
通过源码,SnapHelper
在RecyclerView停止滑动的时候,通过调用findSnapView
和calculateDistanceToFinalSnap
来得到目标View,并让RecyclerView继续滑动达到目标View的位置。
在RecyclerView在Fling开始的,通过调用findTargetSnapPosition
方法来判断是否自己处理Fling
事件。如果不想自己处理Fling事件,可以在findTargetSnapPosition
方法返回RecyclerView. NO_POSITION
。
看来SnapHelper只是一个基础类,并没有帮助我们做很多事。但谷歌已经为我们实现两个实例类分别是:LinearSnapHelper
,PagerSnapHelper
LinearSnapHelper源码分析
LinearSnapHelper实现的卡片效果是:使目标View能够居中显示,效果图如下:
LinearSnapHelper.findSnapView寻找目标View
LinearSnapHelper寻找目标View就是计算
View的中心点位置
与RecyclerView的中心位置
最近的一个View。
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
// 通过findCenterView方法找到目标View
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}
// 返回最靠近RecyclerView中心点的View
private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
// 遍历屏幕可见View的数量
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
// 最靠近中心点的View
View closestChild = null;
// recyclerView的中点
final int center = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
// 最小的距离值
int absClosest = Integer.MAX_VALUE;
// 遍历计算
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
int childCenter = helper.getDecoratedStart(child)
+ (helper.getDecoratedMeasurement(child) / 2);
int absDistance = Math.abs(childCenter - center);
// 如果比前面的View的距离小,则更改目标View
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}
因为LinearSnapHelper的效果使目标View居中显示,所以选中的目标View就是离父类中心点最近的View。
LinearSnapHelper.calculateDistanceToFinalSnap计算需要滑动的距离
需要滑动的距离,就是目标View的中心点到RecyclerView中心点的距离。
@Override
public int[] calculateDistanceToFinalSnap(
@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
// 通过distanceToCenter方法计算x,y轴需要滑动的距离
int[] out = new int[2];
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToCenter(targetView,
getHorizontalHelper(layoutManager));
} else {
out[0] = 0;
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToCenter(targetView,
getVerticalHelper(layoutManager));
} else {
out[1] = 0;
}
return out;
}
private int distanceToCenter(@NonNull View targetView, OrientationHelper helper) {
// 目标View的中心位置
final int childCenter = helper.getDecoratedStart(targetView)
+ (helper.getDecoratedMeasurement(targetView) / 2);
// RecyclerView的中心位置
final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
// 目标View中心位置到控件中心位置的距离
return childCenter - containerCenter;
}
代码也很简单,就不做讲解。
LinearSnapHelper.findTargetSnapPosition在Fling时最后的位置
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
// 谷歌提供的三个layoutManager都实现了,用来判断布局的方向
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return RecyclerView.NO_POSITION;
}
// 没有数据
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
// 未fling前,离容器中心点最近的View
final View currentView = findSnapView(layoutManager);
if (currentView == null) {
return RecyclerView.NO_POSITION;
}
// 未fling前,目标View的实际位置
final int currentPosition = layoutManager.getPosition(currentView);
if (currentPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}
RecyclerView.SmoothScroller.ScrollVectorProvider vectorProvider =
(RecyclerView.SmoothScroller.ScrollVectorProvider) layoutManager;
// 到最后一个View的布局方向
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()) {
// 计算水平Fling可以横跨多少个位置
hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getHorizontalHelper(layoutManager), velocityX, 0);
if (vectorForEnd.x < 0) {
hDeltaJump = -hDeltaJump;
}
} else {
// 垂直滑动
hDeltaJump = 0;
}
// 垂直滑动
if (layoutManager.canScrollVertically()) {
// 计算垂直Fling可以横跨多少个位置
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;
}
// Fling最后的Position
int targetPos = currentPosition + deltaJump;
if (targetPos < 0) {
targetPos = 0;
}
if (targetPos >= itemCount) {
targetPos = itemCount - 1;
}
return targetPos;
}
findTargetSnapPosition返回的是Fling最后停留的位置,计算的方式是通过当前目标View的position+Fling横跨的View的数量。由上面代码可以看到estimateNextPositionDiffForFling
就是返回Fling后横跨View数量的方法。
private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper, int velocityX, int velocityY) {
// 根据加速度计算滑动的距离
int[] distances = calculateScrollDistance(velocityX, velocityY);
// 根据在屏幕两边的距离/屏幕的View数,得到平均的View的长度
float distancePerChild = computeDistancePerChild(layoutManager, helper);
if (distancePerChild <= 0) {
return 0;
}
int distance =
Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
// Fling的距离 / 平均View的长度
return (int) Math.round(distance / distancePerChild);
}
PagerSnapHelper
PagerSnapHelper
的滑动效果也是居中显示,不过一次只能滑动一页。实现的效果如下。
PagerSnapHelper的findSnapView、calculateDistanceToFinalSnap
PagerSnapHelper的findSnapView
和calculateDistanceToFinalSnap
和LinearSnapHelper
的一样,都是寻找离中心点最近的View和计算两中心点的距离,这里不做分析。
PagerSnapHelper的findTargetSnapPosition计算Fling的最后位置
我们从前面的效果图可以知道,就是Fling时也最多滑动一页,所以我们看看代码如何实现。
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
final OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
if (orientationHelper == null) {
return RecyclerView.NO_POSITION;
}
// 离中心点最近的左边View
View closestChildBeforeCenter = null;
int distanceBefore = Integer.MIN_VALUE;
// 离中心点最近的右边View
View closestChildAfterCenter = null;
int distanceAfter = Integer.MAX_VALUE;
// 遍历屏幕中的view,寻找离中心点最近的左右view
final int childCount = layoutManager.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
if (child == null) {
continue;
}
// 计算子view到中心点的距离
final int distance = distanceToCenter(child, orientationHelper);
// 找到左边离中心点最近的子View,如果是已经有View的中心点等于控件的中心点,则这个View为closestChildBeforeCenter,且
if (distance <= 0 && distance > distanceBefore) {
// Child is before the center and closer then the previous best
distanceBefore = distance;
closestChildBeforeCenter = child;
}
// 找到右边离中心点最近的子View
if (distance >= 0 && distance < distanceAfter) {
// Child is after the center and closer then the previous best
distanceAfter = distance;
closestChildAfterCenter = child;
}
}
// 判断Fling的方向,true则滑右边,false为滑左边
final boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY);
if (forwardDirection && closestChildAfterCenter != null) {
return layoutManager.getPosition(closestChildAfterCenter);
} else if (!forwardDirection && closestChildBeforeCenter != null) {
return layoutManager.getPosition(closestChildBeforeCenter);
}
// 这种就是一个子View占满控件,需要上下切换View的情况。
View visibleView = forwardDirection ? closestChildBeforeCenter : closestChildAfterCenter;
if (visibleView == null) {
return RecyclerView.NO_POSITION;
}
int visiblePosition = layoutManager.getPosition(visibleView);
int snapToPosition = visiblePosition
+ (isReverseLayout(layoutManager) == forwardDirection ? -1 : +1);
if (snapToPosition < 0 || snapToPosition >= itemCount) {
return RecyclerView.NO_POSITION;
}
return snapToPosition;
}
所以为什么在Fling后只会切换一页,原因是它会找出屏幕中离中心点最近的左右两个View,然后根据滑动方向决定使用那个view的position。
总结
SnapHelper的作用在RecyclerView停止滑动的时候,对某个特定的View进行位置的滑动调整,并可以自定义RecyclerView在Fling的距离,Fling的最后的一个位置。
原理大致是:SnapHelper对RecyclerView进行OnScrollListener和OnFlingListener监听。OnScrollListener监听在滑动结束后,会依次调用findSnapView
和calculateDistanceToFinalSnap
分别得到目标View和需要滑动的距离。OnFlingListener监听在fling方法时,调用findTargetSnapPosition
得到Fling后的距离,并进行滑动Fling。