手写RecycleView

网易云课堂笔记

一、手写RecycleView的重点

1.View的回收池
2.如何设计适配器
3.滑动的边界
4.子View如何布局

二、回收池和适配器

  1. RecycleView内部有一个回收池负责缓存滑出屏幕的View,设计缓存是需要考虑到RecycleView的Item可能有多重布局样式,如何缓存下这些View并且区分出他们的种类。

考虑到RecycleView滑出屏幕一个View就需要一个新的Item进入的情况,使用 Stack作为缓存池,并且针对每一种Item的布局都分配一个Stack

在构造方法中可以看到针对每一种View都分配了一个Stack<View>,而这些Stack<View>对象又由一个数组对象views来管理,当然也可以由一个Map来管理

/**
 * 回收池:回收 View 根据 Type 来存放回收的 View 对象
 * 选取集合需要考虑到滑动的情况,先进先出的特性,因为 RecycleView 的一个 Item 滑出屏幕后有可能会被立即取出
 */
public class Recycler {
    private Stack<View>[] views;

    public Recycler(int typeNumber) {
        views = new Stack[typeNumber];
        for (int i = 0; i < typeNumber; i++) {

            views[i] = new Stack<View>();
        }
    }

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

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

    }

}

  1. RecycleView 调用 Adapter的 onCreateViewHolder 创建一个Item,当第一屏的item都满时,完成第一屏的加载。当手指滑动时,划出屏幕的item会进入回收池,这时候屏幕加载新的item时会去回收池查看是否有item,并且布局和新进入的item一致,一致的话从回收池中拿出这个item进行复用,复用的方式是将这个item交给适配器(因为数据不一致),适配器拿到item后进行刷新,然后再绘制到屏幕上

三、适配器

RecycleView需要知道

1.一共有多少条数据要渲染
2.Item有多少个种类
3.创建布局
4.使用缓存的View刷新布局

参考以有的适配器模式,接口如下设计

  interface Adapter {
        View onCreateViewHodler(int position, View convertView, ViewGroup parent);
        /**
         * 刷新 View 的参数
         *
         * @param position
         * @param convertView
         * @param parent
         * @return
         */
        View onBinderViewHodler(int position, View convertView, ViewGroup parent);

        //获取指定行数的 View 类型
        int getItemViewType(int row);

        //Item的类型数量
        int getViewTypeCount();

        // 数据的数量
        int getCount();

        // 每一个 Item 的高度
        public int getHeight(int index);
    }

四、RecycleView的布局

onMeasure

onMeasure 需要考虑到所有子View,sumArray就是计算出所有子View的高

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int h = 0;
        if (adapter != null) {
            // 获取到有几条数据
            this.rowCount = adapter.getCount();
            // 获取到所有数据的高
            heights = new int[rowCount];
            for (int i = 0; i < heights.length; i++) {
                heights[i] = adapter.getHeight(i);
            }
        }
        // 取布局设置的高以及数据总长度的高最小的一个
        int tmpH = sumArray(heights, 0, heights.length);
        // 取最小的高度
        h = Math.min(heightSize, tmpH);
        setMeasuredDimension(widthSize, h);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

onLayout中需要计算每一个子View的位置,这里上一个Item底部的位置就是下一个Item的Top

......
  for (int i = 0; i < rowCount && top < height; i++) {
                    right = width;
                    bottom = top + heights[i];
                    // 生成一个View
                    View view = makeAndStep(i, 0, top, right, bottom);
                    viewList.add(view);
                    // 下一个 view 的 top 是上一个 View 的 bottom
                    top = bottom;//循环摆放
                }
....
 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;
    }

五、如何处理滑动事件

需要监听手指按下的事件和移动的事件,当移动的距离大于滑动最小距离时认为是一次滑动事件

1.通过ViewConfiguration获取系统设定的最小滑动距离
2.onIntercept用来判断是否拦截事件,处理滑动事件是在onTouchEvent中。

   ViewConfiguration configuration = ViewConfiguration.get(context);
   this.touchSlop = configuration.getScaledTouchSlop();
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercept = false;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                currentY = (int) event.getRawY();
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                // 当手指按下的位值 比在 Y 方向移动的距离大于最小滑动的距离,我们拦截这个事件
                int y2 = Math.abs(currentY - (int) event.getRawY());
                if (y2 > touchSlop) {
                    intercept = true;
                }
            }
        }
        return intercept;
    }

