天天看點

【Android】掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。概述一 常見誤區、問題、注意事項:二 常用API:

轉載請标明出處:

http://blog.csdn.net/zxt0601/article/details/52948009

本文出自:【張旭童的部落格】

本系列文章相關代碼傳送門:

自定義LayoutManager實作的流式布局

歡迎star,pr,issue。

本系列文章目錄:

掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。

掌握自定義LayoutManager(二) 實作流式布局

概述

這篇文章是深入掌握自定義LayoutManager系列的開篇,是一份總結報告。部分内容不屬于引言、過于深入,用作系列後續文章的參考,以及浏覽完後的複習之用。

本文内容涉及RecyclerView、LayoutManager、RecyclerViewPool、Recycler。

注:

1 以下問題,初學者如有不了解的,可以不用太在意,等學習完自定義LayoutManager相關知識,寫幾個Demo再回來看更好了解。

2 在RecyclerView中,ItemView和ViewHolder其實是一一綁定的,是以提到的View = ViewHolder。

一 常見誤區、問題、注意事項:

在自定義LayoutManager文章開始之前,我總結了一些我在學習以及閱讀别人的文章、編碼的過程中,遇到的一些疑惑問題,并附上我個人的了解與答案。歡迎拍磚讨論。

因網上有大量半吊子寫的LayoutManager相關的中文文章。(包括我也是半吊子),是以很多文章看完了,心中都有N個疑問,如,作者好牛逼啊,但是為什麼我獨立寫還是寫不出來。 自定義一個LayoutManager就自動複用了嗎?…等等,下面逐個來講講。

Q1 看完了,但是我獨立寫還是不知道怎麼寫。

A1: 自定義LayoutManager是一項頗有難度的工程,你很難僅僅閱讀一兩篇文章,花兩三個小時就能學習完。

裡面涉及到子View的布局,坐标的計算,偏移量的計算,在滑動時、在合适的時機回收螢幕上不再顯示的View,如何判斷這些View是在螢幕上不可見,以及View究竟是暫時detach掉,還是recycle回收掉…等大量問題

。老實說,也許我水準有限,這是我在學習Android過程中,耗時最久的幾個知識點之一。(十幾個小時才寫出第一個及格的作品)

但是它值得你學習。是以獨立寫不出來别灰心,先仿照一個Demo寫一寫,如果用心了解,第二遍第二遍應該就可以獨立完成了。

Q2 學習自定義LayoutManager需要的鋪墊知識

一 :熟練掌握自定義ViewGroup。

(在自定義LayoutManager過程的第一步,

onLayoutChildren()

方法裡,就類似于自定義ViewGroup的onLayout()方法。)

但與自定義LayoutManager相比,自定義ViewGroup是一種靜态的layout 子View的過程,因為ViewGroup内部不支援滑動,是以隻需要無腦layout出所有的View,便不用再操心剩下的事。

而自定義LayoutManager與之不同,在第一步layout時,千萬不要layout出所有的子View,這裡也是網上一些文章裡的錯誤做法,他們帶着老思想,在第一步就layout出了所有的childView,這會導緻一個很嚴重的問題:你的自定義LayoutManager = 自定義ViewGroup。即,他們沒有View複用機制。

why?這裡簡單證明結論,在Q5的回答裡會說明為什麼。

在Adapter的onCreateViewHolder()方法裡增加列印語句,如果你的資料源有100000條資料,那麼在RecyclerView第一次顯示在螢幕上時,onCreateViewHolder()會執行100000次,你就可以盡情的欣賞ANR了。

反觀使用官方提供的三種LayoutManager,開始時螢幕上有n少個ItemView,一般就執行n次onCreateViewHolder(),(也有可能多執行1次),在後續滑動時,大部分情況都隻是執行onBindViewHolder()方法,不會再執行onCreateViewHolder()。

二 : 熟練使用RecyclerView。這個不用多說,畢竟RecyclerView是LayoutManager的宿主。

其實會以上兩點就可以開始我們的學習之旅了,不過如果能對RecyclerView的Adapter、RecyclerViewPool、ItemDecoration也有一定的了解那是最好。

Q3 自定義LayoutManager的實戰場景多嗎?

A3:實戰場景還是相當有限的。系統自帶的三個LayoutManager已經很夠用,滿足絕大部分需求。

我個人從學習自定義LayoutManager至今的收獲 ,大部分是對RecyclerView機制的了解進一步加深,也會伴随一定量的源碼閱讀經驗提升。随沒有我想象中的提升巨大生産力的趕腳,因為很多時候,産品設計要求的布局,現有方案已經可以很好解決。

但是它值得學習。

Q4 自定義一個LayoutManager就自動複用ItemView了嗎?

A4:不是,實際上這是自定義LayoutManager的重頭戲之一,要做到在合适的時機回收 不可見的舊子View ,複用子View layout 新的子View,以及Q2提及的在LayoutManager的初始化時合理布局可見數量的子View等,才算是複用了ItemView。

