【Android】深入掌握自訂LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。

來源:互聯網
上載者:User

標籤:靜態   重要   技術   情境   normal   hand   csdn   nes   .net   

轉載請標明出處:
http://blog.csdn.net/zxt0601/article/details/52948009
本文出自:【張旭童的部落格】

本系列文章相關代碼傳送門:
自訂LayoutManager實現的流式布局
歡迎star,pr,issue。

本系列文章目錄:
深入掌握自訂LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。
深入掌握自訂LayoutManager(二) 實現流式布局(creating)

概述

這篇文章是深入掌握自訂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複用嗎?
如:

答案: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是通過如下方法得到:

View child = recycler.getViewForPosition(i);

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

holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);

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

final int type = mAdapter.getItemViewType(offsetPosition);

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

holder = getRecycledViewPool().getRecycledView(type);

這裡由於我們的Banner的viewType和normalItem的viewType不一樣即使Banner被回收進了RecyclerViewPool,但是由於itemViewtype和普通的ItemView不同,它也無法被取出、從而複用,(發散一下,另外一點,它也無法被釋放,被強引用在記憶體裡,http://blog.csdn.net/zxt0601/article/details/52267325 這篇文章有詳細分析)。
再往下由於holder還是空的,最終便會調用Adapter的onCreateViewHolder()方法create一個新的ViewHolder。

`holder = mAdapter.createViewHolder(RecyclerView.this, type);`
驗證:

感興趣的人去重寫任意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等概念。

這張圖摘自(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複用 要滿足
原話如下:

文章連結: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, 0);//將View添加至RecyclerView中,childIndex為0,但是View的位置還是由layout的位置決定,該方法在逆序layout子View時有大用
measureChildWithMargins(scrap, 0, 0);//測量View,這個方法會考慮到View的ItemDecoration以及Margin
//將ViewLayout出來,顯示在螢幕上,內部會自動追加上該View的ItemDecoration和Margin。此時我們的View已經可見了layoutDecoratedWithMargins(view, leftOffset, topOffset,                        leftOffset + getDecoratedMeasuredWidth(view),                        topOffset + getDecoratedMeasuredHeight(view));
回收API:
detachAndScrapAttachedViews(recycler);//detach輕量回收所有ViewdetachAndScrapView(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;    }

【Android】深入掌握自訂LayoutManager(一) 系列開篇 常見誤區、問題、注意事項,常用API。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.