? ? ? ?LayoutManager是RecyclerView的布局管理器,RecyclerView里面每個item的布局都依賴于LayoutManager的實現。Andorid系統給提供了三個LayoutManager布局管理器:LinearLayoutManager(線性布局管理器)、GridLayoutManager(網格布局管理器)、StaggeredGridLayoutManager(錯列網格布局管理器)。大部分情況下這三個布局管理器都能滿足我們的需求。但是,總有特殊情況的時候。有的時候UI設計師給的效果用系統自帶的這三個布局管理器就是達不到。這個時候咱們就得根據實際需求自定義LayoutManager了,這也是我們接下來所要討論的重點。
一、效果展示
? ? ? ?為了讓大家在客觀上有更好的感受,這里實現了兩種效果:卡片式效果、表格式效果。效果如下:
表格實現的功能:
- 行數和列數不固定,可以隨便多少個。
- 行數或者列數超過屏幕大小是可以上下左右全方位滑動。
- 支持設置固定第一行的功能,因為第一行經常是標題欄,在上下滑動的時候,大部分情況也許固定的效果更好點。
- 支持設置固定每一行的前面多少列,在左右滑動的時候固定。
- 支持設置除固定多少列之外,當總的列數小于這個設定值的時候。讓剩下的列平分剩下的寬度。比如我們表格只有兩列,我們可以設置讓這兩列平分屏幕的寬度。(@2018.1.19)
二、LayoutManager自定義
? ? ? ?對于自定義LayoutManager我們主要處理好三件事情,就所有的問題就都迎刃而解了:
- 布局每個ItemView
- 處理滑動事件
- 緩存重用ItemView
2.1、LayoutManager自定義時常用函數介紹
- 布局每個ItemView
? ? ? ?布局每個item需要重寫的函數:
/**
* 這里定義RecyclerView里面每個item默認的LayoutParams
*/
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
/**
* 重寫這個函數來布局RecyclerView當前需要顯示的item,確定每個item的位置
*/
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
//TODO:來布局RecyclerView當前需要顯示的item
}
? ? ? ?generateDefaultLayoutParams和onLayoutChildren函數也是每個LayoutManager自定義的時候都必須重寫的函數。
? ? ? ?布局每個item常用API:
/**
* 根據Adapter的位置position找RecyclerView中對應的item
* 我們不管它是從scrap里取,還是從RecyclerViewPool里取,亦或是onCreateViewHolder里拿
* 系統已經幫我們處理好了緩存了
*/
View view = recycler.getViewForPosition(position);
/**
* 將item添加到RecyclerView當中去
*/
addView(item);
addView(item, index);
/**
* 測量item,這個方法會考慮到View的ItemDecoration以及Margin
*/
measureChildWithMargins(item, 0, 0);
/**
* 將item layout布局出來,顯示在屏幕上,內部會自動追加上該View的ItemDecoration和Margin
* 這里就需要自己去控制顯示的位置了
*/
layoutDecoratedWithMargins(item, 0, 0, 0, 0);
? ? ? ?布局每個ItemView關鍵部分確定當前界面上需要顯示的position對應的item,接著測量item,最后把item layout到指定的位置上。
- 處理滑動事件
? ? ? ?處理滑動需要重寫的函數:
/**
* 是否可以水平滑動
*/
@Override
public boolean canScrollHorizontally() {
return true;
}
/**
* canScrollHorizontally返回true的基礎上,RecyclerView有手指水平滑動的時候回調該函數
* 注意處理完之后要返回消費的距離
*/
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
//TODO:滑動邏輯
return dx;
}
/**
* 是否可以垂直滑動
*/
@Override
public boolean canScrollVertically() {
return true;
}
/**
* canScrollVertically返回true的基礎上,RecyclerView有手指垂直滑動的時候回調該函數
* 注意處理完之后要返回消費的距離
*/
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//TODO:滑動邏輯
return dy;
}
? ? ? ?canScrollHorizontally和canScrollVertically是滑動的開關操作,只有開關打開了scrollHorizontallyBy()、scrollVerticallyBy()才會被回調,這樣咱們就可以在里面處理滑動的邏輯了。在滑動的時候不管是重新去布局item,還是讓item offset都可以。
? ? ? ?處理滑動常用API:
/**
* 垂直移動RecyclerView內所有的item
*/
offsetChildrenVertical(-dy);
/**
* 垂直移動RecyclerView內所有的item
*/
offsetChildrenHorizontal(-dx);
? ? ? ?處理滑動事的關鍵之處是,在滑動過程中RecyclerView里面每個item的位置變化。
- 緩存重用ItemView
? ? ? ?常用API:
/**
* detach輕量回收View
*/
detachAndScrapAttachedViews(recycler);
detachAndScrapView(view, recycler);
/**
* recycle真的回收一個View ,該View再次回來需要執行onBindViewHolder方法
*/
removeAndRecycleView(View child, Recycler recycler)
removeAndRecycleAllViews(Recycler recycler);
/**
* 超級輕量回收一個View,馬上就要添加回來,和attachView()對應
*/
detachView(view);
/**
* 將detachView(view) detach的View attach回來
*/
attachView(view);
/**
* detachView 后 沒有attachView的話 就要真的回收掉他們
*/
recycler.recycleView(viewCache.valueAt(i));
? ? ? ?緩存重用ItemView的時候,只要你調用了合適的回收函數(detachAndScrapAttachedViews、detachAndScrapView)對應的View會自動回收到緩存機制里面去,當下次recycler.getViewForPosition(position)的時候會先檢測緩存里面是否存在,如果存在則直接從緩存里面獲取,如果不存在則執行Adapter的onBindViewHolder獲取。
2.2、LayoutManager自定義實例講解
? ? ? ?前面講了一大堆,都是為自定義LayoutManager實例做鋪墊。咱得真刀真槍的用上來。接下來介紹兩個自定義LayoutManager的思路:CardLayoutManager、TableLayoutManager。
- CardLayoutManager
? ? ? ?CardLayoutManager實現卡片式布局,RecyclerView里面所有的item從上往下依次疊在一起,同時為了看起來有層次感,越底下的item會我們稍微添加錯位的效果。并且上層第一張卡片可以隨手指的滑動刪除。關于卡片隨手指滑動刪除效果通過ItemTouchHelper來實現,對于ItemTouchHelper的使用我們就不深究,有興趣的可以參考ItemTouchHelper源碼分析。我們重點在卡CardLayoutManager的實現。
? ? ? ?我們需要通過CardLayoutManager來實現RecyclerView里面所有item疊在一起的效果。而且也不用去處理滑動的邏輯。所有關鍵部分在于重寫onLayoutChildren()函數。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
if (getItemCount() <= 0 || state.isPreLayout()) {
return;
}
// 先移除所有view
detachAndScrapAttachedViews(recycler);
// 防止view過多產生oom情況,這里我們做了view最大個數的限制,因為沒辦法像別的LayoutManager那樣通過item是否落在屏幕內判斷是否回收
int viewCount = getItemCount();
if (getItemCount() > mShowViewMax) {
viewCount = mShowViewMax;
}
// 這里要注意view要反著加,因為adapter position = 0對應的view我們要顯示在最上層
for (int position = viewCount - 1; position >= 0; position--) {
// 獲取到制定位置的view
final View view = recycler.getViewForPosition(position);
addView(view);
// 測量view
measureChildWithMargins(view, 0, 0);
// view在RecyclerView里面還剩余的寬度
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
// layout view,水平居中,靠上
layoutDecoratedWithMargins(view, widthSpace / 2, 0, widthSpace / 2 + getDecoratedMeasuredWidth(view),
getDecoratedMeasuredHeight(view));
// 為了讓重疊在一起的view,有一個更好的顯示效果
view.setScaleX(getScaleX(position));
view.setScaleY(getScaleY(position));
view.setTranslationX(getTranslationX(position));
view.setTranslationY(getTranslationY(position));
}
}
? ? ? ?看到里面的邏輯也很簡單,先移除回收之前所有的item view。然后確定RecyclerView當前要顯示多少個item(防止OOM,不可能顯示所有item),接著就是獲取view,添加view,測量view,布局view。 為了讓疊在一起的item有層次感對item做一些縮放和偏移。大功告成。
- TableLayoutManager
? ? ? ?TableLayoutManager實現表格的功能,當表格比較大的時候既可以上下滑動又可以左右滑動。在這個基礎之上我們還加入了可以固定表頭(因為第一行有的時候是標題,需要固定),以及可以固定指定前面多少列的需求。其實之前我們也用ListView實現類似的需求,有興趣的可以點擊:Android打造全方位滾動的ListView。
? ? ? ?TableLayoutManager的實現我們分以下幾個部分:
- item布局:
? ? ? ?表格里面肯定是有很多item項的,我們不可能所有的item都在onLayoutChildren里面布局出來。所以為了防止OOM發生,我們一定要明確當前屏幕要顯示哪些item。所以我們有一個Rect變量來記錄item可以顯示的區域。在item layout布局的時候只布局在區域內的item。
- 處理滑動:
? ? ? ?在滑動的時候,一定要及時去更新顯示區域的變化,以及記錄滑動offset的位置。當每次滑動的時候我們都會重新去layout RecyclerView 里面的item,加上相應的offset。更加具體的邏輯可以參考代碼里面fillChildren()函數實現部分。
- 回收
? ? ? ?其實在item布局的時候就已經把這個考慮進去了,item不在顯示區域之內的不會add到RecyclerView里面,已經避免了OOM的情況。
- 處理固定列,固定行部分:
? ? ? ?處理固定列,固定行我這里用的非常粗暴的辦法,在每次layout RecyclerView 里面item的時候,我會把需要固定的item在重新layout到固定的位置上。具體可以參考代碼里面fillChildren()函數實現。
? ? ? ?TableLayoutManager的實現上面講的比較少,推薦大家對照TableLayoutManager源碼來看,TableLayoutManager源碼里面也有相應的注釋。
? ? ? ?最后給出CardLayoutManager、TableLayoutManager實現下載地址:CardLayoutManager、TableLayoutManager代碼鏈接。里面肯定也有很多可以優化的地方歡迎大家指出,同時如果大家在CardLayoutManager、TableLayoutManager基礎之上有什么新的需要也可以提出來,在能力范圍之內的會盡量幫大家實現的。