SnapHelper作用
SnapHelper:翻譯過來為卡片幫助者,常見的有ViewPager2,Banner的卡片滑動效果都是借助RecyclerView和SnapHeler來實(shí)現(xiàn)。
SnapHelper通過綁定RecyclerView的onScrollListener和onFlingListener來監(jiān)聽RecyclerView的滑動過程,從而實(shí)現(xiàn)一個卡片滑動的效果。
public class RecyclerView extends ViewGroup implements ScrollingView,
NestedScrollingChild2, NestedScrollingChild3 {
// ....
public abstract static class OnScrollListener {
// RecyclerView滑動狀態(tài)改變時調(diào)用,如開始、結(jié)束滑動時調(diào)用
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
}
// RecyclerView滑動時調(diào)用
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
}
}
public abstract static class OnFlingListener {
// RecyclerView在開始Fling時調(diào)用
public abstract boolean onFling(int velocityX, int velocityY);
}
}
SnapHeler有三個抽象方法:calculateDistanceToFinalSnap、findSnapView、findTargetSnapPosition
| 抽象方法 | 作用 |
|---|---|
| findSnapView | scrolling時將要滑到的View |
| calculateDistanceToFinalSnap(RecyclerView.LayoutManager layoutManager,View targetView) | 計算到目標(biāo)View還需要滑動的距離 |
| findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,int velocityY) | fling時RecyclerView滑動的位置,返回RecyclerView.NO_POSITION表示支持fling |
這里的三個抽象方法,是繼承
SnapHelper時必須繼承實(shí)現(xiàn)的方法,下面講解這三個方法分別代表的作用。
findSnapView:RecyclerView正常滑動時,下一個達(dá)到的目標(biāo)View。
calculateDistanceToFinalSnap:RecyclerView正?;瑒訒r,在達(dá)到目標(biāo)View時,還要互動多少距離。
findTargetSnapPosition:RecyclerView在Fling滑動時,返回能滑動到的位置。
這個幫助類,是怎樣通過設(shè)置RecyclerView的監(jiān)聽來達(dá)到卡片滑動的效果的呢?這就要分析其源碼。
SnapHelper源碼分析
入口attachToRecyclerView
public abstract class SnapHelper extends RecyclerView.OnFlingListener {
// 通過外界綁定RecyclerView,設(shè)置監(jiān)聽
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
// 如果和上一個設(shè)置的RecyclerView一樣,則不重新設(shè)置監(jiān)聽
if (mRecyclerView == recyclerView) {
return;
}
// 將上一個的RecyclerView解除綁定
if (mRecyclerView != null) {
destroyCallbacks();
}
// 給現(xiàn)在的RecyclerView設(shè)置OnScrollListener和OnFlingListener監(jiān)聽
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
setupCallbacks();
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
snapToTargetExistingView();
}
}
// 設(shè)置現(xiàn)在的RecyclerView的OnScrollListener和OnFlingListener監(jiān)聽
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監(jiān)聽
private void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
attachToRecyclerView方法很簡單,就是給RecyclerView設(shè)置OnScrollListener和OnFlingListener監(jiān)聽。接下我們看看兩個監(jiān)聽實(shí)例,在RecyclerView滑動的時候做了什么。
SnapHelper的監(jiān)聽實(shí)例
public abstract class SnapHelper extends RecyclerView.OnFlingListener {
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
// RecyclerView是否在滑動變量
boolean mScrolled = false;
// 滑動狀態(tài)發(fā)生改變調(diào)用
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// RecyclerView已經(jīng)結(jié)束滑動
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView(); // 調(diào)用snapToTargetExistingView方法
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// 滑動時,設(shè)置滑動變量為ture
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
// 結(jié)束滑動時調(diào)用
void snapToTargetExistingView() {
// 判空處理
if (mRecyclerView == null) {
return;
}
RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
// 根據(jù)layoutManager調(diào)用findSnapView拿到目標(biāo)view
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
// 根據(jù)layoutManager和目標(biāo)View計算還要滑動的距離
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
// RecyclerView繼續(xù)滑動達(dá)到目標(biāo)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;
}
// 觸發(fā)Fling的最小力度
int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
// 如果達(dá)到觸發(fā)Fling條件則,調(diào)用snapFromFling方法
// 這里返回true表示由自己處理Fling滑動,返回false則由RecyclerView的fling方法處理滑動。
return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
&& snapFromFling(layoutManager, velocityX, velocityY);
}
// 在Fling開始時調(diào)用
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;
}
// 自己根據(jù)positon處理Fling,并返回true
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
通過源碼,SnapHelper在RecyclerView停止滑動的時候,通過調(diào)用findSnapView和calculateDistanceToFinalSnap來得到目標(biāo)View,并讓RecyclerView繼續(xù)滑動達(dá)到目標(biāo)View的位置。
在RecyclerView在Fling開始的,通過調(diào)用findTargetSnapPosition方法來判斷是否自己處理Fling事件。如果不想自己處理Fling事件,可以在findTargetSnapPosition方法返回RecyclerView. NO_POSITION。
看來SnapHelper只是一個基礎(chǔ)類,并沒有幫助我們做很多事。但谷歌已經(jīng)為我們實(shí)現(xiàn)兩個實(shí)例類分別是:LinearSnapHelper,PagerSnapHelper
LinearSnapHelper源碼分析
LinearSnapHelper實(shí)現(xiàn)的卡片效果是:使目標(biāo)View能夠居中顯示,效果圖如下:

