蓄谋已久的列表控件

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

GIF.gif

首先我们需要实现一个静态的页面效果。
蓄谋已久的列表控件_第1张图片
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 rowViewList;//保存一行数据 因为在滑动是可能一行数据直接就滑上去了
    private List columnViewList;
    private List> 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 list = new ArrayList();
                    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亿跳数据,我们看一下内存状况。


蓄谋已久的列表控件_第2张图片
memory1.gif

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

//画Body部分(蓝色部分)
for (int i = firstRow; i < rowCount && top < height ; i++) {
...
    for (int j = firstColumn; j < columnCount  && left < width ; j++) {
      ...
    }
...
} 
蓄谋已久的列表控件_第3张图片
memory2.gif

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

public class Recycler {
    private Stack[] views;
    public Recycler(int type) {
        views=new Stack[type];
        for (int i = 0; i < type; i++) {
            views[i]=new Stack();
        }
    }
    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的一个集合。
最后我感觉可以用 源码 来理解会更方便一些。

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

你可能感兴趣的:(蓄谋已久的列表控件)