RecyclerView系列|理解RecyclerView(九)—自定义LayoutManager

前言: 等你发现时间是贼了,它早已偷光你的选择。????????——《给自己的歌-李宗盛》
一、概述 ??LayoutManager主要用于RecyclerView的布局,itemView的回收和复用,在LayoutManager能对每个item的大小、位置进行更改,做出我们想要的效果。很多优秀的效果都是通过自定义LayoutManager来实现的。在前面的文章源码讲解中,需要自定义LayoutManager则需要重写onLayoutChildren()方法,它是布局RecyclerView的入口,再通过scrollVerticallyBy()等方法控制滑动的距离等。
这一节,我们来手动制作一个LinearLayoutManager,来看下如果自定义LayoutManager。(源码地址在文章最后给出)
二、自定义LayoutManager 2.1 自定义MySelfLayoutManager
??首先创建一个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); } }

运行一下,效果如下:
RecyclerView系列|理解RecyclerView(九)—自定义LayoutManager
文章图片

你会发现什么都没有,我们说过所有的item布局都是在LayoutManager中处理的,在MySelfLayoutManager 中并没有处理任何的item。
2.2 onLayoutChildren()
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的坐标;
我们运行一下,效果如下:
RecyclerView系列|理解RecyclerView(九)—自定义LayoutManager
文章图片

item是出来了,但是这时候还没有滑动效果的,因为我们还没给它添加滑动。
2.3 添加滚动效果
? ?怎么给它添加滑动效果呢?上一篇源码讲解中讲到再通过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); }

来解析一下这个方法:
  • scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) ? 垂直滚动dy像素在屏幕坐标,并且返回实际滚动的距离,dy表示滚动的距离,单位是像素;recycler表示负责回收管理视图的管理器;state表示RecyclerView的状态。
dy表示每次滚动接收的距离,其实dy的距离就是scrollBy(int x, int y)滚动的距离,这里需要注意:
  • dy<0 ??表示手指由上往下滑;
  • dy>0 ??表示手指由下往上滑。
打印了日志:从下往上滑,dy>0:
RecyclerView系列|理解RecyclerView(九)—自定义LayoutManager
文章图片

当手指向上滑动时,需要让所有的子item向上移动,那么需要item减去dy的距离,就是item向上滑动的距离,我们可以通过offsetChildrenVertical()来移动RecyclerView中的所有item。
//垂直滑动的距离 @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { offsetChildrenVertical(-dy); return dy; }

  • offsetChildrenVertical(int dy) ??移动所有的子View一定的距离(像素)在RecyclerView中 ;dy表示移动的距离,单位是像素。
scrollVerticallyBy()需要返回移动的距离dy,我们运行一下来看看效果:
RecyclerView系列|理解RecyclerView(九)—自定义LayoutManager
文章图片

从效果图中可以看出,这里虽然实现了滚动效果,但是有问题,item滑动到顶部和底部后仍然可以滑动,超出了边界,所以需要添加判断当item超出顶部或者底部边界就不让它滑动了。
2.4 边界滑动判断
(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即可。效果如下图所示:
RecyclerView系列|理解RecyclerView(九)—自定义LayoutManager
文章图片

可以看到,现在到顶部不会再移动了,那么来看看到底部怎么解决?
(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,效果如下:
RecyclerView系列|理解RecyclerView(九)—自定义LayoutManager
文章图片

到顶部和到底部的问题都解决了,下面给出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(九)—自定义LayoutManager】相关文章:

理解RecyclerView(五)

?● RecyclerView的绘制流程

理解RecyclerView(六)

?● RecyclerView的滑动原理

理解RecyclerView(七)

?● RecyclerView的嵌套滑动机制

理解RecyclerView(八)

?● RecyclerView的回收复用缓存机制详解

理解RecyclerView(九)

?● RecyclerView的自定义LayoutManager

    推荐阅读