广告位联系
返回顶部
分享到

Android手写RecyclerView实现列表加载

Android 来源:互联网 作者:佚名 发布时间:2022-08-28 20:59:12 人浏览
摘要

前言 我相信一点,只要我们的产品中,涉及到列表的需求,肯定第一时间想到RecyclerView,即便是自定义View,那么RecyclerView也会是首选,为什么会选择RecyclerView而不是ListView,主要就是

前言

我相信一点,只要我们的产品中,涉及到列表的需求,肯定第一时间想到RecyclerView,即便是自定义View,那么RecyclerView也会是首选,为什么会选择RecyclerView而不是ListView,主要就是RecyclerView的内存复用机制,这也是RecyclerView的核心 

 当RecyclerView展示列表信息的时候,获取ItemView的来源有2个:一个是从适配器拿,另一个是从复用池中去拿;一开始的时候就是从复用池去拿,如果复用池中没有,那么就从Adapter中去拿,这个时候就是通过onCreateViewHolder来创建一个ItemView。

1 RecyclerView的加载流程

 首先,当加载第一屏的时候,RecyclerView会向复用池中请求获取View,这个时候复用池中是空的,因此就需要我们自己创建的Adapter,调用onCreateViewHolder创建ItemView,然后onBindViewHolder绑定数据,展示在列表上 

当我们滑动的时候第一个ItemView移出屏幕时,会被放到复用池中;同时,底部空出位置需要加载新的ItemView,触发加载机制,这个时候复用池不为空,拿到复用的ItemView,调用Adapter的onBIndViewHolder方法刷新数据,加载到尾部;

这里有个问题,放在复用池的仅仅是View吗?其实不是的,因为RecyclerView可以根据type类型加载不同的ItemView,那么放在复用池中的ItemView也是根据type进行归类,当复用的时候,根据type取出不同类型的ItemView;

例如ItemView07的类型是ImageView,那么ItemView01在复用池中的类型是TextView,那么在加载ItemView07时,从复用池中是取不到的,需要Adapter新建一个ImageView类型的ItemView。

2 自定义RecyclerView

其实RecyclerView,我们在使用的时候,知道怎么去用它,但是内部的原理并不清楚,而且就算是看了源码,时间久了就很容易忘记,所以只有当自己自定义RecyclerView之后才能真正了解其中的原理。

2.1 RecyclerView三板斧

通过第一节的加载流程,我们知道RecyclerView有3个重要的角色:RecyclerView、适配器、复用池,所以在自定义RecyclerView的时候,就需要先创建这3个角色;

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

/**

 * 自定义RecyclerView

 */

public class MyRecyclerView extends ViewGroup {

     

    public MyRecyclerView(Context context) {

        super(context);

    }

    public MyRecyclerView(Context context, AttributeSet attrs) {

        super(context, attrs);

    }

 

    public MyRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

    }

 

    @Override

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

 

    }

    @Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

    @Override

    public boolean onTouchEvent(MotionEvent event) {

        return super.onTouchEvent(event);

    }

    @Override

    public boolean onInterceptTouchEvent(MotionEvent ev) {

        return super.onInterceptTouchEvent(ev);

    }

    @Override

    public void scrollBy(int x, int y) {

        super.scrollBy(x, y);

 

    }

    interface Adapter<VH extends ViewHolder>{

        VH onCreateViewHolder(ViewGroup parent,int viewType);

        void onBindViewHolder(VH holder,int position);

        int getItemCount();

        int getItemViewType(int position);

    }

}

1

2

3

4

5

/**

 * 复用池

 */

public class MyRecyclerViewPool {

}

1

2

3

4

5

6

7

8

9

10

11

/**

 * Rv的ViewHolder

 */

public class ViewHolder {

 

    private View itemView;

 

    public ViewHolder(View itemView) {

        this.itemView = itemView;

    }

}

真正在应用层使用到的就是MyRecyclerView,通过设置Adapter实现View的展示

2.2 初始化工作

从加载流程中,我们可以看到,RecyclerView是协调Adapter和复用池的关系,因此在RecyclerView内部是持有这两个对象的引用的。

1

2

3

4

5

6

7

8

//持有Adapter和复用池的引用

private Adapter mAdapter;

private MyRecyclerViewPool myRecyclerViewPool;

//Rv的宽高

private int mWidth;

private int mHeight;

//itemView的高度

private int[] heights;

