美文网首页
蓄谋已久的列表控件

蓄谋已久的列表控件

作者: CrazyCarrot | 来源:发表于2017-06-10 21:07 被阅读0次

    之前分析了 事件分布ListView复用机制 ,不能只分析不使用把,这次就用之前分析的知识来完成一个自定义列表控件
    首先看一下效果,结合了ListView的复用机制以及触摸事件的使用。

    GIF.gif
    首先我们需要实现一个静态的页面效果。
    base.jpg
    他分为四部分,左上角是怎么滑动都不会动的,上和左各有一个首行只可以单向滑动,而蓝色部分是可以上下左右,甚至斜着都可以,而且在实现静态页面的同是我们利用学过的ListView源码里的逻辑可以实现只加载屏幕内显示的View,所以不论有多少数据,我们都不用担心内存问题。
    首先我们看一下需要用到的变量都是干什么的
        private BaseTableAdapter adapter;
    
        private int downX;//滑动时手指落下的X Y
        private int downY;
        private int scrollX;//滑动的距离
        private int scrollY;
        private int firstRow;//当前第一行postiton
        private int firstColumn; //当前第一列position
        private int[] widths;//存放每个View的宽高
        private int[] heights;
    
        @SuppressWarnings("unused")
        private View headView;//头View 为使用
        private List<View> rowViewList;//保存一行数据 因为在滑动是可能一行数据直接就滑上去了
        private List<View> columnViewList;
        private List<List<View>> bodyViewTable;//表格数据
        private int rowCount;//行数
        private int columnCount;//列数
        private int width;//控件宽高
        private int height;
        private final ImageView[] shadows;//分割的黑线
        private final int shadowSize;//黑线宽度
    
        private int minimumVelocity;//惯性滑动时最小和最大速率
        private int maximumVelocity;
        private final Flinger flinger;//惯性滑动
        private VelocityTracker velocityTracker;//惯性滑动
    
        private boolean needRelayout;    //需要重绘标志位
        private int touchSlop;    //滑动最小距离
        private Recycler recycler;//复用相关类
    

    接下来按一下onMeasure方法。

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            final int w;
            final int h;
    
            if (adapter != null) {
                this.rowCount = adapter.getRowCount();//获取数据个数
                this.columnCount = adapter.getColumnCount();
                //
                widths = new int[columnCount + 1];//初始化保存的数组 这里+1 是包括了有一个单向滑动的头部。
                for (int i = -1; i < columnCount; i++) {//这里从-1开始是为了可以添加columnCount + 1条数据
                    widths[i + 1] += adapter.getWidth(i);
                }
                heights = new int[rowCount + 1];
                for (int i = -1; i < rowCount; i++) {
                    heights[i + 1] += adapter.getHeight(i);
                }
    
                if (widthMode == MeasureSpec.AT_MOST) {//AT_MOST wrap_content
                    //sumArray方法是计算出数组的总和
                    w = Math.min(widthSize, sumArray(widths));//判读屏幕宽度和数据宽度,取最小的
                } else if (widthMode == MeasureSpec.UNSPECIFIED) {
                    w = sumArray(widths);
                } else {//具体指或match_parent
                    w = widthSize;
                    int sumArray = sumArray(widths);
                    if (sumArray < widthSize) {//如果 现有view的宽度小于 屏幕宽度 将会把屏幕宽度平分
                        final float factor = widthSize / (float) sumArray;
                        for (int i = 1; i < widths.length; i++) {
                            widths[i] = Math.round(widths[i] * factor);
                        }
                        widths[0] = widthSize - sumArray(widths, 1, widths.length - 1);
                    }
                }
    
                if (heightMode == MeasureSpec.AT_MOST) {
                    h = Math.min(heightSize, sumArray(heights));
                } else if (heightMode == MeasureSpec.UNSPECIFIED) {
                    h = sumArray(heights);
                } else {
                    h = heightSize;
                }
            } else {
                if (heightMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
                    w = 0;
                    h = 0;
                } else {
                    w = widthSize;
                    h = heightSize;
                }
            }
            //必须调用
            setMeasuredDimension(w, h);
        }
    

    通过onMeasure我们不仅适配了屏幕,而且还获取了每个View的宽高,这样任由我们摆放了,所以接下来看一下onLayout方法。

        @SuppressLint("DrawAllocation")
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            if (needRelayout || changed) {
                needRelayout = false;
                resetTable();//情况所有的集合和View
    
                if (adapter != null) {
                    width = r - l;//屏幕当前的宽度和高度
                    height = b - t;
                    //画那四条黑线,在实现静态页面是还没什么用
                    int left, top, right, bottom;
    
                    right = Math.min(width, sumArray(widths));
                    bottom = Math.min(height, sumArray(heights));
                    addShadow(shadows[0], widths[0], 0, widths[0] + shadowSize, bottom);
                    addShadow(shadows[1], 0, heights[0], right, heights[0] + shadowSize);
                    addShadow(shadows[2], right - shadowSize, 0, right, bottom);
                    addShadow(shadows[3], 0, bottom - shadowSize, right, bottom);
                    //画左上角那个固定的ViewItem(红色部分)
                    headView = makeAndSetup(-1, -1, 0, 0, widths[0], heights[0]);
                    //画除左上角以外的第一行数据(橘黄色部分)
                    left = widths[0] ;
                    //这里用到了源码里的机制,只加载屏幕以内的View
                    //当left(当前View的左边<屏幕的宽度才去加载)
                    for (int i = firstColumn; i < columnCount && left < width; i++) {
                        //不停的去找下个View的左右边的值
                        right = left + widths[i + 1];
                        final View view = makeAndSetup(-1, i, left, 0, right, heights[0]);
                        rowViewList.add(view);//保存第一行数据
                        left = right;
                    }
                    //画除左上角以外的第一列数据(棕色部分)
                    top = heights[0] ;
                    for (int i = firstRow; i < rowCount && top < height; i++) {
                        bottom = top + heights[i + 1];
                        final View view = makeAndSetup(i, -1, 0, top, widths[0], bottom);
                        columnViewList.add(view);
                        top = bottom;
                    }
                    //画Body部分(蓝色部分)
                    top = heights[0];
                    for (int i = firstRow; i < rowCount && top < height; i++) {
                        bottom = top + heights[i + 1];
                        left = widths[0] - scrollX;
                        List<View> list = new ArrayList<View>();
                        for (int j = firstColumn; j < columnCount && left < width; j++) {
                            right = left + widths[j + 1];
                            final View view = makeAndSetup(i, j, left, top, right, bottom);
                            list.add(view);//当前行 一个一个添加  最后相当于一行数据
                            left = right;
                        }
                        bodyViewTable.add(list);//添加一行数据 最后相当于 表格内所有数据
                        top = bottom;
                    }
    
                    shadowsVisibility();//分割的黑线
                }
            }
        }
    

    这里突出了静态时的一个关键点,就是只加载当前页面内的数据,优化效率非常明显。之后只要设置数据就可以正常显示了,具体看 源码,这里我们看一下优化的效率。首先我们改变一下代码

    //画Body部分(蓝色部分)
    for (int i = firstRow; i < rowCount ; i++) {
    ...
        for (int j = firstColumn; j < columnCount ; j++) {
          ...
        }
    ...
    } 
    

    添加蓝色区域View的时候 我们把屏幕限制条件删除,并且我们隔八秒后添加1亿跳数据,我们看一下内存状况。


    memory1.gif

    宝宝表示震精了~我们在看一下添加上限制条件后是什么情况。

    //画Body部分(蓝色部分)
    for (int i = firstRow; i < rowCount && top < height ; i++) {
    ...
        for (int j = firstColumn; j < columnCount  && left < width ; j++) {
          ...
        }
    ...
    } 
    
    memory2.gif

    效果很明显,在第8秒时内存只是增加了一点,将屏幕填满了,之后就再也没有变化,静态的效果我们已经达到了,之后就是滑动时对View的分离以及复用的操作。首先我们需要了解一下复用类。

    public class Recycler {
        private Stack<View>[] views;
        public Recycler(int type) {
            views=new Stack[type];
            for (int i = 0; i < type; i++) {
                views[i]=new Stack<View>();
            }
        }
        public void addRecycledView(View view,int type){//滑动时就会调用
            views[type].push(view);//根据ItemType添加View
        }
        public View getRecyclerView(int type){//添加View的时候调用(静态页面第一次添加View也会调用)
            try {//一定要try catch 因为type第一次出现时可能还没有添加过
                return views[type].pop();//根据ItemType拿到View
            } catch (Exception e) {
                return null;
            }
        }
    }
    

    这个类其实就是对View的一个Item滑出屏幕时需要添加到这个数组里,item出现是需要判断之前是否有缓存过。在添加View的时候就会起到非常大的优化作用。
    然后就开始实现滑动效果,这里主要会将触摸事件拦截,以及滑动时临界值的计算。首先看一下拦截事件。

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            boolean intercept = false;
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    downX = (int) ev.getRawX();
                    downY = (int) ev.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    //拦截move事件  防止子view中有Button一类的控件
                    int x2 =  Math.abs(downX - (int)ev.getRawX());
                    int y2 =  Math.abs(downY - (int)ev.getRawY());
                    //touchSlop是用来判断是否是一个合理的滑动
                    //因为一般情况只要我们手指按下去,不发生Move事件的情况很少很少,总要动一点点的,可能我们都没察觉自己动了。
                    //这里给一个滑动最小距离,大于这个最小距离才算是滑动。
                    if (x2 > touchSlop || y2 > touchSlop) {
                        intercept = true;
                    }
                    break;
            }
            return intercept;
        }
    

    拦截事件很简单,防止子View中有Button一类的控件,如果没有就可以不用拦截。重点还是在onTouchEvent()的Move事件。

        @Override
        public void scrollBy(int x, int y) {
            scrollX += x;
            scrollY += y;
            if (needRelayout) {
                return;
            }
            scrollBounds();
    
            if (scrollX == 0) {
                // no op
            } else if (scrollX > 0) {//向左滑动
                //当scrollX大于body(蓝色区域)内第一个可见View的宽度的时候
                //这里用while是有可能快速移动,直接处理多个View的情况
                while (widths[firstColumn + 1] < scrollX) {
                    if (!rowViewList.isEmpty()) {
                        removeLeft();
                    }
                    scrollX -= widths[firstColumn + 1];
                    firstColumn++;
                }
                //如果不快速滑动 这里的rowViewList可以理解为body中可见的View
                //这里的getFilledWidth()其实就是计算出第一列(单向滑动的那列)的宽度+body(蓝色区域)内rowViewList的中保存的所有View的宽度(有可能首尾的View超出去一部分或超出多个View,那部分也算)-scrollX
                //所以这里计算的就是body(蓝色区域)左边到rowViewList的中保存的最后一个View的宽度(最后一个View超出屏幕部分也算)。因为scrollX就是向左滑了的部分,也就是左边超出的部分
                //这里用while是有可能快速移动,直接处理多个View的情况
                while (getFilledWidth() < width) {//这里的判断就是当把最后一个View超出屏幕的部分全部移回来了,就是添加下个view的时候
                    addRight();
                }
            } else {//向右滑动第一个View全部出现时调用一次
                //往右滑的时候scrollX是负的。所以getFilledWidth()里的-scrollX 成了 +|scrollX|
                //和上边的一样getFilledWidth()计算的是第一列(单向滑动的那列)的宽度+body(蓝色区域)的第一个view到rowViewList的中保存的最后一个View的宽度(最后一个View超出屏幕部分也算)
                //因为只有在右滑时第一个View全部出现的时候调用一次,所以这里body(蓝色区域)的第一个view就相当于从body的左边开始
                //这里判断就相当于:可见的第一个View到rowViewList的中保存的最后一个
                while (!rowViewList.isEmpty() && getFilledWidth() - widths[firstColumn + rowViewList.size()] >= width) {
                    removeRight();
                }
                //当scrollX小于0的时候证明已经 将之前左滑的部分又向右滑回来了
                while (0 > scrollX) {
                    addLeft();
                    firstColumn--;
                    scrollX += widths[firstColumn + 1];
                }
            }
            ...
            repositionViews();//没有这个体现不出滑动的效果
    
            shadowsVisibility();//分割线
        }
    

    我在这里只分析了左右滑动的临界值的计算,说实话有点烧脑,不过自己多尝试两遍还是可以理解的。注释中基本把所有的理解都写了。提示一点注释用到rowViewList的地方如果不快速滑动,可以理解为当前行可见View的一个集合。
    最后我感觉可以用 源码 来理解会更方便一些。

    这篇文章是在我学习的基础上进行了总结,可想而知我还是个很小的菜鸟,如果其中有错误还请指出,我会尽快修改文章,并改正自己的理解,谢谢。

    相关文章

      网友评论

          本文标题:蓄谋已久的列表控件

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