转载自琼珶和予
RecyclerView 源码分析(七)自定义LayoutManager及其相关组件的源码分析
自定义LayoutManager及其相关组件的源码分析
对于使用ReccyclerView的我们来说,LayoutManager早已非常熟悉。可是,有没有想过我们所说的熟悉是哪种熟悉?对的,就是会使用而已,这其中包括谷歌爸爸帮我们实现的几种LayoutManager,例如:LinearLayoutManager,GridLayoutManager等等。
仔细想一想,我们使用LayoutManager就像我们当初初学Android时使用各种基础控件,我们处于只会使用的阶段,如果后续有一些特殊的要求,系统的实现已经不能满足我们自身的需求,此时自定义LayoutManager就必须出手了。同时,如果想要自定义LayoutManager,我们就必须了解它相关的原理。所以,学习LayoutManager的源码是至关重要的。
本文参考资料:
RecyclerView系列(7)—自定义LayoutManager(上),视觉上定义一个具备上下边界的RecyclerView.layoutMnager
RecyclerView系列(8)—自定义LayoutManager(下) ,回收复用及优化
LayoutManagerGroup
介于LayoutManger的特殊性,我们不可能将LayoutManager及其所有子类的代码都分析一遍,所以本文的源码分析重点是,从源码角度来解释为什么这样自定义LayoutManager。自定义LayoutManager要求的门槛相对较高,它不是简单的照着模板来写,而是需要了解它内部的原理,这其中包括回收机制(这个我们在分析RecyclerView的三大流程时已经从LinearLayoutManager内部看到了),滑动机制等等。所以,在自定义LayoutManager时,我默认大家都懂得这些原理,如果还有同学不懂的话,可以参考文章:
RecyclerView 源码分析(一) - RecyclerView的三大流程
RecyclerView 源码分析(二) - RecyclerView的滑动机制
RecyclerView 源码分析(三) - RecyclerView的缓存机制
本文打算从如下几个角度来分析LayoutManager:
- 知识储备–相关方法的解释,这里的相关方法主要是自定义涉及到的方法
- 自定义一个LayoutManager
- SnapHelper基本使用、源码分析和自定义SnapHelper
1. 概述
在正式分析LayoutManager之前,我们先来对LayoutManager及其它的相关组件做一个简单的概述。
我们都知道LayoutManager就是一个布局管理器,主要负责RecyclerView的ItemView测量和布局,所以自定义LayoutManager的过程跟自定义View的过程非常的相似。本文打算从一个Demo开始来介绍怎么自定义一个LayoutManager,效果如下:
同时在这里,我们还介绍了跟LayoutManager相关的两个组件–SnapHelper和SmoothScroller。这个其中SnapHelper主要负责来调整RecyclerView的滑动距离,比如想要在滑动结束之后,ItemView停留在RecyclerView正中央,可以依靠SnapHelper。
2. LayoutManager的相关方法
我们在自定义LayoutManager之前,先来看一下LayoutManager的几个方法。
方法名 | 作用 |
---|---|
generateDefaultLayoutParams | 抽象方法,必须实现。这个方法的作用主要是给RecyclerView的ItemView生成LayoutParams |
onMeasure | 用来测量RecyclerView的大小的。通常不用重写此方法 ,但是在一种情况下必须重写,就是LayouytManager不支持自动测量,这种情况下RecyclerView不会进行自我测量,会调用LayoutManager的onMeasure方法来测量 。 |
onLayoutChildren | 此方法的作用是布局ItemView。此方法就像是ViewGroup的onLayout方法,RecyclerView内部的ItemView怎么布局,全看这个方法怎么实现 。 |
canScrollHorizontally | 设置该LayoutManager的RecyclerView是否可以水平滑动。与之对应的还有canScrollVertically,用来设置RecyclerView是否垂直滑动 |
scrollHorizontallyBy | 水平可以滑动的距离。此方法带一个dx参数,表示RecyclerView已经产生了dx的滑动距离 ,此时我们需要做的是调用相关方法,进行重新布局 。同时此方法的返回值表示水平可以滑动的距离。与之对应的方法是scrollVerticallyBy 。 |
3. 自定义LayoutManager
简单的了解了自定义LayoutManager的几个方法,现在我将带领来实现一个Demo,具体的效果就是上面的gif动图,我们来看看怎么自己实现一个LayoutMananger。
(1) 重写generateDefaultLayoutParams方法
首先,自定义LayoutManager的第一步就是重写generateDefaultLayoutParams方法,这个方法的作用在上面我已经介绍了,在这里就不介绍了。通常来说,我们这样来实现generateDefaultLayoutParams方法就行了:
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}
我们这里没有特殊的要求,所以让每个ItemView的自适应就行了。
(2) onLayoutChildren方法
然后,第二步就是重写onLayoutChildren方法,也是最复杂的一步。在这一步,我们主要完成两步:
- 定位每个ItemView的位置,然后布局。
- 适配滑动和缩放的效果。
我们先来结合图片来分析一下这个效果。
整个效果我们可以这么来考虑,ItemView是从左往右开始布局,不过我们得从从右往左计算每个ItemView的宽高,因为最右边的ItemView宽高是最原始,同时它的left位置也是最容易的计算(RecyclerView的水平空闲空间减去ItemView的width就行。)。
然后我们可以设置一个offset,后面的ItemView根据这个offset来重新定位。我们通过之前看LinearLayoutManager源码的经验,发现LinearLayoutManager计算位置通过一个remainSpace变量来实现的。remainSpace表示当前RecyclerView的剩余空间,每布局一个ItemView,remainSpace减去消耗的距离就OK!
下面我结合代码来具体分析:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.getItemCount() == 0 || state.isPreLayout()) return;
removeAndRecycleAllViews(recycler);
if (!mHasChild) {
mItemViewHeight = getVerticalSpace();
mItemViewWidth = (int) (mItemViewHeight / mItemHeightWidthRatio);
mHasChild = true;
}
mItemCount = getItemCount();
mScrollOffset = makeScrollOffsetWithinRange(mScrollOffset);
fill(recycler);
}
在onLayoutChildren方法里面,我们初始化了几个变量,其中mItemViewHeight和mItemViewWidth两个变量分别表示ItemView的高和宽。其次就是mScrollOffset的初始化:
private int makeScrollOffsetWithinRange(int scrollOffset) {
return Math.min(Math.max(mItemViewWidth, scrollOffset), mItemCount * mItemViewWidth);
}
第一次调用onLayoutChildren方法来初始化mScrollOffset时,mScrollOfffet的值被设置为mItemCount * mItemViewWidth。这有什么意义呢?我待会会解释。
在onLayoutChidlren方法的最后,调用fill方法。fill方法才是真正计算每个ItemView的位置,我们来看看:
private void fill(RecyclerView.Recycler recycler) {
// 1.初始化基本变量
int bottomVisiblePosition = mScrollOffset / mItemViewWidth;
final int bottomItemVisibleSize = mScrollOffset % mItemViewWidth;
final float offsetPercent = bottomItemVisibleSize * 1.0f / mItemViewWidth;
final int space = getHorizontalSpace();
int remainSpace = space;
final int defaultOffset = mItemViewWidth / 2;
final List