那么这些变量的初始化,是在哪里做的呢?首先肯定不是在构造方法中做的,我们在使用Adapter的时候,会调用setAdapter,其实就是在这个时候,进行初始化的操作。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

public void setAdapter(Adapter mAdapter) {

    this.mAdapter = mAdapter;

    this.needLayout = true;

    //刷新页面

    requestLayout();

}

 

/**

 * 对子View进行位置计算摆放

 * @param changed

 * @param l

 * @param t

 * @param r

 * @param b

 */

@Override

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

    if(changed || needLayout){

        needLayout = false;

        mWidth = r - l;

        mHeight = b - t;

    }

}

每次调用setAdapter的时候,都会调用requestLayout刷新重新布局,这个时候会调用onLayout,因为onLayout的调用很频繁非常耗性能,因此我们通知设置一个标志位needLayout,只有当需要刷新的时候,才能刷新重新摆放子View

2.3 ItemView的获取与摆放

其实在RecyclerView当中,是对每个子View进行了测量,得到了它们的宽高,然后根据每个ItemView的高度摆放,这里我们就写死了高度是200,仅做测试使用,后续优化。

那么在摆放的时候,比如我们有200条数据,肯定不会把200条数据全部加载进来,默认就展示一屏的数据,所以需要判断如果最后一个ItemView的bottom超过了屏幕的高度,就停止加载。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

@Override

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

    if(changed || needLayout){

        needLayout = false;

        if(mAdapter != null){

            mWidth = r - l;

            mHeight = b - t;

 

            //计算每个ItemView的宽高,然后摆放位置

            rowCount = mAdapter.getItemCount();

            //这里假定每个ItemView的高度为200,实际Rv是需要测量每个ItemView的高度

            heights = new int[rowCount];

            for (int i = 0; i < rowCount; i++) {

                heights[i] = 200;

            }

            //摆放 -- 满第一屏就停止摆放

            for (int i = 0; i < rowCount; i++) {

                bottom = top + heights[i];

                //获取View

                ViewHolder holder = getItemView(i,0,top,mWidth,bottom);

                viewHolders.add(holder);

                //第二个top就是第一个的bottom

                top = bottom;

            }

 

        }

    }

}

我们先拿到之前的图,确定下子View的位置 

 其实每个子View的left都是0,right都是RecyclerView的宽度,变量就是top和bottom,其实从第2个ItemView开始,top都是上一个ItemView的bottom,那么bottom就是 top + ItemView的高度

在确定了子View的位置参数之后,就可以获取子View来进行摆放,其实在应用层是对子View做了一层包装 --- ViewHolder,因此这里获取到的也是ViewHolder。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

private ViewHolder getItemView(int row,int left, int top, int right, int bottom) {

 

    ViewHolder viewHolder = obtainViewHolder(row,right - left,bottom - top);

    viewHolder.itemView.layout(left,top,right,bottom);

    return viewHolder;

}

 

private ViewHolder obtainViewHolder(int row, int width, int height) {

 

    ViewHolder viewHolder = null;

    //首先从复用池中查找

 

    //如果找不到,那么就通过适配器生成

    if(mAdapter !=null){

        viewHolder = mAdapter.onCreateViewHolder(this,mAdapter.getItemViewType(row));

    }

    return viewHolder;

}

通过调用obtainViewHolder来获取ViewHolder对象,其实是分2步的,首先 是从缓存池中去拿,在第一节加载流程中提及到,缓存池中不只是存了一个ItemView的布局,而是通过type标注了ItemView,所以从缓存池中需要根据type来获取,如果没有获取到,那么就调用Adapter的onCreateViewHolder获取,这种避免了每个ItemView都通过onCreateViewHolder创建,浪费系统资源;

在拿到了ViewHolder之后,调用根布局ItemView的layout方法进行位置摆放。

2.4 复用池

前面我们提到,在复用池中不仅仅是缓存了一个布局,而是每个type都对应一组回收的Holder,所以在复用池中存在一个容器存储ViewHolder

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

/**

 * 复用池

 */

public class MyRecyclerViewPool {

 

    static class scrapData{

        List<ViewHolder> viewHolders = new ArrayList<>();

    }

 

    private SparseArray<scrapData> array = new SparseArray<>();

 

    /**

     * 从缓存中获取ViewHolder

     * @param type ViewHolder的类型,用户自己设置

     * @return ViewHolder

     */

    public ViewHolder getRecyclerView(int type){

 

    }

 