注意,這裡的回收是recycle,而不是detach。

如果你隻detach了ItemView,并沒有recycle它們,它們會一直被儲存在Recycler的

mAttachedScrap

裡,它是一個ArrayList,儲存了被detach但還沒有recycle的ViewHolder。

public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
           

(實際上Recycler内部的緩存機制遠不止一個mAttachedScrap 。)

Q5 用RecyclerView就等于ItemView複用?

A5:顯然也不是。除了Q4的因素外,這裡還有一個很大的誤區:很多人認為使用了RecyclerView,ItemView就都回收複用了。

這裡出個題:基本上APP都有個TopBanner在,它放在RecyclerView裡作為HeaderView(通過特殊的ItemViewType實作),剩下都是普通的ItemView,那麼清單滾動,當Banner早已不可見時,它的View(ViewHolder)會被回收、被其他ItemView複用嗎?

如下圖:

【Android】掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。概述一 常見誤區、問題、注意事項:二 常用API:

答案:Banner的ViewHolder 會被回收,但該ViewHolder的記憶體空間 不會被釋放 , 不會被其他的ItemView複用。

回收都好了解,在螢幕上不可見時,LayoutManager會把它回收至RecyclerViewPool裡。

然而卻不會給normalItem複用,因為它們的ItemViewType不同。

是以它的記憶體空間不會被釋放,将一直被RecyclerViewPool持有着,等待着需求相同ItemViewType的ViewHolder的請求到來。

即,當頁面滾動回頂部,顯示Banner時,這個View會被複用。

先說為什麼,再說如何去驗證。

為什麼?

