上週五寫了篇仿夸克瀏覽器底部工具欄,相信看過的同窗還有印象吧。在文末我拋出了一個問題,夸克瀏覽器底部工具欄只是單層層疊的ViewGroup,如何實現相似Android系統通知欄的多級層疊列表呢? java
不過當時僅僅有了初步的思路:recyclerView
+自定義
layoutManager
,因此週末又把自定義
layoutManager
狠補了一遍。終於大體實現了這個效果(固然細節有待優化( ̄. ̄))。老樣子,先來看看效果吧:
實際使用時可能不須要頂部層疊,因此還有單邊效果,看起來更天然些:
怎麼樣,乍一看是否是很是形(神)似呢?以上的效果都是自定義
layoutManager
實現的,因此只要一行代碼就能把普通的RecyclerView替換成這種層疊列表:
mRecyclerView.setLayoutManager(new OverFlyingLayoutManager());
複製代碼
好了廢話很少說,直接來分析下怎麼實現吧。如下的主要內容就是幫你從學會到熟悉自定義layoutManager
。git
先簡單說下自定義layoutManager
的步驟吧,其實不少文章都講過,適合沒接觸的同窗:github
generateDefaultLayoutParams()
方法,生成本身所定義擴展的LayoutParams
。onLayoutChildren()
中實現初始列表中各個itemView
的位置scrollVerticallyBy()
和scrollHorizontallyBy()
中處理橫向和縱向滾動,還有view的回收複用。我的理解就是:layoutManager
就至關於自定義ViewGroup
中把onMeasure()
、onlayout()
,scrollTo()
等方法獨立出來,單獨交給它來作。實際表現也是相似:onLayoutChildren()
做用就是測量放置itemView
。瀏覽器
咱們先實現本身的佈局參數:bash
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
複製代碼
也就是不實現,自帶的RecyclerView.LayoutParams
繼承自ViewGroup.MarginLayoutParams
,已經夠用了。經過查看源碼,最終這個方法返回的佈局參數對象會設置給:ide
holder.itemView.setLayoutParams(rvLayoutParams);
複製代碼
而後實現onLayoutChildren()
,在裏面要把全部itemView
沒滑動前自身應該在的位置都記錄並放置一遍: 定義兩個集合:工具
// 用於保存item的位置信息
private SparseArray<Rect> allItemRects = new SparseArray<>();
// 用於保存item是否處於可見狀態的信息
private SparseBooleanArray itemStates = new SparseBooleanArray();
複製代碼
把全部View虛擬地放置一遍,記錄下每一個view的位置信息,由於此時並無把View真正到recyclerview中,也是不可見的:佈局
private void calculateChildrenSiteVertical(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 先把全部的View先從RecyclerView中detach掉,而後標記爲"Scrap"狀態,表示這些View處於可被重用狀態(非顯示中)。
detachAndScrapAttachedViews(recycler);
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
// 測量View的尺寸。
measureChildWithMargins(view, 0, 0);
//去除ItemDecoration部分
calculateItemDecorationsForChild(view, new Rect());
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
Rect mTmpRect = allItemRects.get(i);
if (mTmpRect == null) {
mTmpRect = new Rect();
}
mTmpRect.set(0, totalHeight, width, totalHeight + height);
totalHeight += height;
// 保存ItemView的位置信息
allItemRects.put(i, mTmpRect);
// 因爲以前調用過detachAndScrapAttachedViews(recycler),因此此時item都是不可見的
itemStates.put(i, false);
}
addAndLayoutViewVertical(recycler, state, 0);
}
複製代碼
而後咱們開始真正地添加View到RecyclerView中。爲何不在記錄位置的時候添加呢?由於後添加的view若是和前面添加的view重疊,那麼後添加的view會覆蓋前者,和咱們想要實現的層疊的效果是相反的,因此須要正向記錄位置信息,而後根據位置信息反向添加View:post
private void addAndLayoutViewVertical(RecyclerView.Recycler recycler, RecyclerView.State state) {
int displayHeight = getWidth() - getPaddingLeft() - getPaddingRight();//計算recyclerView能夠放置view的高度
//反向添加
for (int i = getItemCount() - 1; i >= 0; i--) {
// 遍歷Recycler中保存的View取出來
View view = recycler.getViewForPosition(i);
//由於剛剛進行了detach操做,因此如今能夠從新添加
addView(view);
//測量view的尺寸
measureChildWithMargins(view, 0, 0);
int width = getDecoratedMeasuredWidth(view); // 計算view實際大小,包括了ItemDecorator中設置的偏移量。
int height = getDecoratedMeasuredHeight(view);
//調用這個方法可以調整ItemView的大小,以除去ItemDecorator距離。
calculateItemDecorationsForChild(view, new Rect());
Rect mTmpRect = allItemRects.get(i);//取出咱們以前記錄的位置信息
if (mTmpRect.bottom > displayHeight) {
//排到底了,後面統一置底
layoutDecoratedWithMargins(view, 0, displayHeight - height, width, displayHeight);
} else {
//按原位置放置
layoutDecoratedWithMargins(view, 0, mTmpRect.top, width, mTmpRect.bottom);
}
Log.e(TAG, "itemCount = " + getChildCount());
}
複製代碼
這樣一來,編譯運行,界面上已經能看到列表了,就是它還不能滾動,只能停留在頂部。優化
先設置容許縱向滾動:
@Override
public boolean canScrollVertically() {
// 返回true表示能夠縱向滑動
return orientation == OrientationHelper.VERTICAL;
}
複製代碼
處理滾動原理其實很簡單:
onLayoutChildren()
同樣從新佈局就行@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//列表向下滾動dy爲正,列表向上滾動dy爲負,這點與Android座標系保持一致。
//dy是系統告訴咱們手指滑動的距離,咱們根據這個距離來處理列表實際要滑動的距離
int tempDy = dy;
//最多滑到總距離減去列表距離的位置,便可滑動的總距離是列表內容多餘的距離
if (verticalScrollOffset <= totalHeight - getVerticalSpace()) {
//將豎直方向的偏移量+dy
verticalScrollOffset += dy;
}
if (verticalScrollOffset > totalHeight - getVerticalSpace()) {
verticalScrollOffset = totalHeight - getVerticalSpace();
tempDy = 0;//滑到底部了,就返回0,說明到邊界了
} else if (verticalScrollOffset < 0) {
verticalScrollOffset = 0;
tempDy = 0;//滑到頂部了,就返回0,說明到邊界了
}
//從新佈局位置、顯示View
addAndLayoutViewVertical(recycler, state, verticalScrollOffset);
return tempDy;
}
複製代碼
上面說了,滾動其實就是根據滑動距離從新佈局的過程,和onLayoutChildren()
中的初始化佈局沒什麼兩樣。咱們擴展布局方法,傳入偏移量,這樣onLayoutChildren()
調用時只要傳0就好了:
private void addAndLayoutViewVertical(RecyclerView.Recycler recycler, RecyclerView.State state, int offset) {
int displayHeight = getVerticalSpace();
for (int i = getItemCount() - 1; i >= 0; i--) {
// 遍歷Recycler中保存的View取出來
View view = recycler.getViewForPosition(i);
addView(view); // 由於剛剛進行了detach操做,因此如今能夠從新添加
measureChildWithMargins(view, 0, 0); // 通知測量view的margin值
int width = getDecoratedMeasuredWidth(view); // 計算view實際大小,包括了ItemDecorator中設置的偏移量。
int height = getDecoratedMeasuredHeight(view);
Rect mTmpRect = allItemRects.get(i);
//調用這個方法可以調整ItemView的大小,以除去ItemDecorator。
calculateItemDecorationsForChild(view, new Rect());
int bottomOffset = mTmpRect.bottom - offset;
int topOffset = mTmpRect.top - offset;
if (bottomOffset > displayHeight) {//滑到底了
layoutDecoratedWithMargins(view, 0, displayHeight - height, width, displayHeight);
} else {
if (topOffset <= 0 ) {//滑到頂了
layoutDecoratedWithMargins(view, 0, 0, width, height);
} else {//中間位置
layoutDecoratedWithMargins(view, 0, topOffset, width, bottomOffset);
}
}
Log.e(TAG, "itemCount = " + getChildCount());
}
複製代碼
好了,這樣就能滾動了。
由於自定義layoutManager
內容比較多,因此我分紅了上下篇來說。到這裏基礎效果實現了,可是這個RecyclerView尚未實現回收複用(參看addAndLayoutViewVertical
末尾打印),還有邊緣的層疊嵌套動畫和視覺處理也都留到下篇說了。看了上面的內容,實現橫向滾動也是很簡單的,感興趣的本身去github上看下實現吧!