    /**

     * 将ViewHolder放入缓存池中

     * @param holder

     */

    public void putRecyclerView(ViewHolder holder){

 

    }

}

当RecyclerView触发加载机制的时候,首先会从缓存池中取出对应type的ViewHolder;当ItemView移出屏幕之后,相应的ViewHolder会被放在缓存池中,因此存在对应的2个方法,添加及获取

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

/**

 * 从缓存中获取ViewHolder

 *

 * @param type ViewHolder的类型,用户自己设置

 * @return ViewHolder

 */

public static ViewHolder getRecyclerView(int type) {

    //首先判断type

    if (array.get(type) != null && !array.get(type).viewHolders.isEmpty()) {

 

        //将最后一个ViewHolder从列表中移除

        List<ViewHolder> scrapData = array.get(type).viewHolders;

        for (int i = scrapData.size() - 1; i >= 0; i--) {

            return scrapData.remove(i);

        }

    }

    return null;

}

 

/**

 * 将ViewHolder放入缓存池中

 *

 * @param holder

 */

public static void putRecyclerView(ViewHolder holder) {

 

    int key = holder.getItemViewType();

    //获取集合

    List<ViewHolder> viewHolders = getScrapData(key).viewHolders;

    viewHolders.add(holder);

}

 

private static ScrapData getScrapData(int key) {

    ScrapData scrapData = array.get(key);

    if(scrapData == null){

        scrapData = new ScrapData();

        array.put(key,scrapData);

    }

    return scrapData;

}

2.5 数据更新

无论是从缓存池中拿到了缓存的ViewHolder,还是通过适配器创建了ViewHolder,最终都需要将ViewHolder进行数据填充

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

private ViewHolder obtainViewHolder(int row, int width, int height) {

 

    int itemViewType = mAdapter.getItemViewType(row);

    //首先从复用池中查找

    ViewHolder viewHolder = MyRecyclerViewPool.getRecyclerView(itemViewType);

    //如果找不到,那么就通过适配器生成

    if(viewHolder == null){

        viewHolder = mAdapter.onCreateViewHolder(this,itemViewType);

    }

    //更新数据

    if (mAdapter != null) {

        mAdapter.onBindViewHolder(viewHolder, row);

        //设置ViewHOlder的类型

        viewHolder.setItemViewType(itemViewType);

 

        //测量

        viewHolder.itemView.measure(

                MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),

                MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)

        );

        addView(viewHolder.itemView);

    }

    return viewHolder;

}

如果跟到这里,我们其实已经完成了RecyclerView的基础功能,一个首屏列表的展示

3 RecyclerView滑动事件处理

3.1 点击事件与滑动事件

对于RecyclerView来说,我们需要的其实是对于滑动事件的处理,对于点击事件来说,通常是子View来响应,做相应的跳转或者其他操作,所以对于点击事件和滑动事件,RecyclerView需要做定向的处理。

那么如何区分点击事件和滑动事件?

1

2

3

4

5

6

7

8

9

10

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

 

    switch (ev.getAction()){

 

        case MotionEvent.ACTION_MOVE:

            return true;

    }

    return false;

}

在容器中,如果碰到MOVE事件就拦截就认为是滑动事件,这种靠谱吗?显然 不是的,当手指点击到屏幕上时,首先系统会接收到一次ACTION_DWON时间,在手指抬起之前,ACTION_DWON只会响应一次,而且ACTION_MOVE会有无数次,因为人体手指是有面积的,当我们点下去肯定不是一个点,而是一个面肯定会存在ACTION_MOVE事件,但这种我们会认为是点击事件;

所以对于滑动事件,我们会认为当手指移动一段距离之后,超出某个距离就是滑动事件,这个最小滑动距离通过ViewConfiguration来获取。

1

2

3

4

private void init(Context context) {

    ViewConfiguration viewConfiguration = ViewConfiguration.get(context);

    this.touchSlop = viewConfiguration.getScaledTouchSlop();

}

因为列表我们认为是竖直方向滑动的,所以我们需要记录手指在竖直方向上的滑动距离。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

    //判断是否拦截

    boolean intercept = false;

 

    switch (ev.getAction()){

        case MotionEvent.ACTION_DOWN:

            mCurrentY = (int) ev.getY();

            break;

 

        case MotionEvent.ACTION_MOVE:

            //y值在不停改变

            int y = (int) ev.getY();

            if(Math.abs(y - mCurrentY) > touchSlop){

                //认为是滑动了

                intercept = true;

            }

            break;

    }

    return intercept;

}

