RecyclerView(五):SnapHelper對慣性滑動的作用說明

概述

SnapHelper可以看做是RecyclerView慣性滑動的一個輔助類,可以幫我們做一些慣性滑動時和滑動後的一些處理,所以對於一些慣性滑動的操作處理就可以優先考慮使用這個類,可以處理的點可以歸納爲以下三點:

  1. 可以監聽到滑動時手指擡起的那一刻;
  2. 指定手指擡起後RecyclerView慣性滑動的item個數;
  3. 滑動結束後指定item在界面所顯示的位置;
SnapHelper用到的關鍵類說明

Google爲我們提供了兩個內部實現了SnapHelper的類,分別是LinearSnapHelper和PagerSnapHelper,作用分別是:

  1. LinearSnapHelper會將當前最靠近中間位置的item居中顯示;
  2. PagerSnapHelper可以讓RecyclerView實現像ViewPager一樣的功能,只不過有一個前提條件,RecyclerView的item佈局必須在滑動方向上使用MATCH_PARENT佈局的;

爲了更好的理解SnapHelper,這裏有必要先了解下RecyclerView的兩個內部類,分別是RecyclerView.OnFlingListener和RecyclerView.SmoothScroller:

  1. RecyclerView.OnFlingListener可以通過RecyclerView的setOnFlingListener()進行設置,設置完後,RecyclerView.OnFlingListener的onFling()方法會在滑動時(有慣性滑動)手指擡起的那一刻調用到,所以這時就可以對之後的慣性動作做一些設置;
  2. 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詳解,這裏面有對這些方法的詳細解釋。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章