2年之后重新写的页面

当年刚到公司的时候,就遇上了这个项目启动,从零开始做的项目。
�第一个任务就是做一个展示房间列表的页面。具体的样子就是下图所长的样子

ui图

要求能够�展示所有房间14天内的预定信息,并且能对选中�部分日期的房间做预定,入住等操作。当时做的还是比较慢的,基本的想法就是用横向滑动的ScrollView里面嵌套一个GridView这些现有的控件�来做,可想而知,效果并不好。首先AbsListView嵌套在ScrollView里面的话,�就完全没法复用其中的contentViewviewHolder,有多少就一次性的生成多少对象,当数据量一大的时候造成性能上的低下,页面渲染的卡顿,并且当要更新选中的�格子的状态和UI,调用notifyDataSetChange()的时候,会对整个GridView进行了刷新,造成很大的系统开销,反应迟缓,因为GridView并没有像ReccyclerView一样,有只对某一个对象的更新而更新UI的功能。其次,ScrollViewGridVIew都只能进行一个方向的滑动,比如一旦竖直方向的滑动开始了之后,再进行水平方向的滑动是不会有响应的,用户的体验比较差。

�第一版差不多就是这样做的,数据小的时候还勉强能用,�当有100个以上的房间的时候,加载的时候就特别慢,�网络请求拿到数据之后,还要过差不多3-4秒的时间,整个GridView才会显示出来,体验很差。由于是项目刚开始,对产品其他功能的需求实现比较迫切,所以对这个房态页面的功能仅仅是要求达到暂时能用,并没有立即�着手进行优化。后来,我记得�差不多半年之后,对页面做过一次优化,基本的结构并没有怎么改变,�UI上做了一些改善,还有�就是�自定了外面的ScrollView,能够进行同时两个方向的滑动了,这对体验上来说是一个提升,但是性能上并没有什么改善,数据一大,还是那么的卡,老样子。

项目迭代了两年,�好几次都想要优化这个页面,但是由于功能快速迭代,一直没有时间和精力去好好的想方案。

今年感觉积累的差不多了,稍微有点空的时候,就琢磨着捣鼓捣鼓这个页面,给它来一次全面的升级,抛弃掉以前的包袱,重新来写一遍。

分析一下页面卡,主要是要渲染的东西太多了,或者一次生成的对象太多了,像GridView这种控件,每一个item都是一个对象,还有一个viewHolder,一旦出现没法复用contentViewviewHolder的话,数据量一多,一下子生成的对象多的会造成虚拟机内存抖动,在短时间内产生大量的对象,严重占用Young Generation(分代垃圾回收的年轻代)的内存区域,当剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。调用控件的notifyDataSetChange()方法的时候,就会引起不断的GC,从而导致UI线程被频繁的阻塞,导致UI卡顿。初步分析可能是这个原因。

memory.jpg

当我�进入页面的时候内存使用率直接从20多mb跃到了40多mb,可怕的翻了一番,然后看一下memory usage的报表

memoryusage1.jpg
memoryusage2.jpg

�objects中的view从105个增长到了14363�个�(当前页面有180个房间14天的情况�),每个item的视图都会有几个控件对象的生成,结果就是造成了现在一下子多了这么多的对象在内存中。打开模拟器的Gpu呈现模式分析,看到进去页面的时候柱状图反应的当前界面�渲染时间非常的高。

gpu.jpg

蓝色代表了绘制的时间,橙色代表处理的时间,红色代表执行的时间�几条最高的柱子中,�蓝色占了这么多的部分,说明很多时间都花在了gpu的绘制上。 说了这么多,�我觉得首先要控制对象的生成,一下子那么多对象,怎么都会卡,解决了对象的数量问题,什么过度绘制,cpu和gpu消耗大量资源导致系统卡顿的问题自然而然能够解决了。

第一步确定方案,这次我打算不适用原生的那些类似ListView的组件了,直接自己在View上面绘制,我需要的内容,这样只有一个View对象,不会出现之前那种大量对象占用内存的情况了。

对象直接�继承于View,首先需要确定这个�View的大小,确定View的大小�得在onMeasure()方法里面调用setMeasuredDimension()方法,参数传入�你需要的长度和宽度,宽我们直接可以确定,因为一行需要十四个格子,高度的话,需要根据数据的大小来设置,设置完了长宽,�就要在上面画格子,格子是一行14个,每列的数量也是根据数据来设置的,�所以我们可以在这个自定义View的构造函数,或者设置数据的时候来设置这个值,然后在onDraw()方法中,用canvas.drawLine()方法来画线,只要确定,线的起点,终点就可以。

 protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mData != null) {
            for (int i = 0; i < mData.length; i++) {
                canvas.drawLine(0, unitHeight * i, unitWidth * 14, unitHeight * i, painttext);
            }
            for (int i = 0; i < mData[0].length; i++) {
                canvas.drawLine(unitWidth * i, 0, unitWidth * i, unitHeight * mData.length, painttext);
            }
        }
    }

画完线之后,要让View能够滑动起来,查看没有在当前页面显示的数据。滑动就交给onTouchEvent()方法,来追踪手势。

  switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                int index = event.getActionIndex();
                downX = (int) event.getX();
                downY = (int) event.getY();
                event.getX(index);
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int y = (int) event.getY();
                int moveX = downX - x;
                downX = x;
                int moveY = downY - y;
                downY = y;

                if (moveX < 0) {
                    if (getScrollX() + moveX < 0) {
                        moveX = 0;
                    }
                } else {
                    if (getScrollX() + moveX > getMeasuredWidth() - mDm.widthPixels) {
                        moveX = 0;
                    }
                }
                if (moveY < 0) {
                    if (getScrollY() + moveY < 0) {
                        moveY = 0;
                    }
                } else {
                    if (getScrollY() + moveY > getMeasuredHeight() - mDm.heightPixels) {
                        moveY = 0;
                    }
                }
                scrollBy(moveX, moveY);
                break;

