Android 手动实现RecyclerView

在Android开发中,RecyclerView是一个非常重要且经常用到的框架,它的功能十分强大,且性能很好。为了弄懂RecyclerView是怎么实现的,完成了一个简单的基本原理实现,本章通过简单的不能再简单的语句与图片搞懂RecyclerView最最基本的原理。

首先搞懂这个英文名什么意思:Recycler View(可回收使用的View)。

为什么RecyclerView能够加载几万甚至上亿的数据量还能保持如此优秀的效率?其实不过是通过一个Recycler来回收使用View罢了。举个简单的例子:在饭店吃饭时,大饭店平均每天要面对成千上万的顾客。饭店通常只有几百个盘子,这些盘子被使用后并不会丢掉,而是洗好后接着用,每个盘子都会被使用很多很多次。相同的道理,RecyclerView后台通过一个SparseArray来维护回收数据,它可以提高内存效率,每个View又需要一个List来存放。为了简单方便,我直接使用了Stack数据来存放数据,每个View都需要使用一个独立的Stack来存放:
public class Recycler {

    private Stack[] stacks;

    public Recycler(int typeNumber){
        stacks = new Stack[typeNumber];
        for (int i = 0; i<typeNumber;i++){
            stacks[i] = new Stack<View>();
        }
    }

    public View get(int typeNumber){
        try {
            return (View) stacks[typeNumber].pop();
        }catch (Exception e){
            return null;
        }
    }

    public void put(View view,int type){
        stacks[type].push(view);
    }
}

这样一个简单的Recycler就好了,分别有根据View的类型存和取的方法。

接下来就是Adapter适配器,这个适配器是一个接口,需要由开发者实现,

public interface Adapter {

    View onCreateViewHolder(ViewGroup parent, int position);

    View onBindViewHolder(View recyclerView, int position);

    int getItemViewType(int position);

    int getCount();

    int getViewTypeCount();

    int getHeight();
}

分别定义了:
1、onCreateViewHolder:创建view的方法。因为View是不会无中生有的,初始化必须得创建若干个View,就好像饭店的盘子必须从超市先买才行。

2、onBindViewHolder:绑定数据到已有的View上。这时候View已经被创建,使用完后放入回收池,绑定新的数据重回放回RecyclerView中。类似于:盘子上菜了一盘龙虾,吃完后盘子拿回后厨,洗干净后重新放置新的菜上桌。

3、getItemViewType:根据item的位置获取它的种类。

4、getCount:数据总量。

5、getViewTypeCount:一共有多少种类的数据。

6、getHeight:每个数据的高度。

接着就可以定义容器布局了:

public class RecyclerView extends ViewGroup {

    public RecyclerView(Context context) {
        this(context,null);
    }

    public RecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        
    }
}

很简单的实现ViewGroup,因为RecyclerView本质上是一个View容器。

开发者必须实现Adapter的所有方法,并将接口对象返回给RecylerView,RecylerView才能根据这些实现方法摆放数据。

如何摆放数据?覆写RecylerView的onMeasure和onLayout方法:

onMeasure中,使用一个 int[]数组存放所有的item数据高度,然后设置父容器的高度:

int[] heights;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    Log.d(TAG, "onMeasure: ");
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    Log.d(TAG, "onMeasure: " + heightSize);
    if(adapter != null){
        for (int i=0;i<adapter.getCount();i++){
            heights[i] = adapter.getHeight();
        }
        int h = Math.min(sumArray(heights,0,heights.length),heightSize);
        setMeasuredDimension(widthSize,h);
    }
}

sumArray是一个工具类,用于计算高度数组从x的位置开始,后面y项数据这一数据片段的高度:

private int sumArray(int[] array, int firstIndex,int count){
    int sum = 0;
    count += firstIndex;
    for (int i = firstIndex;i<count;i++){
        sum += array[i];
    }
    return sum;
}

接着在onLayout中对添加的item进行测量并布局:

int top = 0, bottom = 0;
for (int i = 0; i < adapter.getCount() && top < height; i++) {
    bottom += heights[i];
    View view = makeAndStep(i, 0, top, width, bottom);    //获取数据
    viewList.add(view);
    top = bottom;
}