onTouchEvent 中去计算滑动的距离,并执行滑动

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE: {
                //                移动的距离   y方向
                int y2 = (int) event.getRawY();
                //         //            上滑正  下滑负
                int diffY = currentY - y2;
                //                画布移动  并不影响子控件的位置
                scrollBy(0, diffY);
            }
        }
        return super.onTouchEvent(event);
    }

六、从换从中获取View并且刷新布局

Item可以直接创建,也可以从缓存中获取,直接创建的话调用onCreateViewHolder创建View并加入到缓存中,从缓存中获取到的View通过onBinderViewHolder刷新数据,给View设置一个Tag可以通过这个Tag来区分Item的布局类型

   private View obtainView(int row, int width, int height) {
        //    获取到这一行 View 的类型
        int itemType = adapter.getItemViewType(row);
        //    根据类型去 缓存池中获取
        View reclyView = recycler.get(itemType);
        View view = null;
        // 如果回收池里没有 View 使用 onCreateViewHolder 创建一个
        if (reclyView == null) {
            view = adapter.onCreateViewHodler(row, reclyView, this);
            if (view == null) {
                throw new RuntimeException("onCreateViewHodler  必须填充布局");
            }
        } else {
            // 否则使用onBinderView
            view = adapter.onBinderViewHodler(row, reclyView, this);
        }
        // 给View 一个 Tag
        view.setTag(R.id.tag_type_view, itemType);
        view.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
                , MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        addView(view, 0);
        return view;
    }

七、处理滑动事件

1.首先要判断是否超出了滑动的边界 即滑动到最后一个View仍然上滑或者第一个VIew仍然下滑

2.如果上滑的高度已经大于当前可见的第一个Item的距离,就移除这个Item,使用while循环是因为防止用户一次性滑出多个View 所以使用
3.加入一个Item的条件是新加入Item后所有显示出的Item是否会超出高度

 @Override
    public void scrollBy(int x, int y) {
        //        scrollY表示 第一个可见Item的左上顶点 距离屏幕的左上顶点的距离
        scrollY += y;
        // 判断是否达到了极限条件 数据的最顶端和数据的最低端
        scrollY = scrollBounds(scrollY);
        //        scrolly
        if (scrollY > 0) {
            /**
             * 当用户滑动特别快的时候 可能一下子滑出去3,4个 View 所以要不断去判断 scrollY 是否比当前
             * 第一个 View 的 heights 只内,如果不在继续移除,知道 scrollY 在 当前 第一个 item的高度范围内
             *
             */
            //              上滑正  下滑负  边界值
            while (scrollY > heights[firstRow]) {
                //      1 上滑移除  2 上划加载  3下滑移除  4 下滑加载
                removeView(viewList.remove(0));
                // 因为用户可能滑动的很快,可能一次性滑出了好几个View,所以用这个方式来
                // 计算一次性滑出了几个 View
                scrollY -= heights[firstRow];
                firstRow++;
            }
            // 是否添加一个 View: 数据高度减去-scrollY的值
            while (getFillHeight() < height) {
                int addLast = firstRow + viewList.size();
                View view = obtainView(addLast, width, heights[addLast]);
                viewList.add(viewList.size(), view);
            }
            // 下滑添加
        } else if (scrollY < 0) {
            //            4 下滑加载
            while (scrollY < 0) {
                int firstAddRow = firstRow - 1;
                View view = obtainView(firstAddRow, width, heights[firstAddRow]);
                // 因为是下滑加载,缓存永远在第一个位置
                viewList.add(0, view);
                firstRow--;
                scrollY += heights[firstRow + 1];
            }
            //   总和高度 - 滑出屏幕的高度 scrollY - 最后一个 item 的高度 就等于 View 的高度
            while (sumArray(heights, firstRow, viewList.size()) - scrollY - heights[firstRow + viewList.size() - 1]
                    >= height) {
                removeView(viewList.remove(viewList.size() - 1));
            }

        } else {
        }
        // 重新摆放子View的位置
        repositionViews();
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容