FormLayoutManager -- 解說(1)

FormLayoutManager首頁,裏面有github地址

目錄

前言

構造方法

onLayoutChildren

handleLayoutChildren

總結


前言

接下來會一步步帶大家走進FormLayoutManager。在寫這篇博客之前,我已經實現可以不同行或不同列多類型佈局。所以下面講解是以最新的v2.0代碼來說明,後面也會介紹到怎麼實現多類型。FormLayoutManager的完整代碼有點長,我就不貼出來,大家可以自己看着源碼,一邊看文章,我們由上至下來說。

構造方法

    public FormLayoutManager(int columnCount){
        mColumnCount = columnCount;
    }

    public FormLayoutManager(boolean isHorV, int count){
        this(isHorV, count, null);
    }

    /**
     * 什麼場景需要傳入RecyclerView
     * 在滾動過程會刷新的數據的時候,最好設置RecyclerView
     */
    public FormLayoutManager(int columnCount, RecyclerView recyclerView){
        this(true, columnCount, recyclerView);
    }

    public FormLayoutManager(boolean isHorV, int count, RecyclerView recyclerView){
        mIsHorV = isHorV;
        if (isHorV){
            mColumnCount = count;
        }else{
            mRowCount = count;
        }
        mRecyclerView = recyclerView;
    }

isHorV是標誌是水平表格還是垂直表格。默認是大家常用的水平表格,最基本的構造方法就是要傳入列數。也提供一個構造方法讓大家設置isHorV,來告訴FormLayoutManager你的表格水水平表格還是垂直表格,而當時垂直表格時,傳入的count就代表列數了。

    private int getColumnCount() {
        if (mIsHorV){
            return mColumnCount;
        }else{
            return (getItemCount() - 1) / mRowCount + 1;
        }
    }

目光來到getColumnCount,當爲水平表格時,列數就是我們構造函數傳進來的那個columnCount的值。而當它爲垂直表格的時候,我們列數是要根據行數計算。行數的getRowCount同理。

目光請有回到構造函數那裏,其中有兩個方法是要傳入RecyclerView。註釋有解析到如果你的表格在你滾動情況下也會突然刷新數據,建議設置一下RecyclerView,因爲刷新數據的時候,我們希望表格刷新的是當前可以看到的view,用了RecyclerView來執行的刷新方法比較快一點,後面再細說。

onLayoutChildren

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (mItemCount != getItemCount()){
            mItemCount = 0;
        }
        if (mItemCount == 0 || mRecyclerView == null){
            mItemCount = getItemCount();
            handleLayoutChildren(recycler);
        }else{
            // 防止數據在更新的時候,用戶又在滑動表格,這時會看到卡頓現象
            // 第二種刷新當前界面可視的view,不過要設置RecyclerView,但刷新時間短
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                int position = getPosition(child);
                mRecyclerView.getAdapter().onBindViewHolder(mRecyclerView.getChildViewHolder(child), position);
            }
        }
    }

我們先來看else下面的方法,這段代碼就是整個FormLayoutManager裏面唯一一段用到RecyclerView的代碼。這段代碼很好理解,就是遍歷界面可視的item數getChildCount,然後逐個調用onBindViewHolder來實現刷新該item。其實if下面的handleLayoutChildren裏面也是隻會刷新當前可視的item,所以整個FormLayoutManager就算你不設置RecyclerView,是一樣可以正常用的。我只是考慮,但表格的總item數不變且要刷新數據的時候,沒必要執行一個handleLayoutChildren這麼龐大的一段邏輯,直接用else下面的方法更快。

        if (mItemCount != getItemCount()){
            mItemCount = 0;
        }

上面這個讓mItemCount歸0是幹什麼的呢?因爲要執行handleLayoutChildren的判斷條件是mItemCount等於0,而handleLayoutChildren裏面初始化了好一些變量,所以當我們刷新數據,導致總item的數目發生變化的時候,必須要調用handleLayoutChildren來重新計算初始化那些變量。

handleLayoutChildren

1、計算每個item的Rect