ACTION_DOWN中确定手指触摸的位置,在ACTION_MOVE中来指定View滑动的距离。这样View虽然可以滑动了,但是�滑动是很生硬的,�只能用手指拖着View走,所以要加入fling这个操作,在手指滑动然后松开的时候View能够继续按照之前滑动的方向滑翔一段距离。Scroller的概念就不在这里述说了,Scroller有一个fling()的方法,根据这个方法的描述

Start scrolling based on a fling gesture. The distance travelled will depend on the initial velocity of the fling.

基于一个手势来进行滑动,滑动的距离将取决于开始时候的速度。然后看下这个方法的参数,startXstartY比较简单,就是开始滑动的位置。第三个和第四个参数需要了解一下,是两个根据x轴方向和y轴方向以像素/秒为单位测量的抛射速度,简单点说就是手指在屏幕上滑动的速度,我们要获取的是手指离开屏幕的那一瞬间的速度,用这个速度来控制View的惯性滑动距离。�在这里我们要用到android的VelocityTracker速度追踪器这个类,该类是用来计算在滑动控件时,手指在水平方向和竖直方向的速度。基本用法如下:

VelocityTracker mVelocityTracker = VelocityTracker.obtain();//初始化  
mVelocityTracker.addMovement(event);//将事件MotionEvent交给它处理  
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//获取当前的速度

int xVelocity = (int) mVelocityTracker.getXVelocity ();
        int yVelocity = (int) mVelocityTracker.getYVelocity ();//获取X轴方向和Y轴方向的速度

computeCurrentVelocity()方法第一个参数的意思是指定单位时间内移动的像素,如果设为1就是1毫秒秒内移动了多少个像素,1000的话就是一秒内。maxVelocity是指最大的移动速度,移动速度的取值在0到设置的范围之内。使用完该对象后,需要对该对象释放,以节约内存,mVelocityTracker.recycle()。获取到了速度之后就可以用Scroller来做根据惯性滑动了。如下图所示。 模拟器使用起来还是没有真机流畅。

1.gif

格子和滑动都做好了,接下来就要将数据填充进去了。像使用RecyclerView一样,也是要有一个对象数组的,数据的改变映射到视图上面。从服务器获取的数据是一个房间的订单列表,里面包括了订单ID,订单使用的�房间的ID,开始日期和结束日期,根据这些字段,要把订单的信息填充到房间日历里面去。

首先是确定哪些格子要被填充,被填充格子的横坐标用开始日期和结束日期来确定,因为横坐标就是14天的时间段,Y轴显示的是具体的�房间,可以通过订单的房间号去确定纵坐标。确定了横坐标和纵坐标之后,就能在那个位置画出订单的信息来了,canvas.drawRect()画一个矩形,来表示某一个房间的某一段时间被这个订单占用了,这样订单的展示就做完了。接下来是能够响应点击事件,触发空闲的房间被选中,从而能够预定某个房间的某个时间段。

当手指点下去的时候就会触发MotionEvent.ACTION_DOWN事件,这个时候通过event.getX()event.getY()就能获取相对于当前View的坐标,�然后除以格子的宽高得到的整数,就能获得当前点击是那个位置的格子,�调用invalidate()来刷新整个View,我在这里是用一个二维数组来记录格子的点击状态的,�刷新View的时候,遍历这个二维数组,把选中状态的格子画上一个被选中的标记,如果之前已经被选中了,那就把选中取消掉,如果那个格子有订单,也是记录在二维数组中,就不响应这个点击事件。响应的是点击之后跳转到相应的订单详情页面。点击事件也做好了,接下来是横坐标的�日期列表,和纵坐标的房间列表的展示,和响应格子图的滑动,它们俩也跟着滑动。

响应滑动其实很简单,复写当前ViewonScrollChanged()方法:

@Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (viewOne!= null) {
            viewOne.scrollTo(l, t);
        }
        if (viewTwo != null) {
            viewTwo.scrollTo(l, t);
        }
    }

这样,纵坐标和横坐标都能跟着中间的格子来滑动了。具体代码在这里 代码还在不断修改中。

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

推荐阅读更多精彩内容

  • 本笔记整理自: https://www.gitbook.com/book/tom510230/android_...
    01_小小鱼_01阅读 1,001评论 0 4
  •   JavaScript 与 HTML 之间的交互是通过事件实现的。   事件,就是文档或浏览器窗口中发生的一些特...
    霜天晓阅读 3,490评论 1 11
  • 现在的视频太多了,层出不穷,同时劣质的出现了,优质的 人很少看。当一些人看视频的同时,另一些人已经选择用视频赚钱了...
    苏如实阅读 306评论 1 0
  • 最近发生在自己身上的几件小事,让我对育儿经验有了一些新的认识: 01. 和朋友家庭聚会时,朋友的表姐对我不给一岁半...
    NH一起成长阅读 163评论 2 1
  • 今天因同事出差,我是他的代理人,受托处理一项急事。他告诉了我处理方法,让我去他的电脑找寻相关文件来操作。可是就这一...
    行笺阅读 632评论 0 4