這涉及到Recycler、RecyclerViewPool的知識,(小安利,我在http://blog.csdn.net/zxt0601/article/details/52267325 這篇文章的第四節裡對RecyclerViewPool的源碼進行過全解,不過大家也可以自己去檢視,源碼很短。)

在LayoutManager裡,擷取childView是通過如下方法得到:

該方法内部,先通過position去擷取是否有detach掉的scrapView(ViewHolder),

如果沒有則根據position去擷取itemViewType,

final int type = mAdapter.getItemViewType(offsetPosition);
           

根據itemViewType擷取在RecyclerViewPool裡是否有該ViewHolder,

這裡由于我們的Banner的viewType和normalItem的viewType不一樣,即使Banner被回收進了RecyclerViewPool,但是由于itemViewtype和普通的ItemView不同,它也無法被取出、進而複用,(發散一下,另外一點,它也無法被釋放,被強引用在記憶體裡,http://blog.csdn.net/zxt0601/article/details/52267325 這篇文章有詳細分析)。

再往下由于holder還是空的,最終便會調用Adapter的onCreateViewHolder()方法create一個新的ViewHolder。

驗證:

感興趣的人去重寫任意Adapter的

getItemViewType()

方法:

@Override
            public int getItemViewType(int position) {
                return position;
            }
           

這樣每一個ItemViewType都不一樣,RecyclerView不會有任何的複用,因為每一個ItemView在RecyclerViewPool裡都找不到可以複用的holder,ItemView有n個,onCreateViewHolder方法會執行n次。

看到這裡就能回答Q2一的問題:

因為在初始化時,Recycler(scrapCache)和RecyclerViewPool裡的緩存都是空的,是以此時得到的ViewHolder都是通過onCreateViewHolder(),new 出的ViewHolder。如果此時get了整個itemCount數量的View,那麼也會new出itemCount數量的ViewHolder,此時這些ViewHolder都存在記憶體裡,和普通ViewGroup毫無分别,也更容易OOM。

Q6 RecyclerView的緩存機制簡述

A6: 上面BB了這麼多,涉及到Recycler、RecyclerViewPool以及scrap,detach,remove,recycle等概念。

【Android】掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。概述一 常見誤區、問題、注意事項:二 常用API:

這張圖摘自(http://kymjs.com/code/2016/07/10/01),源頭應該是Google官方的視訊裡。

我了解圖上的cache是被detach掉的ViewHolder存放的區域,即scrapCache區域。

這個區域由

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        ArrayList<ViewHolder> mChangedScrap = null;

        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
           

這三個ArrayList組成。

而被remove掉的ViewHolder會按照ViewType分組被存放在RecyclerViewPool裡,預設最大緩存每組(ViewType)5個。

private SparseArray<ArrayList<ViewHolder>> mScrap =
                new SparseArray<ArrayList<ViewHolder>>();
           

Q7 detach 和recycle的時機。

一個View隻是暫時被清除掉,稍後立刻就要用到,使用detach。它會被緩存進scrapCache的區域。

一個View 不再顯示在螢幕上,需要被清除掉,并且下次再顯示它的時機目前未知 ,使用remove。它會被以viewType分組,緩存進RecyclerViewPool裡。

注意:一個View隻被detach,沒有被recycle的話,不會放進RecyclerViewPool裡,會一直存在recycler的scrap 中。網上有人的Demo就是如此,是以View也沒有被複用,有多少ItemCount,就會new出多少個ViewHolder。

Q8 初始化時,onLayoutChildren()為什麼會執行兩次?

答 :參看RecyclerView源碼,onLayoutChildren 會執行兩次,一次RecyclerView的onMeasure() 一次onLayout()。

李菊福:RecyclerView的onMeasure(),會調用

dispatchLayoutStep2()

方法,該方法内部會調用

mLayout.onLayoutChildren(mRecycler, mState);

,這是第一次。如下:

@Override
    protected void onMeasure(int widthSpec, int heightSpec) {
            ......
            dispatchLayoutStep2();
            ......
    }
           
/**
     * The second layout step where we do the actual layout of the views for the final state.
     * This step might be run multiple times if necessary (e.g. measure).
     */
    private void dispatchLayoutStep2() {
        .....
        mLayout.onLayoutChildren(mRecycler, mState);
        .....
    }
           

onLayout()方法會調用

dispatchLayout();

,該方法内部又調用了

dispatchLayoutStep2();

,這是第二次。

Q9 基于上個問題,我們要注意什麼?

答:即使是在寫onLayoutChildren()方法時,也要考慮将螢幕上的View(如果有),detach掉,否則螢幕初始化時,同一個position的ViewHolder,也會onCreateViewHolder兩次。是以childCount也會翻倍。

最後也是最重要的

LayoutManager API 支援強大且複雜的布局回收,正因為它API強大,是以我們需要實作大量的代碼才能完成功能。不要過度封裝、過度優化你的代碼,隻要能完成你的需求即可。(當然最基本的要求:ViewHolder複用 要滿足)

原話如下:

【Android】掌握自定義LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。概述一 常見誤區、問題、注意事項:二 常用API:

文章連結:http://wiresareobsolete.com/2014/09/building-a-recyclerview-layoutmanager-part-1/

該文章是我見過學習自定義LayoutManager最好的資料。

二 常用API:

布局API:

//找recycler要一個childItemView,我們不管它是從scrap裡取,還是從RecyclerViewPool裡取,亦或是onCreateViewHolder裡拿。
View view = recycler.getViewForPosition(xxx);  //擷取postion為xxx的View
           
addView(view);//将View添加至RecyclerView中,
addView(child, );//将View添加至RecyclerView中,childIndex為0,但是View的位置還是由layout的位置決定,該方法在逆序layout子View時有大用
           
//将ViewLayout出來,顯示在螢幕上,内部會自動追加上該View的ItemDecoration和Margin。此時我們的View已經可見了
layoutDecoratedWithMargins(view, leftOffset, topOffset,
                        leftOffset + getDecoratedMeasuredWidth(view),
                        topOffset + getDecoratedMeasuredHeight(view));
           

回收API:

detachAndScrapAttachedViews(recycler);//detach輕量回收所有View
detachAndScrapView(view, recycler);//detach輕量回收指定View

// recycle真的回收一個View ,該View再次回來需要執行onBindViewHolder方法
removeAndRecycleView(View child, Recycler recycler)
removeAndRecycleAllViews(Recycler recycler);
           
detachView(view);//超級輕量回收一個View,馬上就要添加回來
attachView(view);//将上個方法detach的View attach回來
recycler.recycleView(viewCache.valueAt(i));//detachView 後 沒有attachView的話 就要真的回收掉他們
           

移動子ViewAPI:

offsetChildrenVertical(-dy); // 豎直平移容器内的item 
offsetChildrenHorizontal(-dx);//水準平移容器内的item
           

工具API:

public int getPosition(View view)//擷取某個view 的 layoutPosition,很有用的方法,卻鮮(沒)有文章提及,是我翻看源碼找到的。
           
//以下方法會我們考慮ItemDecoration的存在,但部分函數沒有考慮margin的存在
getDecoratedLeft(view)=view.getLeft()
getDecoratedTop(view)=view.getTop()
getDecoratedRight(view)=view.getRight()
getDecoratedBottom(view)=view.getBottom()
getDecoratedMeasuredHeight(view)=view.getMeasuredWidth()
getDecoratedMeasuredHeight(view)=view.getMeasuredHeight()
           
//由于上述方法沒有考慮margin的存在,是以我參考LinearLayoutManager的源碼:
    /**
     * 擷取某個childView在水準方向所占的空間
     *
     * @param view
     * @return
     */
    public int getDecoratedMeasurementHorizontal(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getDecoratedMeasuredWidth(view) + params.leftMargin
                + params.rightMargin;
    }

    /**
     * 擷取某個childView在豎直方向所占的空間
     *
     * @param view
     * @return
     */
    public int getDecoratedMeasurementVertical(View view) {
        final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                view.getLayoutParams();
        return getDecoratedMeasuredHeight(view) + params.topMargin
                + params.bottomMargin;
    }