編輯:關於Android編程
這篇文章是深入掌握自定義LayoutManager系列的開篇,是一份總結報告。部分內容不屬於引言、過於深入,用作系列後續文章的參考,以及浏覽完後的復習之用。
本文內容涉及RecyclerView、LayoutManager、RecyclerViewPool、Recycler。
注:
1 以下問題,初學者如有不理解的,可以不用太在意,等學習完自定義LayoutManager相關知識,寫幾個Demo再回來看更好理解。
2 在RecyclerView中,ItemView和ViewHolder其實是一一綁定的,所以提到的View = ViewHolder。
在自定義LayoutManager文章開始之前,我總結了一些我在學習以及閱讀別人的文章、編碼的過程中,遇到的一些疑惑問題,並附上我個人的理解與答案。歡迎拍磚討論。
因網上有大量半吊子寫的LayoutManager相關的中文文章。(包括我也是半吊子),所以很多文章看完了,心中都有N個疑問,如,作者好牛逼啊,但是為什麼我獨立寫還是寫不出來。 自定義一個LayoutManager就自動復用了嗎?…等等,下面逐個來講講。
A1: 自定義LayoutManager是一項頗有難度的工程,你很難僅僅閱讀一兩篇文章,花兩三個小時就能學習完。
裡面涉及到子View的布局,坐標的計算,偏移量的計算,在滑動時、在合適的時機回收屏幕上不再顯示的View,如何判斷這些View是在屏幕上不可見,以及View究竟是暫時detach掉,還是recycle回收掉…等大量問題
。老實說,也許我水平有限,這是我在學習Android過程中,耗時最久的幾個知識點之一。(十幾個小時才寫出第一個及格的作品)
但是它值得你學習。所以獨立寫不出來別灰心,先仿照一個Demo寫一寫,如果用心理解,第二遍第二遍應該就可以獨立完成了。
(在自定義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的Adapter、RecyclerViewPool、ItemDecoration也有一定的了解那是最好。
A3:實戰場景還是相當有限的。系統自帶的三個LayoutManager已經很夠用,滿足絕大部分需求。
我個人從學習自定義LayoutManager至今的收獲 ,大部分是對RecyclerView機制的理解進一步加深,也會伴隨一定量的源碼閱讀經驗提升。隨沒有我想象中的提升巨大生產力的趕腳,因為很多時候,產品設計要求的布局,現有方案已經可以很好解決。
但是它值得學習。
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 ArrayListmAttachedScrap = new ArrayList<>();
(實際上Recycler內部的緩存機制遠不止一個mAttachedScrap 。)
A5:顯然也不是。除了Q4的因素外,這裡還有一個很大的誤區:很多人認為使用了RecyclerView,ItemView就都回收復用了。 這涉及到Recycler、RecyclerViewPool的知識,(小安利,我在http://blog.csdn.net/zxt0601/article/details/52267325 這篇文章的第四節裡對RecyclerViewPool的源碼進行過全解,不過大家也可以自己去查看,源碼很短。) 該方法內部,先通過position去獲取是否有detach掉的scrapView(ViewHolder), 如果沒有則根據position去獲取itemViewType, 根據itemViewType獲取在RecyclerViewPool裡是否有該ViewHolder, 這裡由於我們的Banner的viewType和normalItem的viewType不一樣,即使Banner被回收進了RecyclerViewPool,但是由於itemViewtype和普通的ItemView不同,它也無法被取出、從而復用, 感興趣的人去重寫任意Adapter的getItemViewType()方法: 這樣每一個ItemViewType都不一樣,RecyclerView不會有任何的復用,因為每一個ItemView在RecyclerViewPool裡都找不到可以復用的holder,ItemView有n個,onCreateViewHolder方法會執行n次。 看到這裡就能回答Q2一的問題: A6: 上面BB了這麼多,涉及到Recycler、RecyclerViewPool以及scrap,detach,remove,recycle等概念。 這三個ArrayList組成。 一個View只是暫時被清除掉,稍後立刻就要用到,使用detach。它會被緩存進scrapCache的區域。 答 :參看RecyclerView源碼,onLayoutChildren 會執行兩次,一次RecyclerView的onMeasure() 一次onLayout()。 李菊福:RecyclerView的onMeasure(),會調用dispatchLayoutStep2()方法,該方法內部會調用 mLayout.onLayoutChildren(mRecycler, mState); ,這是第一次。如下: onLayout()方法會調用dispatchLayout();,該方法內部又調用了dispatchLayoutStep2();,這是第二次。 答:即使是在寫onLayoutChildren()方法時,也要考慮將屏幕上的View(如果有),detach掉,否則屏幕初始化時,同一個position的ViewHolder,也會onCreateViewHolder兩次。因此childCount也會翻倍。 LayoutManager API 支持強大且復雜的布局回收,正因為它API強大,所以我們需要實現大量的代碼才能完成功能。不要過度封裝、過度優化你的代碼,只要能完成你的需求即可。(當然最基本的要求:ViewHolder復用 要滿足)
這裡出個題:基本上APP都有個TopBanner在,它放在RecyclerView裡作為HeaderView(通過特殊的ItemViewType實現),剩下都是普通的ItemView,那麼列表滾動,當Banner早已不可見時,它的View(ViewHolder)會被回收、被其他ItemView復用嗎?
如下圖:
答案:Banner的ViewHolder 會被回收,但該ViewHolder的內存空間 不會被釋放 , 不會被其他的ItemView復用。
回收都好理解,在屏幕上不可見時,LayoutManager會把它回收至RecyclerViewPool裡。
然而卻不會給nZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcm1hbEl0ZW24tNPDo6zS8s6qy/zDx7XESXRlbVZpZXdUeXBlsrvNrDwvc3Ryb25nPqGjPGJyIC8+DQrL+dLUy/y1xMTatOa/1bzksru74bG7ys23xaOsvavSu9axsbtSZWN5Y2xlclZpZXdQb29ss9bT0NfFo6y1yLT918XQ6Mfzz+DNrEl0ZW1WaWV3VHlwZbXEVmlld0hvbGRlcrXEx+vH87W9wLShozxiciAvPg0KvLSjrLWx0rPD5rn2tq+72Lalsr+jrM/Uyr5CYW5uZXLKsaOs1eK49lZpZXe74bG7uLTTw6GjPGJyIC8+DQrPyMu1zqrKssO0o6zU2cu1yOe6zsil0enWpKGjPC9jb2RlPjwvY29kZT48L3A+DQo8aDMgaWQ9"為什麼">為什麼?
在LayoutManager裡,獲取childView是通過如下方法得到:
View child = recycler.getViewForPosition(i);
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
final int type = mAdapter.getItemViewType(offsetPosition);
holder = getRecycledViewPool().getRecycledView(type);
再往下由於holder還是空的,最終便會調用Adapter的onCreateViewHolder()方法create一個新的ViewHolder。
`holder = mAdapter.createViewHolder(RecyclerView.this, type);`
驗證:
@Override
public int getItemViewType(int position) {
return position;
}
因為在初始化時,Recycler(scrapCache)和RecyclerViewPool裡的緩存都是空的,所以此時得到的ViewHolder都是通過onCreateViewHolder(),new 出的ViewHolder。如果此時get了整個itemCount數量的View,那麼也會new出itemCount數量的ViewHolder,此時這些ViewHolder都存在內存裡,和普通ViewGroup毫無分別,也更容易OOM。Q6 RecyclerView的緩存機制簡述
這張圖摘自(http://kymjs.com/code/2016/07/10/01),源頭應該是Google官方的視頻裡。
我理解圖上的cache是被detach掉的ViewHolder存放的區域,即scrapCache區域。
這個區域由
final ArrayList
而被remove掉的ViewHolder會按照ViewType分組被存放在RecyclerViewPool裡,默認最大緩存每組(ViewType)5個。
private SparseArray> mScrap =
new SparseArray>();
Q7 detach 和recycle的時機。
一個View 不再顯示在屏幕上,需要被清除掉,並且下次再顯示它的時機目前未知 ,使用remove。它會被以viewType分組,緩存進RecyclerViewPool裡。
注意:一個View只被detach,沒有被recycle的話,不會放進RecyclerViewPool裡,會一直存在recycler的scrap 中。網上有人的Demo就是如此,因此View也沒有被復用,有多少ItemCount,就會new出多少個ViewHolder。Q8 初始化時,onLayoutChildren()為什麼會執行兩次?
@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);
.....
}
Q9 基於上個問題,我們要注意什麼?
最後也是最重要的
原話如下:
文章鏈接: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輕量回收所有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;
}
摘要:剛一開始接觸Chromium on Android時,就很好奇Chromium的主消息循環是怎麼整合到Android應用程序中的。對於Android程序來說,一旦啟
原始圖效果 模仿效果PNGGIF 流程繪制中心線,用於計算外層多邊形各點的坐標 繪制最外層多邊形 分析原型圖算出每個多邊形之間的間距 繪制裡三層多邊形
Android提供了很多控件便於開發者進行UI相關的程序設計。但是很多時候,默認的一些UI設置不足以滿足我們的需求,要麼不好看,要麼高度不夠,亦或者是與應用界面不協調。於
最近在項目開發中遇到一個關於手機輸入鍵盤的坑,特來記錄下。應用場景:項目中有一個界面是用viewpaper加三個fragment寫的,其中viewpaper被我屏蔽了左右