本文轉(zhuǎn)自RecyclerView系列之三自定義LayoutManager
LayoutManager主要用于布局其中的item,在LayoutManager中能夠?qū)γ總€(gè)item的大小、位置進(jìn)行更改,將它放到指定的位置上,在很多優(yōu)秀的效果中,都是通過自定義LayoutManager實(shí)現(xiàn)的。
下面先來看下如何自定義LinearLayoutManager。
1、初始化展示界面
1.1、自定義CustomLinearLayoutManager
public class CustomLinearLayoutManager extends LayoutManager {
@Override
public LayoutParams generateDefaultLayoutParams() {
return null;
}
}
繼承LayoutManager會(huì)強(qiáng)制讓我們實(shí)現(xiàn)generateDefaultLayoutParams()
方法。該方法主要用于設(shè)置RecyclerView中item的LayoutParams。
當(dāng)在onCreateViewHolder方法中通過LayoutInflater.inflate(resource, root,attachToRoot)加載布局時(shí)root傳入為null時(shí)item的LayoutParams為null,此時(shí)
generateDefaultLayoutParams()
生成的LayoutParams就派上用場了。
一般來說沒有什么特殊的情況下,讓子item自己決定寬高,故設(shè)置為wrap_content。
public class CustomLayoutManager extends LayoutManager {
@Override
public LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
}
這樣自定義LayoutManager就完成了嗎,運(yùn)行一下你會(huì)發(fā)現(xiàn),啥也不顯示。這又是為啥呢?前面我們就說了LayoutManager主要負(fù)責(zé)item的布局,而上面代碼中并未對item布局進(jìn)行布局。
1.2、onLayoutManager
在LayoutManager中,item的布局是在onLayoutManager中完成的。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//定義豎直方向的偏移量
int offsetY = 0;
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
layoutDecorated(view, 0, offsetY, width, offsetY + height);
offsetY += height;
}
}
上面方法中主要做了兩件事:
- 1、將所有的item添加進(jìn)來
//從緩存中取出合適的View,如果緩存中沒有則通過onCreateViewHolder創(chuàng)建
View view = recycler.getViewForPosition(i);
//添加View
addView(view);
- 2、對所有item進(jìn)行布局
//添加View后,進(jìn)行View測量
measureChildWithMargins(view, 0, 0);
//getDecoratedMeasuredWidth得到的是item+decoration總寬度
int width = getDecoratedMeasuredWidth(view);
//getDecoratedMeasuredHeight得到的是item+decoration總高度
int height = getDecoratedMeasuredHeight(view);
//布局item
layoutDecorated(view, 0, offsetY, width, offsetY + height);
再次運(yùn)行就會(huì)發(fā)現(xiàn)可以顯示界面了。
2、滾動(dòng)邏輯
2.1、添加滾動(dòng)效果
但是現(xiàn)在還不能滾動(dòng),如果要實(shí)現(xiàn)滾動(dòng),需要修改兩個(gè)地方。
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
// 平移容器內(nèi)的item
offsetChildrenVertical(-dy);
return dy;
}
- 1、重寫canScrollVertically()返回true,表示豎直滾動(dòng),如果想實(shí)現(xiàn)水平滾動(dòng)則需要重寫canScrollHorizontally()并返回true。
- 2、方法scrollVerticallyBy()中接收參數(shù)dy表示每次豎直滾動(dòng)的距離。當(dāng)手指從下向上滑動(dòng)時(shí):dy>0,手指從上向下滑動(dòng)時(shí):dy<0。當(dāng)手指向上滑動(dòng)時(shí),RecyclerView中item應(yīng)該也跟著手指向上移動(dòng),所以在調(diào)用
offsetChildrenVertical(-dy)
方法實(shí)現(xiàn)item的平移時(shí),應(yīng)傳入-dy
才合適。
這樣就完成了item的滾動(dòng),運(yùn)行后發(fā)現(xiàn)有兩個(gè)問題,當(dāng)滾動(dòng)到頂部和底部時(shí),依然可以滾動(dòng),這顯然是不對的,下面就看下如何進(jìn)行邊界的限制。
2.2、邊界判斷
現(xiàn)在我們就面臨兩個(gè)問題:如何判斷滑動(dòng)到了頂部和底部。
2.2.1、判斷到頂部
將滑動(dòng)過程中所有dy累加,如果小于0,表示已經(jīng)滑動(dòng)到了頂部,此時(shí)不讓它移動(dòng)即可。
private int mSumDy = 0;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
int travel = dy;
//如果滑動(dòng)到最頂部
if (mSumDy + dy < 0) {
travel = -mSumDy;
}
mSumDy += travel;
// 平移容器內(nèi)的item
offsetChildrenVertical(-travel);
return travel;
}
上面代碼中,通過mSumDy累加所有滑動(dòng)過的距離,如果當(dāng)前滑動(dòng)距離小于0,就讓它移動(dòng)到y(tǒng)=0的位置,mSumDy是之前移動(dòng)過的距離,所以計(jì)算方法為:
travel+mSumDy=0,即travel = -mSumDy;
然后調(diào)用offsetChildrenVertical()
將修正后的travel傳入。
2.2.2、判斷到底部
當(dāng)向上滑動(dòng),在恰好滑動(dòng)到底部時(shí),滑動(dòng)距離=所有item的高度和-RecyclerView的高度。當(dāng)滑動(dòng)的距離大于所有item的高度和-RecyclerView的高度時(shí)就說明已經(jīng)滑動(dòng)到了底部。
根據(jù)上面的分析,首先我們要得到所有item的高度和,在onLayoutChildren中我們會(huì)對每一個(gè)item進(jìn)行測量并布局,在這里進(jìn)行累加就可以算出總高度。
private int mTotalHeight = 0;
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//定義豎直方向的偏移量
int offsetY = 0;
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
layoutDecorated(view, 0, offsetY, width, offsetY + height);
offsetY += height;
}
//如果所有子View的高度和沒有填滿RecyclerView的高度,
// 則將高度設(shè)置為RecyclerView的高度
mTotalHeight = Math.max(offsetY, getVerticalSpace());
}
private int getVerticalSpace() {
return getHeight() - getPaddingBottom() - getPaddingTop();
}
getVerticalSpace()函數(shù)可以得到RecyclerView用于顯示item的真實(shí)高度。而相比上面的onLayoutChildren,這里只添加了一句代碼:mTotalHeight = Math.max(offsetY, getVerticalSpace());
這里只所以取最offsetY和getVerticalSpace()的最大值是因?yàn)椋琽ffsetY是所有item的總高度,而當(dāng)item填不滿RecyclerView時(shí),offsetY應(yīng)該是比RecyclerView的真正高度小的,而此時(shí)的真正的高度應(yīng)該是RecyclerView本身所設(shè)置的高度。
接下來就是在scrollVerticallyBy中判斷到底并處理了:
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
int travel = dy;
//如果滑動(dòng)到最頂部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
travel = mTotalHeight - getVerticalSpace() - mSumDy;
}
mSumDy += travel;
// 平移容器內(nèi)的item
offsetChildrenVertical(-travel);
return dy;
}
當(dāng)滑動(dòng)偏移量大于 mTotalHeight - getVerticalSpace()時(shí)說明已經(jīng)滑到了底部,所以此時(shí)需要對將要滑動(dòng)的dy進(jìn)行修正,使得滿足
travel+mSumDy=mTotalHeight - getVerticalSpace();
即travel = mTotalHeight - getVerticalSpace() - mSumDy;
這樣就完成了底部邊界的問題的修復(fù)。下面貼出完整代碼:
public class CustomLayoutManager extends LayoutManager {
private int mSumDy = 0;
private int mTotalHeight = 0;
@Override
public LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//定義豎直方向的偏移量
int offsetY = 0;
for (int i = 0; i < getItemCount(); i++) {
View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int width = getDecoratedMeasuredWidth(view);
int height = getDecoratedMeasuredHeight(view);
layoutDecorated(view, 0, offsetY, width, offsetY + height);
offsetY += height;
}
//如果所有子View的高度和沒有填滿RecyclerView的高度,
// 則將高度設(shè)置為RecyclerView的高度
mTotalHeight = Math.max(offsetY, getVerticalSpace());
}
private int getVerticalSpace() {
return getHeight() - getPaddingBottom() - getPaddingTop();
}
@Override
public boolean canScrollVertically() {
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
int travel = dy;
//如果滑動(dòng)到最頂部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
//如果滑動(dòng)到最底部
travel = mTotalHeight - getVerticalSpace() - mSumDy;
}
mSumDy += travel;
// 平移容器內(nèi)的item
offsetChildrenVertical(-travel);
return dy;
}
}