这里的for循环中加了top<height的结束判断,因为只需要加载一屏的数据就够了,后面无论多少数据都不需要加载。通过循环,把top和bottom的值循环替代可以获取所有的view。

如何获取View呢?首先从回收池取,取不到然后再创建它:

private View makeAndStep(int row, int left, int top, int right, int bottom) {
    View view = obtainView(row, right - left, bottom - top);
    view.layout(left, top, right, bottom);
    return view;
}

    private View obtainView(int position, int width, int height) {
//        key type
        int itemType= adapter.getItemViewType(position);
//       取不到
        View recyclerView = recycler.get(itemType);
        View view;
        if (recyclerView == null) {
            view = adapter.onCreateViewHolder(this,position);
            if (view == null) {
                throw new RuntimeException("onCreateViewHodler  必须填充布局");
            }
        }else {
            view = adapter.onBindViewHolder(recyclerView,position);
        }
        view.setTag(R.id.type_item, itemType);
        view.measure(MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY)
                ,MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY));
        addView(view,0);
        return view;
    }

这一步完成后,初始化的数据就添加完成了!只是现在依然不能滑动在这里插入图片描述

接着添加滑动事件:

首先获取屏幕的最小滑动距离,这个系统已经定义了,直接获取就行:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercept =false;
    switch (ev.getAction()){
        case MotionEvent.ACTION_DOWN:
            originY= (int) ev.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            if(Math.abs(originY- ev.getRawY()) > touchSlop){
                intercept = true;
            }
            break;
    }
    return intercept;
}

如果大于最小距离,那么进行拦截并处理滑动事件:

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()){
        case MotionEvent.ACTION_MOVE:
            int diffY = (int) (originY- event.getRawY());
            scrollBy(0,diffY);
            break;
    }
    return true;
}

在onTouchEvent事件中调用scrollBy方法,并进行覆写:

判断滑动的方向:通过两点之间的距离来判断方向,如果diffY大于0,那么为上滑

在这里插入图片描述
在这里插入图片描述
这里使用一个重要的参数:scrollY,它表示屏幕中第一个item的左上顶点到屏幕左上顶点的距离

上滑情况:

在这里插入图片描述
首先,灰色部分是屏幕,屏幕高度是固定的。什么时候第一项(这里的第一项仅仅表示在屏幕中的第一个数据,而不是总数据的第一个,事实上它可以是总数据中任意一段数据)可以被移除呢?可以发现,上滑的中,scrollY是不断变大的,因为scrollY表示的永远是第一个数据左上角顶点到屏幕左上角的距离。当scroll增大到临界值:

6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTI1MzM5Mw==,size_16,color_FFFFFF,t_70)

图中处于临界值,当继续上滑时,scrollY就会大于第一个数据的高度,同时此时也是第一个数据该被移除的时刻,因为继续上滑就是如下情况:
在这里插入图片描述
会发现,scrollY的值重新开始计算,因为之前的数据已被移除,第二个数据此时变成第一个了。相关代码:

while (scrollY > heights[firstRow]){
    removeView(viewList.remove(0));
    scrollY -= heights[firstRow];
    firstRow ++;
}

@Override
public void removeView(View view) {
    super.removeView(view);
    int key = (int) view.getTag(R.id.type_item);
    recycler.put(view,key);
}

一旦scrollY大于第一个数据的高度后,”删除"第一个数据,然后将scrollY重置。可以发现删除并不是真的删除,而是将他放入回收池中,等着以后被绑定数据后重新展示。

屏幕下方的数据如何添加呢?
在这里插入图片描述

可以发现,随着不断上滑x的值会不断变小(x = sumArray - scrollY - height),scrollY是不断增大的,其他2个数是定值,因此x会不断变小。当x<0时:

在这里插入图片描述

此时达到了下边添加数据的临界状态,继续上滑,x<0,添加数据,相关代码:

while(getFillHeight() < height){
    int nextItemIndex = viewList.size() + firstRow;
    View view = obtainView(nextItemIndex,width,heights[nextItemIndex]);
    viewList.add(viewList.size(),view);
}

private int getFillHeight(){
    return sumArray(heights,firstRow,viewList.size()) - scrollY;
}

