蓄谋已久的列表控件

之前分析了 事件分布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的一个集合。
最后我感觉可以用 源码 来理解会更方便一些。

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

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,856评论 25 707
  • 接上一篇:Android艺术开发探索第三章————View的事件体系(上) 3.4 View 的事件分发机制 本节...
    kongjn阅读 1,106评论 1 0
  • 第3章 View的事件体系 [TOC] 3.1 View基础知识 1. View的位置参数 首先来认识一下View...
    反复横跳的龙套阅读 909评论 0 5
  • 一天,喂儿子吃饭(孩子自立能力让我培养的超差)。“妈妈,你炒菜花第一名,太美味了。”儿子认真的夸奖我。 我大笑他的...
    痴行人阅读 231评论 2 5
  • 8🈷️20日,也不觉得今天🈶多特别。早上路过花店,突然被路旁五颜六色的玫瑰🌹给吸引住,身子忍不住的就随着脚步...
    随心语录阅读 1,546评论 3 3