接下來看一下handleLayoutChildren裏面第一個大的for循環幹了什麼

         for (int i = 0; i < getItemCount(); i++) {
            // item所在的行和列的index
            int row = i / mColumnCount;
            int column = i % mColumnCount;

            View itemView = recycler.getViewForPosition(i);
            Integer itemViewType = getItemViewType(itemView);
            int itemWidth;
            int itemHeight;
            if (mItemViewSizeMap.containsKey(itemViewType)){
                itemWidth = mItemViewSizeMap.get(itemViewType).width;
                itemHeight = mItemViewSizeMap.get(itemViewType).height;
            }else{
                measureChildWithMargins(itemView, 0, 0);
                itemWidth = getDecoratedMeasuredWidth(itemView);
                itemHeight = getDecoratedMeasuredHeight(itemView);
                mItemViewSizeMap.put(itemViewType, new ItemViewSize(itemWidth, itemHeight));
            }

            Rect rect = getViewRect(row, column, itemWidth, itemHeight);
            mItemRects.add(rect);
            mHasAttachedItems.add(false);
        }

上面是遍歷了總item的個數,之前沒做多類型表(多類型表指不同行或不同列多類型)的時候,獲取itemWidth和itemHeight非常簡單,直接拿position爲0的第一個item的寬高就行了,因爲沒多類型,故每個格子的寬高都一樣。這個新寫的for循環是考慮到多類型的情況。

LayoutManager自帶有個getItemViewType獲取item類型的,然後我們有個字典mItemViewSizeMap來保存不同類item的寬高。最關鍵是後面getViewRect,我先說一下mItemRects裏面保存的是什麼,這個列表裏面保存的rect,其實是在不滾動的情況下,所有item(不管可視還是不可視)在界面的位置。之前沒多類型,每個格子寬高一樣時候,每個item的rect很好算的,就是按循環累加一下itemWidth或itemHeight就行了,現在多了多類型,就寫了一個getViewRect的方法,下面我們看一下里面寫了什麼。

    // 獲取view對應的Rect
    private Rect getViewRect(int row, int column, int itemWidth, int itemHeight) {
        int left = 0;
        int right = itemWidth;
        int top = 0;
        int bottom = itemHeight;

        if (mItemRects.size() > 0){
            Rect lastRect = mItemRects.get(mItemRects.size() - 1);
            if (mCurRow != row){
                mCurRow = row;
                left = 0;
                right = itemWidth;
                top = lastRect.bottom;
                bottom = top + itemHeight;
            }else if (mCurColumn != column){
                mCurColumn = column;
                left = lastRect.right;
                right = left + itemWidth;
                top = lastRect.top;
                bottom = lastRect.bottom;
            }
        }

        Rect rect = new Rect(left, top, right, bottom);

        return rect;
    }

上面可以看到當mItemRects.size() > 0纔會執行一段計算,當它不大於0的時候,其實就是在計算第一個item的rect,它的對應屬性其實就是一開始的默認值

        int left = 0;
        int right = itemWidth;
        int top = 0;
        int bottom = itemHeight;

而其他的rect就要通過if下面的代碼計算了。先跟大家說明一下我們的所有item要是描繪在整個表格,它是按什麼順序把item加進去。答案就是從左到右,然後上到下,就是一個個item從左到右地加進去,一行加滿就向下換行,繼續左到右。好,那麼就來看if下面的代碼了。

            if (mCurRow != row){
                mCurRow = row;
                left = 0;
                right = itemWidth;
                top = lastRect.bottom;
                bottom = top + itemHeight;
            }

當mCurRow不等於row,即當前行不等於傳進來的行時,證明開始換行,那換行的第一個item,好明顯left就是0,right就是itemWidth啦。而關鍵的top其實就是上一個rect的bottom了,上一個的rect其實就是它的上一行的最後一個item的rect,而bottom是top加itemHeight就不多說了。那接下來

            else if (mCurColumn != column){
                mCurColumn = column;
                left = lastRect.right;
                right = left + itemWidth;
                top = lastRect.top;
                bottom = lastRect.bottom;
            }

else if  /—~—/,沒錯就是else if,當行相等的時候纔會進入,然後判斷列是否相等,不相等就說明換列,因爲是同行換列,故left就是上一個rect的right,而right就不多說了,top和bottom跟上一個rect的一樣。這樣就準確記錄了每個item的rect了。

 

