RecyclerView的滾動事件研究

DevWiki發表於2017-12-21

列表滾動過程

開始之前,我們想一下一個列表的滾動過程是怎樣的?

列表的滾動一般分為兩種:

  1. 手指按下 -> 手指拖拽列表移動 -> 手指停止拖拽 -> 抬起手指
  2. 手指按下 -> 手指快速拖拽後抬起手指 -> 列表繼續滾動 -> 停止滾動

從上面可以看出,滾動狀態分為:

|--靜止
|--滾動
    |--被迫拖拽移動
	|--自己滾動
複製程式碼

上面的過程的狀態變化如下:

  1. 靜止 -> 被迫拖拽移動 -> 靜止
  2. 靜止 -> 被迫拖拽移動 -> 自己滾動 -> 靜止

監聽RecyclerView的滾動

好了,我們分析完滾動的過程,再看看如何監聽RecyclerView的滾動.檢視原始碼是最好的方法.

看原始碼

檢視RecyclerView的原始碼,我們可以看到以下程式碼:

/**
 * Set a listener that will be notified of any changes in scroll state or position.
 * @param listener Listener to set or null to clear
 * @deprecated Use {@link #addOnScrollListener(OnScrollListener)} and
 *             {@link #removeOnScrollListener(OnScrollListener)}
 */
@Deprecated
public void setOnScrollListener(OnScrollListener listener) {
    mScrollListener = listener;
}

/**
 * Add a listener that will be notified of any changes in scroll state or position.
 * <p>Components that add a listener should take care to remove it when finished.
 * Other components that take ownership of a view may call {@link #clearOnScrollListeners()}
 * to remove all attached listeners.</p>
 * @param listener listener to set or null to clear
 */
public void addOnScrollListener(OnScrollListener listener) {
    if (mScrollListeners == null) {
        mScrollListeners = new ArrayList<>();
    }
    mScrollListeners.add(listener);
}
複製程式碼

也就是說有兩種方式可以監聽滾動事件:

  1. setOnScrollListener()
  2. addOnScrollListener()

其中 setOnScrollListener 已經過時(@deprecated),建議使用 addOnScrollListener.如果在原始碼中沒有 addOnScrollListener 方法,可能你的版本過舊,請升級recyclerview包.

設定的監聽器原始碼如下:

public abstract static class OnScrollListener {
    /**
     * Callback method to be invoked when RecyclerView's scroll state changes.
     * @param recyclerView The RecyclerView whose scroll state has changed.
     * @param newState     The updated scroll state. One of {@link #SCROLL_STATE_IDLE},
     *                     {@link #SCROLL_STATE_DRAGGING} or {@link #SCROLL_STATE_SETTLING}.
     */
    public void onScrollStateChanged(RecyclerView recyclerView, int newState){}

    /**
     * Callback method to be invoked when the RecyclerView has been scrolled. This will be
     * called after the scroll has completed.
     * <p>
     * This callback will also be called if visible item range changes after a layout
     * calculation. In that case, dx and dy will be 0.
     *
     * @param recyclerView The RecyclerView which scrolled.
     * @param dx The amount of horizontal scroll.
     * @param dy The amount of vertical scroll.
     */
    public void onScrolled(RecyclerView recyclerView, int dx, int dy){}
}
複製程式碼

在滾動過程中,此監聽器會回撥兩個方法.

onScrollStateChanged : 滾動狀態變化時回撥 onScrolled : 滾動時回撥

這兩者的區別在於: 狀態與過程

舉例子

注 : 以下原始碼可在最後的地址中找到.

demoRv = (RecyclerView) findViewById(R.id.demo_rv);
layoutManager = new LinearLayoutManager(this);
demoRv.setLayoutManager(layoutManager);
demoRv.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));

bookAdapter = new BookAdapter();
bookAdapter.fillList(MockService.getBookList());
demoRv.setAdapter(bookAdapter);

demoRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        Log.i(TAG, "-----------onScrollStateChanged-----------");
        Log.i(TAG, "newState: " + newState);
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        Log.i(TAG, "-----------onScrolled-----------");
        Log.i(TAG, "dx: " + dx);
        Log.i(TAG, "dy: " + dy);
        Log.i(TAG, "CHECK_SCROLL_UP: " + recyclerView.canScrollVertically(TAG_CHECK_SCROLL_UP));
        Log.i(TAG, "CHECK_SCROLL_DOWN: " + recyclerView.canScrollVertically(TAG_CHECK_SCROLL_DOWN));
    }
});
複製程式碼

以上程式碼中輸出了主要個幾個資訊:

  1. newState : 目前的狀態
  2. dx : 水平滾動距離
  3. dy : 垂直滾動距離

onScrollStateChanged 方法

void onScrollStateChanged(RecyclerView recyclerView, int newState)中回撥兩個變數:

  1. recyclerView : 當前在滾動的RecyclerView
  2. newState : 當前滾動狀態.

其中newState有三種值:

//靜止,沒有滾動
public static final int SCROLL_STATE_IDLE = 0;

//正在被外部拖拽,一般為使用者正在用手指滾動
public static final int SCROLL_STATE_DRAGGING = 1;

//自動滾動開始
public static final int SCROLL_STATE_SETTLING = 2;
複製程式碼

onScrolled 方法

void onScrolled(RecyclerView recyclerView, int dx, int dy)方法中回撥了三個變數:

  1. recyclerView : 當前滾動的view
  2. dx : 水平滾動距離
  3. dy : 垂直滾動距離

真機實踐

執行程式碼

