RecyclerView 源码分析(七)自定义LayoutManager及其相关组件的源码分析

本文深入分析了RecyclerView的自定义LayoutManager及其相关组件,包括如何自定义LayoutManager,着重讲解了onLayoutChildren方法的实现,以及如何处理水平滑动。同时,详细探讨了SnapHelper的工作原理,如何实现SnapHelper的自定义,以达到滑动结束时ItemView对齐的效果。通过对源码的解析,加深了对RecyclerView布局管理的理解。
摘要由CSDN通过智能技术生成

转载自琼珶和予

自定义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:

  1. 知识储备–相关方法的解释,这里的相关方法主要是自定义涉及到的方法
  2. 自定义一个LayoutManager
  3. 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方法,也是最复杂的一步。在这一步,我们主要完成两步:

  1. 定位每个ItemView的位置,然后布局
  2. 适配滑动和缩放的效果

我们先来结合图片来分析一下这个效果。
在这里插入图片描述
整个效果我们可以这么来考虑,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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值