理解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(九)—自定义LayoutManager_第1张图片
你会发现什么都没有,我们说过所有的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(九)—自定义LayoutManager_第2张图片
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:
在这里插入图片描述
当手指向上滑动时,需要让所有的子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(九)—自定义LayoutManager_第3张图片
从效果图中可以看出,这里虽然实现了滚动效果,但是有问题,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(九)—自定义LayoutManager_第4张图片
可以看到,现在到顶部不会再移动了,那么来看看到底部怎么解决?

(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(九)—自定义LayoutManager_第5张图片
到顶部和到底部的问题都解决了,下面给出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

你可能感兴趣的:(RecyclerView系列,LayoutManager,自定义LayoutMan,定义RV布局管理器,RecyclerView,setLayoutManage)