前言: 等你发现时间是贼了,它早已偷光你的选择。 ——《给自己的歌-李宗盛》
LayoutManager主要用于RecyclerView的布局,itemView的回收和复用,在LayoutManager能对每个item的大小、位置进行更改,做出我们想要的效果。很多优秀的效果都是通过自定义LayoutManager来实现的。在前面的文章源码讲解中,需要自定义LayoutManager则需要重写onLayoutChildren()
方法,它是布局RecyclerView的入口,再通过scrollVerticallyBy()
等方法控制滑动的距离等。
这一节,我们来手动制作一个LinearLayoutManager,来看下如果自定义LayoutManager。(源码地址在文章最后给出)
首先创建一个MySelfLayoutManager类,继承RecyclerV.LayoutManager,这时会强制复写generateDefaultLayoutParams()
这个方法
public class MySelfLayoutManager extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return null;
}
}
这个方法是RecyclerView的item的布局参数,换种说法来说就是RecyclerView的item的LayoutParameters,如果想要修改item的布局参数,比如宽高、margin、padding等,那么可以在该方法设置。如果没有特别的需要,一般会让子item自己决定自己的宽高,即设置为wrap_content:
public class MySelfLayoutManager extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
}
我们把MySelfLayoutManager 设置给RecyclerView看看效果如何:
public class MyLayoutManagerActivity extends AppCompatActivity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recyclerview);
RecyclerView recyclerView = findViewById(R.id.recyclerView);
······
recyclerView.setLayoutManager(new MySelfLayoutManager());
ItemClickAdapter adapter = new ItemClickAdapter(this);
recyclerView.setAdapter(adapter);
adapter.setDataList(goodsList);
}
}
运行一下,效果如下:
你会发现什么都没有,我们说过所有的item布局都是在LayoutManager中处理的,在MySelfLayoutManager 中并没有处理任何的item。
在LayoutManager中,所有的item都是在onLayoutChildren()
布局的, 重写这个函数:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
int offSetY = 0;//垂直方向的偏移量
for (int i = 0; i < getItemCount(); i++) {
View itemView = recycler.getViewForPosition(i);//从缓存取出
addView(itemView);//将itemView加入到RecyclerView中
//对子View进行测量
measureChildWithMargins(itemView, 0, 0);
//拿到宽高(包括ItemDecoration)
int width = getDecoratedMeasuredWidth(itemView);
int height = getDecoratedMeasuredHeight(itemView);
//布局,将itemView列出并摆放对应的位置在RecyclerView坐标中
layoutDecorated(itemView, 0, offSetY, width, offSetY + height);
offSetY += height;
}
}
这个函数中,主要做了两件事:
(1)将对应item的View加进来
for (int i = 0; i < getItemCount(); i++) {
View itemView = recycler.getViewForPosition(i);
addView(itemView);
······
}
首先getItemCount()
获取item的个数,然后通过getViewForPosition()
获取item,最后addView()添加到RecyclerView中。
(2)把所有item摆放在他应在的位置
for (int i = 0; i < getItemCount(); i++) {
·····
measureChildWithMargins(itemView, 0, 0);
int width = getDecoratedMeasuredWidth(itemView);
int height = getDecoratedMeasuredHeight(itemView);
layoutDecorated(itemView, 0, offSetY, width, offSetY + height);
offSetY += height;
}
通过measureChildWithMargins()
方法测量itemView,并通过getDecoratedMeasuredWidth()
得到测量出来的宽度,注意这里的宽度是item+decoration的总宽度,如果你只想要item的宽度调用getMeasuredWidth()
即可;
然后通过layoutDecorated()
将itemView列出并摆放对应的位置在RecyclerView坐标中,每个item的左右位置都是相同的,左侧从X=0开始计算,只是Y需要计算,因为每个item的Y的坐标都是不一样的,所有这里有个变量offSetY,表示累加当前item之前的所有item的高度,从而计算出当前item的Y的坐标;
我们运行一下,效果如下:
item是出来了,但是这时候还没有滑动效果的,因为我们还没给它添加滑动。
怎么给它添加滑动效果呢?上一篇源码讲解中讲到再通过scrollVerticallyBy()
等方法控制滑动的距离等,首先重写canScrollVertically()
这个方法:
@Override
public boolean canScrollVertically() {
return true;
}
返回true表示让LayoutManager能垂直滑动,如果你想设置能水平滑动,那么将canScrollHorizontally()
返回true;接着重写scrollVerticallyBy()
方法,来控制垂直滑动的距离:
//垂直滑动的距离
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
return super.scrollVerticallyBy(dy, recycler, state);
}
来解析一下这个方法:
dy表示每次滚动接收的距离,其实dy的距离就是scrollBy(int x, int y)
滚动的距离,这里需要注意:
打印了日志:从下往上滑,dy>0:
当手指向上滑动时,需要让所有的子item向上移动,那么需要item减去dy的距离,就是item向上滑动的距离,我们可以通过offsetChildrenVertical()
来移动RecyclerView中的所有item。
//垂直滑动的距离
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
offsetChildrenVertical(-dy);
return dy;
}
scrollVerticallyBy()
需要返回移动的距离dy,我们运行一下来看看效果:
从效果图中可以看出,这里虽然实现了滚动效果,但是有问题,item滑动到顶部和底部后仍然可以滑动,超出了边界,所以需要添加判断当item超出顶部或者底部边界就不让它滑动了。
(1)判断到顶部
判断到顶比较简单,将所有dy相加,如果小于0,那么说明到顶了,不让它移动就可以了:
private int mSumDy;
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
Log.e(TAG, "移动的距离: dy == " + dy);
int offSetDy = dy;
//如果滑动到最顶部
if (mSumDy + offSetDy < 0) {
offSetDy = -mSumDy;
}
mSumDy += offSetDy;
offsetChildrenVertical(-offSetDy);//偏移RecyclerView内的item
return dy;
}
通过成员变量mSumDy保存所有移动过的距离dy,如果当前移动的距离加上之前的距离小于0,即mSumDy + offSetDy < 0
,那么就不在累加dy,让它移动到顶端(y=0)位置,因为之前移动的距离是mSumDy。
所以推算公式:
因为 mSumDy + offSetDy = 0; 所以 offSetDy = -mSumDy;
那么将它移动到顶端y=0的位置,将移动的距离设置为-mSumDy即可。效果如下图所示:
可以看到,现在到顶部不会再移动了,那么来看看到底部怎么解决?
(2)判断到底部
判断到底的方法,首先要知道所有item的总高度,用总高度减去RecyclerView的高度,就是到底部时的偏移值,如果超过这个数值就说明超出了最底部。
在onLayoutChildren()
方法中我们对每一个item进行测量并且布局,所以将所有item的高度加起来即得到item的总高度。
private int mItemTotalHeight = 0;//item总高度
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
int offSetY = 0;//垂直方向的偏移量
for (int i = 0; i < getItemCount(); i++) {
View itemView = recycler.getViewForPosition(i);//从缓存取出
addView(itemView);//将itemView加入到RecyclerView中
//对子View进行测量
measureChildWithMargins(itemView, 0, 0);
//拿到宽高(包括ItemDecoration)
int width = getDecoratedMeasuredWidth(itemView);
int height = getDecoratedMeasuredHeight(itemView);
//布局,将itemView列出并摆放对应的位置在RecyclerView坐标中
layoutDecorated(itemView, 0, offSetY, width, offSetY + height);
offSetY += height;
}
mItemTotalHeight = Math.max(offSetY, getRecyclerViewRealHeight());
}
/**
* 获取RecyclerView的真实高度
* @return
*/
private int getRecyclerViewRealHeight() {
return getHeight() - getPaddingBottom() - getPaddingTop();
}
getRecyclerViewRealHeight()
方法是为了得到RecyclerView可以显示item的真实高度,mItemTotalHeight表示总高度,但是这里注意,当item总高度大于RecyclerView真实高度时(item满屏),mItemTotalHeight就是所有item的总高度,当item总高度小于RecyclerView真实高度时(item不满屏),mItemTotalHeight就是RecyclerView的本身设置的真实高度。所以mItemTotalHeight取两者的最大值那个。
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
Log.e(TAG, "移动的距离: dy == " + dy);
int offSetDy = dy;
//如果滑动到底部
if (mSumDy + offSetDy > mItemTotalHeight - getRecyclerViewRealHeight()) {
offSetDy = mItemTotalHeight - getRecyclerViewRealHeight() - mSumDy;
}
mSumDy += offSetDy;
offsetChildrenVertical(-offSetDy);//偏移RecyclerView内的item
return dy;
}
其中,mSumDy + offSetDy
表示当前滑动的距离,mItemTotalHeight - getRecyclerViewRealHeight()
表示滑动到底部时可移动的总距离;那么滑动到底部时,移动的距离需要怎么计算呢?
推算公式:
因为 mSumDy + offSetDy = mItemTotalHeight - getRecyclerViewRealHeight();
所以 offSetDy = mItemTotalHeight - getRecyclerViewRealHeight() - mSumDy;
滑动到底部时,即当前滑动距离(mSumDy + offSetDy) 等于 所有item的总高度(mItemTotalHeight - getRecyclerViewRealHeight());
运行一下demo,效果如下:
到顶部和到底部的问题都解决了,下面给出MySelfLayoutManager的全部代码:(源码地址在文章最后给出)
public class MySelfLayoutManager extends RecyclerView.LayoutManager {
private static final String TAG = MySelfLayoutManager.class.getSimpleName();
private int mSumDy;//垂直滑动的总距离
private int mItemTotalHeight = 0;//item总高度
@Override
public RecyclerView.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 itemView = recycler.getViewForPosition(i);//从缓存取出
addView(itemView);//将itemView加入到RecyclerView中
//对子View进行测量
measureChildWithMargins(itemView, 0, 0);
//拿到宽高(包括ItemDecoration)
int width = getDecoratedMeasuredWidth(itemView);
int height = getDecoratedMeasuredHeight(itemView);
//布局,将itemView列出并摆放对应的位置在RecyclerView坐标中
layoutDecorated(itemView, 0, offSetY, width, offSetY + height);
offSetY += height;
}
mItemTotalHeight = Math.max(offSetY, getRecyclerViewRealHeight());
}
//获取RecyclerView的真实高度
private int getRecyclerViewRealHeight() {
return getHeight() - getPaddingBottom() - getPaddingTop();
}
//能否垂直滑动
@Override
public boolean canScrollVertically() {
return true;
}
//垂直滑动的距离
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
Log.e(TAG, "移动的距离: dy == " + dy);
int offSetDy = dy;
//如果滑动到最顶部
if (mSumDy + offSetDy < 0) {
offSetDy = -mSumDy;
} else if (mSumDy + offSetDy > mItemTotalHeight - getRecyclerViewRealHeight()) {//如果滑动到底部
offSetDy = mItemTotalHeight - getRecyclerViewRealHeight() - mSumDy;
}
mSumDy += offSetDy;
offsetChildrenVertical(-offSetDy);//偏移RecyclerView内的item
return dy;
}
}
最后,我们来总结一下自定义LayoutManager的几个重要步骤:
- 1、通过recycler.getViewForPosition()获取itemVIew,并通过addView()加入到RecyclerView中;
- 2、通过measureChildWithMargins()对item进行测量;
- 3、layoutDecorated()对item布局到RecyclerView中;
- 4、canScrollVertically()设置是否可以垂直滚动;
- 5、scrollVerticallyBy()控制滑动的距离和边界判断。
自定义LayoutManager暂时完成了,但是还不完整;我们知道RecyclerView一般都是一个列表的行为,如果一次性加载多条数据是不行的,这时候就涉及到RecyclerView的回收和复用了。
至此!本文结束。
源码地址:https://github.com/FollowExcellence/RecyclerViewDemo
相关文章:
理解RecyclerView(五)
● RecyclerView的绘制流程
理解RecyclerView(六)
● RecyclerView的滑动原理
理解RecyclerView(七)
● RecyclerView的嵌套滑动机制
理解RecyclerView(八)
● RecyclerView的回收复用缓存机制详解
理解RecyclerView(九)
● RecyclerView的自定义LayoutManager