我们通过intercept标志位,来判断当前是否在进行滑动,如果滑动的距离超出了touchSlop,那么就将事件拦截,在onTouchEvent中消费这个事件。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@Override

public boolean onTouchEvent(MotionEvent event) {

    switch (event.getAction()) {

        case MotionEvent.ACTION_MOVE: {

            //判断滑动的方向

            int diff = (int) (mCurrentY - event.getRawY());

            if(Math.abs(diff) > touchSlop){

                Log.e(TAG,"diff --- "+diff);

                scrollBy(0, diff);

                mCurrentY = (int) event.getRawY();

            }

            break;

        }

    }

    return super.onTouchEvent(event);

}

3.2 scrollBy和scrollTo

在onTouchEvent中,我们使用了scrollBy进行滑动,那么scrollBy和scrollTo有什么区别,那就根据Android的坐标系开始说起 

 scrollBy滑动,其实是滑动的偏移量,相对于上一次View所在的位置,例如上图中,View上滑,偏移量就是(200 - 100 = 100),所以调用scrollBy(0,100)就是向上滑动,反之就是上下滑动;

scrollTo滑动,滑动的是绝对距离,例如上图中,View上滑,那么需要传入详细的坐标scrollTo(200,100),下滑scrollTo(200,300),其实scrollBy内部调用也是调用的scrollTo,所以偏移量就是用来计算绝对位置的。

3.3 滑动带来的View回收

当滑动屏幕的时候,有一部分View会被滑出到屏幕外,那么就涉及到了View的回收和View的重新摆放。

首先分析向上滑动的操作,首先我们用scrollY来标记,屏幕中第一个子View左上角距离屏幕左上角的距离,默认就是0.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

@Override

public void scrollBy(int x, int y) {

    super.scrollBy(x, y);

    scrollY += y;

 

    if (scrollY > 0) {

        Log.e(TAG, "上滑");

        //防止一次滑动多个子View出去

        while (scrollY > heights[firstRow]) {

            //被移除,放入回收池

            if (!viewHolders.isEmpty()) {

                removeView(viewHolders.remove(0));

            }

            scrollY -= heights[firstRow];

            firstRow++;

        }

 

    } else {

        Log.e(TAG, "下滑");

    }

}

在这里插入图片描述

 当ItemView1移出屏幕之后,因为上滑scrollY > 0,所以scrollY肯定会超过Itemiew 的高度,这里有个情况就是,如果一次滑出去多个ItemView,那么高度肯定是超过单个ItemView的高度,这里用firstRow来标记,当前子View在数据集合中的位置,所以这里使用的是while循环。

1

2

3

4

5

6

7

8

9

10

11

/**

 * 移除ViewHolder,放入回收池

 *

 * @param holder

 */

private void removeView(ViewHolder holder) {

    MyRecyclerViewPool.putRecyclerView(holder);

    //系统方法,从RecyclerView中移除这个View

    removeView(holder.itemView);

    viewHolders.remove(holder);

}

如果滑出去多个子View,那么就循环从viewHolders(当前屏幕展示的View的集合)中移除,移除的ViewHolder就被放在了回收池中,然后从当前屏幕中移除;

3.4 加载机制

既然有移除,那么就会有新增,当底部出现空缺的时候,就会触发加载机制,那么每次移除一个元素,都会有一个元素添加进来吗?其实不然 

 像ItemView1移除之后,最底部的ItemView还没有完全展示出来,其实是没有触发加载的,那么什么时候触发加载呢?

在当前屏幕中展示的View其实是在缓存中的,那么只要计算缓存中全部ItemView的高度跟屏幕的高度比较,如果不足就需要填充。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

//如果小于屏幕的高度

 while (getRealHeight(firstRow) <= mHeight) {

     //触发加载机制

     int addIndex = firstRow + viewHolders.size();

     ViewHolder viewHolder = obtainViewHolder(addIndex, mWidth, heights[addIndex]);

     viewHolders.add(viewHolders.size(), viewHolder);

     Log.e(TAG,"添加一个View");

 }

/**

 * 获取实际展示的高度

 *

 * @param firstIndex

 * @return

 */

private int getRealHeight(int firstIndex) {

    return getSumArray(firstRow, viewHolders.size()) - scrollY;

}

 

