前言
上周五寫了篇仿夸克瀏覽器底部工具欄,相信看過(guò)的同學(xué)還有印象吧。在文末我拋出了一個(gè)問(wèn)題,夸克瀏覽器底部工具欄只是單層層疊的ViewGroup,如何實(shí)現(xiàn)類似Android系統(tǒng)通知欄的多級(jí)層疊列表呢?

不過(guò)當(dāng)時(shí)僅僅有了初步的思路:recyclerView+自定義layoutManager,所以周末又把自定義layoutManager狠補(bǔ)了一遍。終于大致實(shí)現(xiàn)了這個(gè)效果(當(dāng)然細(xì)節(jié)有待優(yōu)化( ̄. ̄))。老樣子,先來(lái)看看效果吧:

實(shí)際使用時(shí)可能不需要頂部層疊,所以還有單邊效果,看起來(lái)更自然些:

怎么樣,乍一看是不是非常形(神)似呢?以上的效果都是自定義layoutManager實(shí)現(xiàn)的,所以只要一行代碼就能把普通的RecyclerView替換成這種層疊列表:
mRecyclerView.setLayoutManager(new OverFlyingLayoutManager());
好了廢話不多說(shuō),直接來(lái)分析下怎么實(shí)現(xiàn)吧。以下的主要內(nèi)容就是幫你從學(xué)會(huì)到熟悉自定義layoutManager。
概述
先簡(jiǎn)單說(shuō)下自定義layoutManager的步驟吧,其實(shí)很多文章都講過(guò),適合沒(méi)接觸的同學(xué):
- 實(shí)現(xiàn)
generateDefaultLayoutParams()方法,生成自己所定義擴(kuò)展的LayoutParams。 - 在
onLayoutChildren()中實(shí)現(xiàn)初始列表中各個(gè)itemView的位置 - 在
scrollVerticallyBy()和scrollHorizontallyBy()中處理橫向和縱向滾動(dòng),還有view的回收復(fù)用。
個(gè)人理解就是:layoutManager就相當(dāng)于自定義ViewGroup中把onMeasure()、onlayout(),scrollTo()等方法獨(dú)立出來(lái),單獨(dú)交給它來(lái)做。實(shí)際表現(xiàn)也是類似:onLayoutChildren()作用就是測(cè)量放置itemView。
初始化列表
我們先實(shí)現(xiàn)自己的布局參數(shù):
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
也就是不實(shí)現(xiàn),自帶的RecyclerView.LayoutParams繼承自ViewGroup.MarginLayoutParams,已經(jīng)夠用了。通過(guò)查看源碼,最終這個(gè)方法返回的布局參數(shù)對(duì)象會(huì)設(shè)置給:
holder.itemView.setLayoutParams(rvLayoutParams);
然后實(shí)現(xiàn)onLayoutChildren(),在里面要把所有itemView沒(méi)滑動(dòng)前自身應(yīng)該在的位置都記錄并放置一遍:
定義兩個(gè)集合:
// 用于保存item的位置信息
private SparseArray<Rect> allItemRects = new SparseArray<>();
// 用于保存item是否處于可見狀態(tài)的信息
private SparseBooleanArray itemStates = new SparseBooleanArray();
把所有View虛擬地放置一遍,記錄下每個(gè)view的位置信息,因?yàn)榇藭r(shí)并沒(méi)有把View真正到recyclerview中,也是不可見的:
private void calculateChildrenSiteVertical(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 先把所有的View先從RecyclerView中detach掉,然后標(biāo)記為"Scrap"狀態(tài),表示這些View處于可被重用狀態(tài)(非顯示中)。
detachAndScrapAttachedViews(recycler);
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
// 測(cè)量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);
// 由于之前調(diào)用過(guò)detachAndScrapAttachedViews(recycler),所以此時(shí)item都是不可見的
itemStates.put(i, false);
}
addAndLayoutViewVertical(recycler, state, 0);
}
然后我們開始真正地添加View到RecyclerView中。為什么不在記錄位置的時(shí)候添加呢?因?yàn)楹筇砑拥膙iew如果和前面添加的view重疊,那么后添加的view會(huì)覆蓋前者,和我們想要實(shí)現(xiàn)的層疊的效果是相反的,所以需要正向記錄位置信息,然后根據(jù)位置信息反向添加View:
private void addAndLayoutViewVertical(RecyclerView.Recycler recycler, RecyclerView.State state) {
int displayHeight = getWidth() - getPaddingLeft() - getPaddingRight();//計(jì)算recyclerView可以放置view的高度
//反向添加
for (int i = getItemCount() - 1; i >= 0; i--) {
// 遍歷Recycler中保存的View取出來(lái)
View view = recycler.getViewForPosition(i);
//因?yàn)閯倓傔M(jìn)行了detach操作,所以現(xiàn)在可以重新添加
addView(view);
//測(cè)量view的尺寸
measureChildWithMargins(view, 0, 0);
int width = getDecoratedMeasuredWidth(view); // 計(jì)算view實(shí)際大小,包括了ItemDecorator中設(shè)置的偏移量。
int height = getDecoratedMeasuredHeight(view);
//調(diào)用這個(gè)方法能夠調(diào)整ItemView的大小,以除去ItemDecorator距離。
calculateItemDecorationsForChild(view, new Rect());
Rect mTmpRect = allItemRects.get(i);//取出我們之前記錄的位置信息
if (mTmpRect.bottom > displayHeight) {
//排到底了,后面統(tǒng)一置底
layoutDecoratedWithMargins(view, 0, displayHeight - height, width, displayHeight);
} else {
//按原位置放置
layoutDecoratedWithMargins(view, 0, mTmpRect.top, width, mTmpRect.bottom);
}
Log.e(TAG, "itemCount = " + getChildCount());
}
這樣一來(lái),編譯運(yùn)行,界面上已經(jīng)能看到列表了,就是它還不能滾動(dòng),只能停留在頂部。
處理滾動(dòng)
先設(shè)置允許縱向滾動(dòng):
@Override
public boolean canScrollVertically() {
// 返回true表示可以縱向滑動(dòng)
return orientation == OrientationHelper.VERTICAL;
}
處理滾動(dòng)原理其實(shí)很簡(jiǎn)單:
- 手指在屏幕上滑動(dòng),系統(tǒng)告訴我們一個(gè)滑動(dòng)的距離
- 我們根據(jù)這個(gè)距離判斷我們列表內(nèi)部各個(gè)view的實(shí)際變化,然后和
onLayoutChildren()一樣重新布局就行 - 返回告訴系統(tǒng)我們滑動(dòng)了多少,如果返回0,就說(shuō)明滑到邊界了,就會(huì)有一個(gè)邊緣的波紋效果。
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//列表向下滾動(dòng)dy為正,列表向上滾動(dòng)dy為負(fù),這點(diǎn)與Android坐標(biāo)系保持一致。
//dy是系統(tǒng)告訴我們手指滑動(dòng)的距離,我們根據(jù)這個(gè)距離來(lái)處理列表實(shí)際要滑動(dòng)的距離
int tempDy = dy;
//最多滑到總距離減去列表距離的位置,即可滑動(dòng)的總距離是列表內(nèi)容多余的距離
if (verticalScrollOffset <= totalHeight - getVerticalSpace()) {
//將豎直方向的偏移量+dy
verticalScrollOffset += dy;
}
if (verticalScrollOffset > totalHeight - getVerticalSpace()) {
verticalScrollOffset = totalHeight - getVerticalSpace();
tempDy = 0;//滑到底部了,就返回0,說(shuō)明到邊界了
} else if (verticalScrollOffset < 0) {
verticalScrollOffset = 0;
tempDy = 0;//滑到頂部了,就返回0,說(shuō)明到邊界了
}
//重新布局位置、顯示View
addAndLayoutViewVertical(recycler, state, verticalScrollOffset);
return tempDy;
}
上面說(shuō)了,滾動(dòng)其實(shí)就是根據(jù)滑動(dòng)距離重新布局的過(guò)程,和onLayoutChildren()中的初始化布局沒(méi)什么兩樣。我們擴(kuò)展布局方法,傳入偏移量,這樣onLayoutChildren()調(diào)用時(shí)只要傳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取出來(lái)
View view = recycler.getViewForPosition(i);
addView(view); // 因?yàn)閯倓傔M(jìn)行了detach操作,所以現(xiàn)在可以重新添加
measureChildWithMargins(view, 0, 0); // 通知測(cè)量view的margin值
int width = getDecoratedMeasuredWidth(view); // 計(jì)算view實(shí)際大小,包括了ItemDecorator中設(shè)置的偏移量。
int height = getDecoratedMeasuredHeight(view);
Rect mTmpRect = allItemRects.get(i);
//調(diào)用這個(gè)方法能夠調(diào)整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());
}
好了,這樣就能滾動(dòng)了。
小結(jié)
因?yàn)樽远xlayoutManager內(nèi)容比較多,所以我分成了上下篇來(lái)講。到這里基礎(chǔ)效果實(shí)現(xiàn)了,但是這個(gè)RecyclerView還沒(méi)有實(shí)現(xiàn)回收復(fù)用(參看addAndLayoutViewVertical末尾打?。?,還有邊緣的層疊嵌套動(dòng)畫和視覺(jué)處理也都留到下篇說(shuō)了。看了上面的內(nèi)容,實(shí)現(xiàn)橫向滾動(dòng)也是很簡(jiǎn)單的,感興趣的自己去github上看下實(shí)現(xiàn)吧!