LinearSnapHelper.findSnapView尋找目標(biāo)View
LinearSnapHelper尋找目標(biāo)View就是計算
View的中心點(diǎn)位置與RecyclerView的中心位置最近的一個View。
@Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
// 通過findCenterView方法找到目標(biāo)View
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager));
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager));
}
return null;
}
// 返回最靠近RecyclerView中心點(diǎn)的View
private View findCenterView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
// 遍歷屏幕可見View的數(shù)量
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
// 最靠近中心點(diǎn)的View
View closestChild = null;
// recyclerView的中點(diǎn)
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的距離小,則更改目標(biāo)View
if (absDistance < absClosest) {
absClosest = absDistance;
closestChild = child;
}
}
return closestChild;
}
因為LinearSnapHelper的效果使目標(biāo)View居中顯示,所以選中的目標(biāo)View就是離父類中心點(diǎn)最近的View。
LinearSnapHelper.calculateDistanceToFinalSnap計算需要滑動的距離
需要滑動的距離,就是目標(biāo)View的中心點(diǎn)到RecyclerView中心點(diǎn)的距離。
@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) {
// 目標(biāo)View的中心位置
final int childCenter = helper.getDecoratedStart(targetView)
+ (helper.getDecoratedMeasurement(targetView) / 2);
// RecyclerView的中心位置
final int containerCenter = helper.getStartAfterPadding() + helper.getTotalSpace() / 2;
// 目標(biāo)View中心位置到控件中心位置的距離
return childCenter - containerCenter;
}
代碼也很簡單,就不做講解。
LinearSnapHelper.findTargetSnapPosition在Fling時最后的位置
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
// 谷歌提供的三個layoutManager都實(shí)現(xiàn)了,用來判斷布局的方向
if (!(layoutManager instanceof RecyclerView.SmoothScroller.ScrollVectorProvider)) {
return RecyclerView.NO_POSITION;
}
// 沒有數(shù)據(jù)
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
// 未fling前,離容器中心點(diǎn)最近的View
final View currentView = findSnapView(layoutManager);
if (currentView == null) {
return RecyclerView.NO_POSITION;
}
// 未fling前,目標(biāo)View的實(shí)際位置
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;
}
// 根據(jù)可滑動方向,選擇變量
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最后停留的位置,計算的方式是通過當(dāng)前目標(biāo)View的position+Fling橫跨的View的數(shù)量。由上面代碼可以看到estimateNextPositionDiffForFling就是返回Fling后橫跨View數(shù)量的方法。
private int estimateNextPositionDiffForFling(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper, int velocityX, int velocityY) {
// 根據(jù)加速度計算滑動的距離
int[] distances = calculateScrollDistance(velocityX, velocityY);
// 根據(jù)在屏幕兩邊的距離/屏幕的View數(shù),得到平均的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的滑動效果也是居中顯示,不過一次只能滑動一頁。實(shí)現(xiàn)的效果如下。

PagerSnapHelper的findSnapView、calculateDistanceToFinalSnap
PagerSnapHelper的findSnapView和calculateDistanceToFinalSnap和LinearSnapHelper的一樣,都是尋找離中心點(diǎn)最近的View和計算兩中心點(diǎn)的距離,這里不做分析。
PagerSnapHelper的findTargetSnapPosition計算Fling的最后位置
我們從前面的效果圖可以知道,就是Fling時也最多滑動一頁,所以我們看看代碼如何實(shí)現(xiàn)。
@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;
}
// 離中心點(diǎn)最近的左邊View
View closestChildBeforeCenter = null;
int distanceBefore = Integer.MIN_VALUE;
// 離中心點(diǎn)最近的右邊View
View closestChildAfterCenter = null;
int distanceAfter = Integer.MAX_VALUE;
// 遍歷屏幕中的view,尋找離中心點(diǎn)最近的左右view
final int childCount = layoutManager.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
if (child == null) {
continue;
}
// 計算子view到中心點(diǎn)的距離
final int distance = distanceToCenter(child, orientationHelper);
// 找到左邊離中心點(diǎn)最近的子View,如果是已經(jīng)有View的中心點(diǎn)等于控件的中心點(diǎn),則這個View為closestChildBeforeCenter,且
if (distance <= 0 && distance > distanceBefore) {
// Child is before the center and closer then the previous best
distanceBefore = distance;
closestChildBeforeCenter = child;
}
// 找到右邊離中心點(diǎn)最近的子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后只會切換一頁,原因是它會找出屏幕中離中心點(diǎn)最近的左右兩個View,然后根據(jù)滑動方向決定使用那個view的position。
總結(jié)
SnapHelper的作用在RecyclerView停止滑動的時候,對某個特定的View進(jìn)行位置的滑動調(diào)整,并可以自定義RecyclerView在Fling的距離,F(xiàn)ling的最后的一個位置。
原理大致是:SnapHelper對RecyclerView進(jìn)行OnScrollListener和OnFlingListener監(jiān)聽。OnScrollListener監(jiān)聽在滑動結(jié)束后,會依次調(diào)用findSnapView和calculateDistanceToFinalSnap分別得到目標(biāo)View和需要滑動的距離。OnFlingListener監(jiān)聽在fling方法時,調(diào)用findTargetSnapPosition得到Fling后的距離,并進(jìn)行滑動Fling。