執行以上程式碼,然後按照上面的滾動過程分別進行兩種滾動.

第一種方式緩慢滾動結果如下:

I/MainActivity: -----------onScrollStateChanged-----------
I/MainActivity: newState: 1
I/MainActivity: -----------onScrolled-----------
I/MainActivity: dx: 0
I/MainActivity: dy: -6
I/MainActivity: CHECK_SCROLL_UP: true
I/MainActivity: CHECK_SCROLL_DOWN: true
------------------------n個onScrolled--------------------
I/MainActivity: -----------onScrolled-----------
I/MainActivity: dx: 0
I/MainActivity: dy: -2
I/MainActivity: CHECK_SCROLL_UP: true
I/MainActivity: CHECK_SCROLL_DOWN: false
I/MainActivity: -----------onScrollStateChanged-----------
I/MainActivity: newState: 0
複製程式碼

第二種快速滾動結果如下:

I/MainActivity: -----------onScrollStateChanged-----------
I/MainActivity: newState: 1
I/MainActivity: -----------onScrolled-----------
I/MainActivity: dx: 0
I/MainActivity: dy: 59
I/MainActivity: CHECK_SCROLL_UP: true
I/MainActivity: CHECK_SCROLL_DOWN: true
--------------------------n個onScrolled-------------------
I/MainActivity: -----------onScrolled-----------
I/MainActivity: dx: 0
I/MainActivity: dy: 54
I/MainActivity: CHECK_SCROLL_UP: true
I/MainActivity: CHECK_SCROLL_DOWN: true
I/MainActivity: -----------onScrollStateChanged-----------
I/MainActivity: newState: 2
I/MainActivity: -----------onScrolled-----------
I/MainActivity: dx: 0
I/MainActivity: dy: 56
I/MainActivity: CHECK_SCROLL_UP: true
I/MainActivity: CHECK_SCROLL_DOWN: true
--------------------------n個onScrolled-------------------
I/MainActivity: -----------onScrolled-----------
I/MainActivity: dx: 0
I/MainActivity: dy: 14
I/MainActivity: CHECK_SCROLL_UP: true
I/MainActivity: CHECK_SCROLL_DOWN: true
I/MainActivity: -----------onScrolled-----------
I/MainActivity: dx: 0
I/MainActivity: dy: 1
I/MainActivity: CHECK_SCROLL_UP: true
I/MainActivity: CHECK_SCROLL_DOWN: true
I/MainActivity: -----------onScrollStateChanged-----------
I/MainActivity: newState: 0
複製程式碼

分析結果

且在滾動過程中發現:

1.滾動方向

dy > 0 時為手指向上滾動,列表滾動顯示下面的內容 dy < 0 時為手指向下滾動,列表滾動顯示上面的內容

2.回撥過程

緩慢拖拽回撥過程:

1. newState = RecyclerView.SCROLL_STATE_DRAGGING;
2. dy 多次改變
3. newState = RecyclerView.SCROLL_STATE_IDLE
複製程式碼

快速滾動回撥過程:

1. newState = RecyclerView.SCROLL_STATE_DRAGGING;
2. dy 多次改變
3. newState = RecyclerView.SCROLL_STATE_SETTLING;
4. dy 多次改變
5. newState = RecyclerView.SCROLL_STATE_IDLE;
複製程式碼

3.頂端與底部

以上資訊中還列印了canScrollVertically的資訊,其中:

RecyclerView.canScrollVertically(1)的值表示是否能向下滾動,false表示已經滾動到底部 RecyclerView.canScrollVertically(-1)的值表示是否能向上滾動,false表示已經滾動到頂部

封裝

基於以上,我們可以封裝一個可以回撥滾動狀態和方向的RecyclerView.

先建立事件監聽的介面OnScrollCallback,程式碼如下:

public interface OnScrollCallback {

    void onStateChanged(ScrollRecycler recycler, int state);

    void onScrollUp(ScrollRecycler recycler, int dy);

    void onScrollToBottom();

    void onScrollDown(ScrollRecycler recycler, int dy);

    void onScrollToTop();
}
複製程式碼

再寫一個類ScrollRecycler繼承RecyclerView,在類中新增以下方法:

addOnScrollListener(new OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        super.onScrollStateChanged(recyclerView, newState);
        callback.onStateChanged(ScrollRecycler.this, newState);
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            if (!recyclerView.canScrollVertically(1)) {
                callback.onScrollToBottom();
            }
            if (!recyclerView.canScrollVertically(-1)) {
                callback.onScrollToTop();
            }
        }
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if (dy > 0) {
            callback.onScrollDown(ScrollRecycler.this, dy);
        } else {
            callback.onScrollUp(ScrollRecycler.this, dy);
        }
    }
});
複製程式碼

測試以下是否可行,下面直接給出測試結果:

I/MainActivity: onStateChanged: state:1
I/MainActivity: onScrollUp: -11
I/MainActivity: onScrollUp: -10
I/MainActivity: onScrollUp: -14
I/MainActivity: onScrollUp: -22
I/MainActivity: onStateChanged: state:2
I/MainActivity: onStateChanged: state:0
I/MainActivity: onScrollToTop: 
複製程式碼

完畢,以上就是對RecyclerView的滾動的簡單研究,專案程式碼地址如下:Dev-Wiki/RecyclerScroll: RecyclerView滾動的Demo

更多文章請移步我的部落格:DevWiki Blog

重要說明

想隨時獲取最新部落格文章更新,請關注公共賬號DevWiki,或掃描下面的二維碼:

微信公共號

相關文章