FormLayoutManager首頁,裏面有github地址
目錄
前言
接下來會一步步帶大家走進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)還沒寫……