private int getSumArray(int firstIndex, int count) {

    int totalHeight = 0;

    count+= firstIndex;

    for (int i = firstIndex; i < count; i++) {

        totalHeight += heights[i];

    }

    return totalHeight;

}

这样其实就实现了,一个View移除屏幕之后,会有一个新的View添加进来

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

/**

 * 重新摆放View

 */

private void repositionViews() {

    int left = 0;

    int top = -scrollY;

    int right = mWidth;

    int bottom = 0;

 

    int index = firstRow;

 

    for (int i = 0; i < viewHolders.size(); i++) {

        bottom = top + heights[index++];

        viewHolders.get(i).itemView.layout(left,top,right,bottom);

        top = bottom;

    }

}

当然新的View只要添加进来,就需要对他进行重新摆放,这样上滑就实现了(只有上滑哦) 

3.5 RecyclerView下滑处理

在此之前,我们处理了上滑的事件,顶部的View移出,下部分的View添加进来,那么下滑正好相反。 

 那么下滑添加View的时机是什么呢?就是scrollY小于0的时候,会有新的View添加进来

1

2

3

4

5

6

7

8

9

10

11

//下滑顶部添加View

while (scrollY < 0) {

 

    //获取ViewHolder

    ViewHolder viewHolder = obtainViewHolder(firstRow - 1, mWidth, heights[firstRow - 1]);

    //放到屏幕缓存ViewHolder最顶部的位置

    viewHolders.add(0, viewHolder);

    firstRow--;

    //当顶部ItemView完全加进来之后,需要改变scrollY的值

    scrollY += heights[firstRow];

}

此时需要将添加的View,放在屏幕展示View缓存的首位,然后firstRow需要-1;

那么当新的View添加进来之后,底部View需要移除,那么移除的时机是什么呢?先把尾部最后一个View的高度抛开,继续往下滑动,如果当前屏幕展示的View的高度超过了屏幕高度,那么就需要移除

1

2

3

4

5

6

//底部移除View

while (!viewHolders.isEmpty() &&

        getRealHeight(firstRow) - viewHolders.get(viewHolders.size() - 1).itemView.getHeight() >= mHeight) {

    //需要移除

    removeView(viewHolders.remove(viewHolders.size() - 1));

}

3.6 边界问题

当我们上滑或者下滑的时候,firstRow都在递增或者递减,但是firstRow肯定是有边界的,例如滑到最上端的时候,firstRow最小就是0,如果再-1,那么就会数组越界,最下端也有边界,那就是数组的最大长度。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

/**

 * @param scrollY

 * @param firstRow

 */

private void scrollBounds(int scrollY, int firstRow) {

 

    if (scrollY > 0) {

        //上滑

        if (getSumArray(firstRow, heights.length - firstRow) - scrollY > mHeight) {

            this.scrollY = scrollY;

        } else {

            this.scrollY = getSumArray(firstRow, heights.length - firstRow) - mHeight;

        }

    } else {

        //下滑

        this.scrollY = Math.max(scrollY, -getSumArray(0, firstRow));

    }

}

首先看下滑,这个时候firstRow > 0,这个时候getSumArray的值是逐渐减小的,等到最顶部,也就是滑到firstRow = 0的时候,这个时候getSumArray = 0,那么再往下滑其实还是能滑的,这个时候我们需要做限制,取scrollY 和 getSumArray的最大值,如果一致下滑,getSumArray一致都是0,然后scrollY < 0,最终scrollY = 0,不会再执行下滑的操作了。

接下来看上滑,正常情况下,如果200条数据,那么当firstRow = 10的时候,剩下190个ItemView的高度(减去上滑的高度)肯定是高于屏幕高度的,那么一直滑,当发现剩余的ItemView的高度不足以占满整个屏幕的时候,就是没有数据了,这个时候,其实就可以把scrollY设置为0,不能再继续滑动了。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

