手写RecycleView

作者: 石器时代小古董 | 来源:发表于2019-07-04 17:19 被阅读0次

    网易云课堂笔记

    一、手写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();
        }
    

    相关文章

      网友评论

        本文标题:手写RecycleView

        本文链接:https://www.haomeiwen.com/subject/ibnthctx.html