当年刚到公司的时候,就遇上了这个项目启动,从零开始做的项目。
�第一个任务就是做一个展示房间列表的页面。具体的样子就是下图所长的样子
要求能够�展示所有房间14天内的预定信息,并且能对选中�部分日期的房间做预定,入住等操作。当时做的还是比较慢的,基本的想法就是用横向滑动的
ScrollView
里面嵌套一个GridView
这些现有的控件�来做,可想而知,效果并不好。首先AbsListView
嵌套在ScrollView
里面的话,�就完全没法复用其中的contentView
和viewHolder
,有多少就一次性的生成多少对象,当数据量一大的时候造成性能上的低下,页面渲染的卡顿,并且当要更新选中的�格子的状态和UI,调用notifyDataSetChange()
的时候,会对整个GridView
进行了刷新,造成很大的系统开销,反应迟缓,因为GridView
并没有像ReccyclerView
一样,有只对某一个对象的更新而更新UI的功能。其次,ScrollView
和GridVIew
都只能进行一个方向的滑动,比如一旦竖直方向的滑动开始了之后,再进行水平方向的滑动是不会有响应的,用户的体验比较差。
�第一版差不多就是这样做的,数据小的时候还勉强能用,�当有100个以上的房间的时候,加载的时候就特别慢,�网络请求拿到数据之后,还要过差不多3-4秒的时间,整个GridView
才会显示出来,体验很差。由于是项目刚开始,对产品其他功能的需求实现比较迫切,所以对这个房态页面的功能仅仅是要求达到暂时能用,并没有立即�着手进行优化。后来,我记得�差不多半年之后,对页面做过一次优化,基本的结构并没有怎么改变,�UI上做了一些改善,还有�就是�自定了外面的ScrollView
,能够进行同时两个方向的滑动了,这对体验上来说是一个提升,但是性能上并没有什么改善,数据一大,还是那么的卡,老样子。
项目迭代了两年,�好几次都想要优化这个页面,但是由于功能快速迭代,一直没有时间和精力去好好的想方案。
今年感觉积累的差不多了,稍微有点空的时候,就琢磨着捣鼓捣鼓这个页面,给它来一次全面的升级,抛弃掉以前的包袱,重新来写一遍。
分析一下页面卡,主要是要渲染的东西太多了,或者一次生成的对象太多了,像GridView
这种控件,每一个item
都是一个对象,还有一个viewHolder
,一旦出现没法复用contentView
和viewHolder
的话,数据量一多,一下子生成的对象多的会造成虚拟机内存抖动,在短时间内产生大量的对象,严重占用Young Generation(分代垃圾回收的年轻代)的内存区域,当剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。调用控件的notifyDataSetChange()
方法的时候,就会引起不断的GC,从而导致UI线程被频繁的阻塞,导致UI卡顿。初步分析可能是这个原因。
当我�进入页面的时候内存使用率直接从20多mb跃到了40多mb,可怕的翻了一番,然后看一下memory usage的报表
�objects中的view从105个增长到了14363�个�(当前页面有180个房间14天的情况�),每个item的视图都会有几个控件对象的生成,结果就是造成了现在一下子多了这么多的对象在内存中。打开模拟器的Gpu呈现模式分析,看到进去页面的时候柱状图反应的当前界面�渲染时间非常的高。
蓝色代表了绘制的时间,橙色代表处理的时间,红色代表执行的时间�几条最高的柱子中,�蓝色占了这么多的部分,说明很多时间都花在了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.
基于一个手势来进行滑动,滑动的距离将取决于开始时候的速度。然后看下这个方法的参数,startX
和startY
比较简单,就是开始滑动的位置。第三个和第四个参数需要了解一下,是两个根据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
来做根据惯性滑动了。如下图所示。 模拟器使用起来还是没有真机流畅。
格子和滑动都做好了,接下来就要将数据填充进去了。像使用RecyclerView
一样,也是要有一个对象数组的,数据的改变映射到视图上面。从服务器获取的数据是一个房间的订单列表,里面包括了订单ID,订单使用的�房间的ID,开始日期和结束日期,根据这些字段,要把订单的信息填充到房间日历里面去。
首先是确定哪些格子要被填充,被填充格子的横坐标用开始日期和结束日期来确定,因为横坐标就是14天的时间段,Y轴显示的是具体的�房间,可以通过订单的房间号去确定纵坐标。确定了横坐标和纵坐标之后,就能在那个位置画出订单的信息来了,canvas.drawRect()
画一个矩形,来表示某一个房间的某一段时间被这个订单占用了,这样订单的展示就做完了。接下来是能够响应点击事件,触发空闲的房间被选中,从而能够预定某个房间的某个时间段。
当手指点下去的时候就会触发MotionEvent.ACTION_DOWN
事件,这个时候通过event.getX()
和event.getY()
就能获取相对于当前View
的坐标,�然后除以格子的宽高得到的整数,就能获得当前点击是那个位置的格子,�调用invalidate()
来刷新整个View
,我在这里是用一个二维数组来记录格子的点击状态的,�刷新View
的时候,遍历这个二维数组,把选中状态的格子画上一个被选中的标记,如果之前已经被选中了,那就把选中取消掉,如果那个格子有订单,也是记录在二维数组中,就不响应这个点击事件。响应的是点击之后跳转到相应的订单详情页面。点击事件也做好了,接下来是横坐标的�日期列表,和纵坐标的房间列表的展示,和响应格子图的滑动,它们俩也跟着滑动。
响应滑动其实很简单,复写当前View
的onScrollChanged()
方法:
@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);
}
}
这样,纵坐标和横坐标都能跟着中间的格子来滑动了。具体代码在这里 代码还在不断修改中。