@Override

 public void scrollBy(int x, int y) {

//        super.scrollBy(x, y);

 

     scrollY += y;

     scrollBounds(scrollY, firstRow);

 

 

     if (scrollY > 0) {

         Log.e(TAG, "上滑");

         //防止一次滑动多个子View出去

         while (scrollY > heights[firstRow]) {

             //被移除,放入回收池

             if (!viewHolders.isEmpty()) {

                 removeView(viewHolders.remove(0));

             }

             scrollY -= heights[firstRow];

             firstRow++;

             Log.e("scrollBy", "scrollBy 移除一个View size =="+viewHolders.size());

         }

 

         //如果小于屏幕的高度

         while (getRealHeight(firstRow) < mHeight) {

             //触发加载机制

             int addIndex = firstRow + viewHolders.size();

             ViewHolder viewHolder = obtainViewHolder(addIndex, mWidth, heights[addIndex]);

             viewHolders.add(viewHolders.size(), viewHolder);

             Log.e("scrollBy", "scrollBy 添加一个View size=="+viewHolders.size());

         }

         //重新摆放

         repositionViews();

 

     } else {

         Log.e(TAG, "下滑");

 

         //底部移除View

         while (!viewHolders.isEmpty() &&

                 getRealHeight(firstRow) - viewHolders.get(viewHolders.size() - 1).itemView.getHeight() >= mHeight) {

             //需要移除

             removeView(viewHolders.remove(viewHolders.size() - 1));

         }

 

         //下滑顶部添加View

         while (scrollY < 0) {

 

             //获取ViewHolder

             ViewHolder viewHolder = obtainViewHolder(firstRow - 1, mWidth, heights[firstRow - 1]);

             //放到屏幕缓存ViewHolder最顶部的位置

             viewHolders.add(0, viewHolder);

             firstRow--;

             //当顶部ItemView完全加进来之后,需要改变scrollY的值

             scrollY += heights[firstRow];

         }

     }

 }

OK,这其实跟RecyclerView的源码相比,简直就是一个穷人版的RecyclerView,但是其中的思想我们是可以借鉴的,尤其是回收池的思想,在开发中是可以借鉴的,下面展示的就是最后的成果 


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://juejin.cn/post/7103054421856092168
相关文章
  • Kotlin的Collection与Sequence操作异同点介绍

    Kotlin的Collection与Sequence操作异同点介绍
    在Android开发中,集合是我们必备的容器,Kotlin的标准库中提供了很多处理集合的方法,而且还提供了两种基于容器的工作方式:Collection 和
  • 实现一个Kotlin函数类型方法

    实现一个Kotlin函数类型方法
    接口与函数类型 业务开发中,经常会有实现一个函数式接口(即接口只有一个方法需要实现)的场景,大家应该都会不假思索的写出如下代
  • Android10 App启动Activity源码分析
    ActivityThread的main方法 让我们把目光聚焦到ActivityThread的main方法上。 ActivityThread的源码路径为/frameworks/base/core/java/android/app/ActivityThread。 1 2
  • Android10客户端事务管理ClientLifecycleManager源码解析

    Android10客户端事务管理ClientLifecycleManager源码解析
    在Android 10 App启动分析之Activity启动篇(二)一文中,简单地介绍了Activity的生命周期管理器是如何调度Activity进入onCreate生命周期的流程。这
  • Kotlin对象的懒加载方式by lazy与lateinit异同介绍

    Kotlin对象的懒加载方式by lazy与lateinit异同介绍
    属性或对象的延时加载是我们相当常用的,一般我们都是使用 lateinit 和 by lazy 来实现。 他们两者都是延时初始化,那么在使用时那么他们两
  • Android类加载流程分析

    Android类加载流程分析
    本文分析的代码基于Android8.1.0源码。 流程分析 从loadClass开始,我们来看下Android中类加载的流程 /libcore/ojluni/src/main/java/java/lang/ClassLoader.ja
  • Android实现读写USB串口数据的代码

    Android实现读写USB串口数据的代码
    最近在研究USB方面的内容;先后做了关于Android读写HID、串口设备的DEMO。本文比较简单,主要介绍的是Android实现读取串口数据的功能 废话不
  • Epoxy - 在RecyclerView中构建复杂界面
    Diffing 对于复杂数据结构支持的多个视图类型展示在屏幕上, Epoxy此时是尤其有用的. 在这些场景中, 数据可能会被网络请求, 异步 Observable, 用
  • Android性能优化的详细介绍

    Android性能优化的详细介绍
    性能优化是一个app很重要的一部分,一个性能优良的app从被下载到启动到使用都能给用户到来很好的体验。自然我们做性能优化也是从被下
  • Android进阶宝典-插件化2(Hook启动插件中四大组件

    Android进阶宝典-插件化2(Hook启动插件中四大组件
    在上一节,我们主要介绍了如果通过反射来加载插件中的类,调用类中的方法;既然插件是一个apk,其实最重要的是启动插件中的Activity、
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计