概述
SnapHelper可以看做是RecyclerView慣性滑動的一個輔助類,可以幫我們做一些慣性滑動時和滑動後的一些處理,所以對於一些慣性滑動的操作處理就可以優先考慮使用這個類,可以處理的點可以歸納爲以下三點:
- 可以監聽到滑動時手指擡起的那一刻;
- 指定手指擡起後RecyclerView慣性滑動的item個數;
- 滑動結束後指定item在界面所顯示的位置;
SnapHelper用到的關鍵類說明
Google爲我們提供了兩個內部實現了SnapHelper的類,分別是LinearSnapHelper和PagerSnapHelper,作用分別是:
- LinearSnapHelper會將當前最靠近中間位置的item居中顯示;
- PagerSnapHelper可以讓RecyclerView實現像ViewPager一樣的功能,只不過有一個前提條件,RecyclerView的item佈局必須在滑動方向上使用MATCH_PARENT佈局的;
爲了更好的理解SnapHelper,這裏有必要先了解下RecyclerView的兩個內部類,分別是RecyclerView.OnFlingListener和RecyclerView.SmoothScroller:
- RecyclerView.OnFlingListener可以通過RecyclerView的setOnFlingListener()進行設置,設置完後,RecyclerView.OnFlingListener的onFling()方法會在滑動時(有慣性滑動)手指擡起的那一刻調用到,所以這時就可以對之後的慣性動作做一些設置;
- RecyclerView.SmoothScroller是滑動的工具類,比如對慣性滑動的速度,滑動到哪個位置等,將指定位置滑動到頂部還是底部,都是由它來處理,滑動的距離以及速度也是(onTargetFound()方法中去處理);
如果還不是很明白,可以自己寫個小demo測試下這兩個類,RecyclerView.OnFlingListener就不說了,設置下RecyclerView的回調就可以了,下面就來說下怎麼單獨測試下RecyclerView.SmoothScroller這個類,Google也爲我們提供了它的一個子類LinearSmoothScroller,這裏就說下對它的簡單使用:
public void scrollToOffsetPostion(int position){
LinearSmoothScroller scroller = new LinearSmoothScroller(this);
scroller.setTargetPosition(position);
recyclerView.getLayoutManager().startSmoothScroll(scroller);
}
調用這個方法後會滑動到指定的position位置,如果position已經顯示了,那麼就不會再滑動了,如果position不再當前的顯示界面,那麼會將指定位置的item滑動到邊緣對齊位置。
如果想想將指定的position滑動到置頂或置低,那麼需要去重寫LinearSmoothScroller,如下:
public class PagerGridSmoothScroller extends LinearSmoothScroller {
public PagerGridSmoothScroller(@NonNull RecyclerView recyclerView) {
super(recyclerView.getContext());
}
// SNAP_TO_START = -1;
// SNAP_TO_END = 1;
// SNAP_TO_ANY = 0;
@Override
protected int getHorizontalSnapPreference() {
return SNAP_TO_START;
}
@Override
protected int getVerticalSnapPreference() {
return SNAP_TO_START; // 將子view與父view頂部對齊
}
}
根據想滑動到的位置去返回對應的值就可以了。
SnapHelper各方法的作用說明
1. attachToRecyclerView(@Nullable RecyclerView recyclerView): 將SnapHelper與綁定起來,從而實現輔助滾動的作用,如果想是解綁,傳入null即可;
2.calculateScrollDistance(int velocityX, int velocityY): 根據傳入的速度計算各自方向上滑動的距離並返回;
3.findSnapView(LayoutManager layoutManager): 這也是一個抽象方法,主要作用是找到目標位置的那個view,可用於後面計算這個view到目標位置的距離;
4.calculateDistanceToFinalSnap(LayoutManager layoutManager, @NonNull View targetView): 這是一個抽象方法,這個方法的主要作用是計算targetView到目標位置的距離,這個距離用於後面的滑動,也就是將targetView滑動到指定位置;
5.findTargetSnapPosition(LayoutManager layoutManager, int velocityX, int velocityY): 這是一個抽象方法,主要作用是根據傳入的速度計算最終哪個位置的item需要滑動到目標位置,也可以根據自己的邏輯計算最後慣性滑動停留的位置;
說完這些方法的作用後,接下來就來看看SnapHelper整體邏輯的處理,出發點就是attachToRecyclerView()這個方法了:
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
// 如果已經設置了那就不再設置了,直接返回
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
// 如果傳進來的RecyclerView爲null,那麼就會與之前的RecyclerView進行解綁
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
// 下面這個方法就是對RecyclerView進行綁定
setupCallbacks();
// 根據速度計算滑動距離的時候會用到
mGravityScroller = new Scroller(mRecyclerView.getContext(),new DecelerateInterpolator());
// 將最靠近目標位置的item滾動到目標位置,這裏有一點需要主要,如果在界面繪製之前就已經調用了這個方法,那麼是不會將靠近目標位置
// 的item滾動到目標位置的
snapToTargetExistingView();
}
}
private void destroyCallbacks() {
// 與RecyclerView解綁就是移除之前設置過的回調
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
private void setupCallbacks() throws IllegalStateException {
// 注意這裏,有時候再使用的時候可能會遇到這個異常
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
// 這裏就是對RecyclerView回調的綁定了
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
void snapToTargetExistingView() {
if (mRecyclerView == null) {
return;
}
LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return;
}
// findSnapView()方法的作用上面已經說了,找到距離目標位置最近的View
View snapView = findSnapView(layoutManager);
if (snapView == null) {
return;
}
// 計算到目標位置的距離
int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
// 將對應的view滑動到目標位置
mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
}
}
綁定的整體邏輯上面代碼中都有註釋,代碼不多,也比較好理解,滑動時的邏輯也和這個類似,只是多了一些其他的細節,那就接着往下看,綁定回調的時候有調用mRecyclerView.setOnFlingListener(this),那就是說滑動時手指擡起時會調用到它的onFling()方法:
@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);
}
private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return false;
}
SmoothScroller smoothScroller = createScroller(layoutManager);
if (smoothScroller == null) {
return false;
}
// 根據滑動速度計算最終需要滑動到哪個位置
int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
if (targetPosition == RecyclerView.NO_POSITION) {
return false;
}
// 滑動的工具類,設置最終滑動到哪個位置,注意這裏的滑動是會與RecyclerView的邊界對齊,所以在停止滑動時還需要對其進行處理,後面會說到
smoothScroller.setTargetPosition(targetPosition);
layoutManager.startSmoothScroll(smoothScroller);
return true;
}
在慣性滑動開始的那一刻調用了onFling(),這裏會對需要滑動到的位置進行處理,如果你對滑動時滑動的item個數有要求的話,那麼就可以在findTargetSnapPosition()方法中的返回值進行限制,比如返回的最大值不超過四,那麼這裏滑動的item個數就不會超過四個,慣性滑動的處理就到這裏了,接下來要說的就是滑動快結束時的處理,前面有說到,調用SmoothScroller的setTargetPosition()方法只會將指定位置的item滑動到RecyclerView邊界對齊,要想將指定位置的item滑動到目標位置,這裏還是需要藉助SmoothScroller這個類, 這裏先來看下createScroller(layoutManager)這個方法:
@Nullable
protected SmoothScroller createScroller(LayoutManager layoutManager) {
return createSnapScroller(layoutManager);
}
protected LinearSmoothScroller createSnapScroller(LayoutManager layoutManager) {
if (!(layoutManager instanceof ScrollVectorProvider)) {
return null;
}
// 這裏需要注意onTargetFound()這個方法的調用時機,它是在RecyclerView滑動到指定位置的item時,並且指定位置的item(即下面
//onTargetFound()方法中targetView)在被繪製出來前會被調用到
return new LinearSmoothScroller(mRecyclerView.getContext()) {
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
// 計算targetView到目標位置的距離
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);
}
}
// 這個方法會影響到上面calculateTimeForDeceleration()方法的返回值,而這個返回值就會影響到最後滑動到目標位置的速度
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
}
可以看到,當指定item滑動到RecyclerView邊界時,這時就會調用到onTargetFound()方法,這時在計算這個view到目標位置的距離並滾動到目標位置。
還記得上面綁定RecyclerView時候還調用了mRecyclerView.addOnScrollListener(mScrollListener),現在就來看看mScrollListener這個對象:
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
snapToTargetExistingView();
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
邏輯還是很簡單的,就是在停止滑動時調用了snapToTargetExistingView()方法,這個方法在綁定RecyclerView的時候就有調用到,這裏就不在分析了,到此這個流程就梳理了一遍。如果有對SnapHelper幾個抽象方法實現感興趣的,可以去看看LinearSnapHelper,如果看着比較喫力,可以參考SnapHelper詳解,這裏面有對這些方法的詳細解釋。