2、繪製每個可視的item

        here:
        for (int row = firstShowRow; row < visibleRowCount + firstShowRow; row++) {
            for (int column = firstShowCol; column < visibleColumnCount + firstShowCol; column++) {
                int itemPosition = row * mColumnCount + column;

                if (itemPosition >= mItemRects.size()){
                    break here;
                }
                Rect rect = mItemRects.get(itemPosition);
                if (!Rect.intersects(getVisibleArea(), rect)){
                    continue;
                }
                View view = recycler.getViewForPosition(itemPosition);
                addView(view);
                //addView後一定要measure,先measure再layout
                measureChildWithMargins(view, 0, 0);
                layoutDecorated(view, rect.left - mSumDx, rect.top - mSumDy, rect.right - mSumDx, rect.bottom - mSumDy);
            }
        }

firstShowRow, firstShowCol, visibleColumnCount, visibleRowCount這四個值怎麼得來,前面幾段代碼,我相信你能看明白。我主要說一下我貼出來的這段代碼。爲什麼要獲取第一個可視item的行和列呢(firstShowRow和firstShowCol),就是因爲這兩個值,才讓handleLayoutChildren每次刷新,也只會刷新當前可視的item。

兩個for循環爲了遍歷的就是可視區的所有item,我們着重看一下循環裏面的代碼,第一句其實就是把行和列轉換成該item對應的position。先跳開兩個判斷,淺談下面繪製view的代碼,很明確,就是在recycler,那裏根據itemPosition獲取對應的itemView,然後調layoutDecorated繪製到RecyclerView上。

第一個判斷,爲什麼itemPosition會可能大於mItemRects.size()。我們來看一下visibleColumnCount, visibleRowCount怎麼算的。

        // 可視的最大行數,列數
        int visibleRowCount = (int) Math.ceil(getVerticalSpace() * 1f / minHeight) + 1;
        int visibleColumnCount = (int) Math.ceil(getHorizontalSpace() * 1f / minWidth) + 1;

拿可視最大列數作分析,看下面左圖,我們那RecyclerView可視的寬getHorizontalSpace除以item的寬,得到的結果應該是2點多,然後我們向上取整Math.ceil得到的就是3,但往往這個結果都要加1,。因爲只要你左滑一下就可以發現,最多可視的列其實是4列,如右圖

                                      

關鍵是我拿來計算的不是item的width,而是所有item最小的minWidth(minWidth怎麼得到的,看源碼很容易理解的)。之前沒多類型的時候,每個格子寬高一樣,那個可視最大數是算得很準的。但由於加了多類型,那就是能考慮極端的情況,假設所有item都是最小那個width來計算可視最大數。

回到我們的兩個for循環,因此visibleColumnCount, visibleRowCount可能會比實際的大,所以兩個for循環所遍歷的itemPosition會比實際表格的itemPosition大,有可能會越界,所以纔有第一個判斷出現,當這個itemPosition已經超過表格最大的那個position,我們就直接跳出兩個循環。接下來看第二個判斷:

                Rect rect = mItemRects.get(itemPosition);
                if (!Rect.intersects(getVisibleArea(), rect)){
                    continue;
                }

大家可以打開demo看第一個界面,第一個界面的表格上面的數字其實就是格子對應的itemPosition。

看圖,不難發現可視區爲起始position=16,4X7的一個表,而上面我們說了,兩個for循環遍歷的範圍比我們可視的還大。比如兩個for循環遍歷可能會出現position=20,但這個格子根本就不在我們的可視區,所以存在第二個判斷,當判斷這個position對應的rect不在我們的可視區的時候,就繼續contiunue。

handleLayoutChildren的內容基本說完了,接下來總結一下這篇博客。

總結

FormLayoutManager內容太多了,所以分幾篇來說明。這篇主要還是說了onLayoutManager這個方法的重寫。而我們的表格進行了什麼操作纔會進入onLayoutManager呢?當我們的RecyclerView從gone到可視,還有adapter數據刷新的時候,都會進這方法。這個方法尤其重要,是FormLayoutManager的主入口來的。

下一篇:FormLayoutManager -- 解說(2)還沒寫……

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