一種統計ListView滾動距離的方法

woodWu發表於2020-09-30

注:本文同步釋出於微信公眾號:stringwu的網際網路雜談 一種統計ListView滾動距離的方法

ListView做為Android中最常使用的列表控制元件,主要用來顯示同一類的資料,如應用列表,商品列表等。ListView的詳細使用與介紹可查閱官方文件ListView。這裡不再展示敘述。

1 背景

ListView在螢幕上會固定一定長度,如果內容超過這個長度,一般是通過滑動來向下瀏覽更多的內容。此時有產品就想統計出使用者在某一次瀏覽中是否有滑動,並且想實際量化該滑動距離。雖然覺得這個需求很扯淡,但做為開發的我還是老老實實去尋找實際的統計解決方案。但搜尋了一圈並沒有找到一個滿足需求的解決方案。於是就有了此文。

2 方案

2.1 ListView滾動監聽

ListView提供了一個setOnScrollListener的介面來接收List的滾動事件:

public class AbsListView{
 .....
  /**
     * Set the listener that will receive notifications every time the list scrolls.
     *
     * @param l the scroll listener
     */
    public void setOnScrollListener(OnScrollListener l) {
        mOnScrollListener = l;
        invokeOnItemScrollListener();
    }
}

其中,OnScrollListener的介面為:

public class AbsListView{
 public interface OnScrollListener {
  ....
   /**
         * Callback method to be invoked while the list view or grid view is being scrolled. If the
         * view is being scrolled, this method will be called before the next frame of the scroll is
         * rendered. In particular, it will be called before any calls to
         * {@link Adapter#getView(int, View, ViewGroup)}.
         *
         * @param view The view whose scroll state is being reported
         *
         * @param scrollState The current scroll state. One of
         * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
         */
        public void onScrollStateChanged(AbsListView view, int scrollState);

        /**
         * Callback method to be invoked when the list or grid has been scrolled. This will be
         * called after the scroll has completed
         * @param view The view whose scroll state is being reported
         * @param firstVisibleItem the index of the first visible cell (ignore if
         * visibleItemCount == 0)
         * @param visibleItemCount the number of visible cells
         * @param totalItemCount the number of items in the list adapter
         */
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                int totalItemCount);
 }
}

OnScrollListener的回撥方法onScroll的引數裡我們可以看到,這裡並沒有實際滾動了多少距離的引數變數,如果想統計實際滾動的距離,則需要自定義一個ScrollListener來處理,在接收到滾動回撥時進行自行處理。

2.2 統計方案

核心方案:通過第一個可見item的變化來統計判斷實際滑動的距離,離開時通過累加初始時可見item到離開時可見item的高度來統計實現

  • 第一次進來時(收到滾動回撥)時,記錄下此時第一個可見item的index 為 mInitPosition;
  • 每次收到滾動回撥時,更新已滾動的第一個可見item的 index,並記錄下第一個item的最大的index 為:mMaxPosition;
  • 每次收到滾動回撥時,根據第一個item的變化,記錄下當前已滾動的最大距離;
  • 每次回撥時,如果第一個item的最大index發生變化,則會累加上一個item的距離;
  • 離開時,通過 mMaxPosition 和 mInitPosition計算出當次滾動的最大距離;
//初次回撥時
mInitPosition = getFirstItemPosition();
.....
//其他回撥時
mCurPosition = getFirstItemPosition();
mMaxPosition = Max(mCurPosition,mMaxPosition);
.....

整個統計方案需要解決以下幾個關鍵問題:

  • 滾動不超過一個item時的距離統計;
  • 進來時停留在某一個item時的滾動距離統計;
  • 快速滑動時的距離的統計;

2.2.1 滾動不超過一個item時的統計

因為我們整體的方案是通過累加item的高度來判斷當前滾動了多少距離,大方案只能統計滾動剛好超過item時滾動距離,但如果滾動未超過一個item時,其滾動距離則不能累加item的高度來處理,比如:
滾動不超過一個item時的統計

實際滾動距離為紅色部分,並沒有超過一個item的高度,此時應該怎樣統計該部分的距離呢?這肯定沒有辦法直接通過item的高度來計算得到。這裡核心是通過系統提供的View的方法getTop來拿到該View最頂部距離其Parent的距離:

    /**
     * Top position of this view relative to its parent.
     *
     * @return The top of this view, in pixels.
     */
    @ViewDebug.CapturedViewProperty
    public final int getTop() {
        return mTop;
    }

在該item第一次變成第一個可見item時,記錄下此時通過getTop拿到的初始值:mInitTop ,在離開時,獲取當前停留的top值:mCurTop。在拿到這兩個階段的top值時,我們就可以通過p這兩個值來計算出紅色部分的實際滾動距離:

//這裡大家可以思考下為什麼可以通過減掉當前的top值就能獲取到當前實際滾動的距離的;
int itemHeight = mInitTop - mCurTop;

2.2.2 進來時停留在某一個item時的滾動距離統計;

如果是從當前頁面A跳到其他頁面B後,再跳轉回來,此時當前頁面A正常是停留在上一次瀏覽的位置(前提是頁面A未被回收掉),此時有可能是停留在某個位置上的,如圖:
進來時滾動位置
此時向下滾動時,item1的滾動距離為紅色部分,這部分的距離可以怎樣計算得到呢?在進入該頁面時,我們通過該itemView的getTop方法拿到的初始值:mInitTop,該值的絕對值就為橙色部分的高度。而 橙色部分高度 + 紅色部分高度 = 該item的實際高度,進而我們可以通過item的高度 - 橙色部分高度來得到紅色部分的高度:

//進來時,記錄下該item的初始top
mInitTop = item1View.getTop();
.......
//item1的實際滾動距離scrollDistance
int scrollDistance = item1View.getHeight() + mInitTop;

2.2.3 快速滑動時的距離的統計

ListView在快速滑動時的滾動回撥並不會每次都回撥給註冊了滾動監聽的物件,有可能是隔幾次才會回撥一次,這樣會導致我們在收到滾動回撥時時記錄的當前最大滾動距離不準?這裡有沒有辦法相容快速滑動這種場景下的統計?筆者在實踐中採用了一種補償機制的方案:

  • 記錄下當前可見頁面的所有item的高度;
  • 每次更新最大滾動距離時,同步記錄下已更新到最大滾動距離的itemIndex;
  • 最終獲取最大滾動距離時,會判斷是否有漏掉item的高度,如果有漏掉item,則會記錄的所有item的高度進行一次補償;
//記錄下最大滾動距離裡記錄的itemIndex;
private List<Integer> mFistVisibleItem = new ArrayList<>();
//記錄下當前所有item的高度情況
private SparseIntArray mItemHeight = new SparseIntArray();

最終獲取時會根據是否有漏掉記錄,根據記錄的mItemHeight的值進行一個補償:

boolean isMissing(){
int count = mMaxPosition -mInitPosition;
if (mFistVisibleItem.size() < count) {
    return true;
}
return false;
}

2.3 使用

實際使用時,我們需要把自定義的ScrollListener設定給對應的ListView就能統計到具體的滾動距離:

ListView mList = findViewById(R.id.list_view);
mList.setOnScrollListener(new ScrollListener());

3 總結

本文從實際使用的場景出發,提出了一個可記錄ListView滾動距離的實際方案,該方案可精確統計各種場景下ListView的實際滾動距離,併相容了常見的邊界統計的問題。是目前可直接運用於實際的生產環境的最優方案,沒有之一,就是這麼自信的。

相關文章