首先确定下个数据的索引,之后与初始化数据相同:从回收池中先取数据,取不到再创建数据,然后添加到当前屏幕中的view列表中。

下滑情况:

下滑情况与上滑相反,不断从屏幕上边添加1数据,从下边删除数据。

在这里插入图片描述

添加数据的临界图:

在这里插入图片描述
此时scrollY可以发现为0,如果继续下滑,然后scrollY就会为负数,这个时候是添加数据的时刻:

while(scrollY<0){
    firstRow--;
    scrollY += heights[firstRow];
    View view = obtainView(firstRow,width,heights[firstRow]);
    viewList.add(0,view);
}

首先将索引减一作为新数据的索引,添加数据与之前都是先沟通的:首先从回收池中取,取不到则创建。

下边什么时候删除数据呢?看看删除数据的临界:
在这里插入图片描述

可以发现最下方的数据即将被删除,距离被删除只剩一步之遥:

在这里插入图片描述
继续下滑后,出现了一个 x,这个x本不该出现,现在x>0了,说明当x>=0的时候,数据该被删除了:
x=sumArray - item[viewList.size() - 1] - height(屏幕高度) - scrollY.相关代码:

while(sumArray(heights,firstRow,viewList.size()) - scrollY - heights[firstRow + viewList.size() - 1] - height >= 0){
    removeView(viewList.remove(viewList.size() -1));
}

最后,无论上滑还是下滑,都别忘了对viewList(当面屏幕中存在的view)中所有的view进行重新摆放(layout)。

private void repositionViews(){
    int top,bottom,i;
    top = - scrollY;
    i = firstRow;
    for (View view : viewList){
        bottom = top + heights[i++];
        view.layout(0,top,width,bottom);
        top = bottom;
    }
}

到了这里还未结束,因为最后剩下一个极限位置,那就是总数据的第一个数据如果位于屏幕最上方,继续上滑会数组越界异常,这是因为继续上滑也没有数据填充了。因为取不到一个heights[-1]的数据。同样的,如果已经滑动到数据的最低端,继续滑动也无法获得数据了,无法取得一个heights[height.length]的数据。因此就应该再添加或删除数据前进行判断:

//上滑
if(scrollY >0){
    scrollY = Math.min(scrollY,sumArray(heights,firstRow,heights.length - firstRow) - height);
}
//下滑
else{
    scrollY = Math.max(scrollY,-sumArray(heights,0,firstRow));
}

首先看下滑情况:

在这里插入图片描述

可以看到,继续下滑已经没有数据能添加了,此时scrollY<0,scrollY是不会等于0的。这里使用-sumArray(heights,0,firstRow)来和它取最大值。可以分为2种情况

正常情况(有数据补充):

此时sumArray(heights,0,firstRow)永远是一个正数,因此-sumArray(heights,0,firstRow)就是一个负数,理由是firstRow代表的总数据的索引值,firstRow可以是1、2、3.。。因此scrollY永远会大于-sumArray(heights,0,firstRow),没有影响。

极限情况(没有数据可补充了):

滑动到最顶层,此时firstRow为0,-sumArray(heights,0,firstRow)(表示总数据中从第一项开始,到第0项的数据)为0,但是此时scrollY为负数,因此0大于负数,scrollY被赋值为0了

                             正常情况都取这个值                           极限情况取这个值
scrollY = Math.max(    scrollY,                         -sumArray(heights,0,firstRow));

上滑情况也相同:

在这里插入图片描述
如图:滑动数据最低端,此时屏幕中数据的总长度为:

sumArray(heights,firstRow,heights.length - firstRow)

正常情况:如下图:
sumArray(heights,firstRow,heights.length - firstRow)- height的长度一定为大于scrollY,因为scrollY + 一个item的高度 > scrollY,因为总会加上下面新添加上来的一个高度。

在这里插入图片描述

极限情况:scrollY + item高度 = scrollY。这个情况说明item高度为0 👉 最下方没有数据添加上来了👉已经滑动到数据最底端了。

                               正常情况取这个值                              极限情况取这个值
 scrollY = Math.min(   scrollY,                   sumArray(heights,firstRow,heights.length - firstRow) - height);

在这里插入图片描述

demo地址:https://github.com/lyx19970504/RecylerView_By_Self

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哒哒呵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值