零、ScrollView的短板
在cocoscreator内,ScrollView控件封装的挺完美的了,不过对于一些对性能要求比较高的场景,会存在问题,以top100排行榜排行榜举例子
1、应用卡顿甚至崩溃
按照官方用例使用ScrollView,插入100个玩家的item,理所当然的需要new 100次item。根据设备不同,这一过程可能持续数秒甚至导致程序崩溃
2、多余的内存占用
用户永远只看得到其中10个以下的item,其余的90个item将会被mask遮挡在外面,但是每个item依然会产生性能消耗。
一、优化思路分析
假设一个ScrollView内,用户同时最多可以看到8个item,那么理论上,我们只需要建立8个Item,当触发滑动切换Item展示的时候,将移动到界面外item的数据设置成新Item的数据。
如此一来,既不用担心同一时间new 100个对象会导致卡顿和崩溃,因为我们最多只new 8个item,也不用担心这除了这8个item会有额外的性能消耗了。
,
二、代码实现
1、确定scroll内最大item数量
let maskView = this.node.getChildByName("view")
// 这个scorllview的长度 ,除以 单个item的长度,得出可以容纳的item数量
let showMaxCount = maskView[this.__diConfig[2]] / (this.item.data[this.__diConfig[2]] + this.space)
// 向下取证,再+2个item,为真实的再大展示数量
// 因为可能存在情况,content在中部显示,底部、顶部两个item各显示一半
showMaxCount = Math.floor(showMaxCount) + 2
this.__showMaxCount = showMaxCount
2、添加layout,使scrollview自动适配大小
let layout = this.__content.getComponent(cc.Layout)
if (!layout) {
layout = this.__content.addComponent(cc.Layout)
// 根据scroll的滑动方向,判断layout的排列方向
layout.type = this.__scrollView.vertical ? 2 : 1
// 给layout设置spacing
layout["spacing" + this.__diConfig[1]] = this.space
// scrollview根据children自动适配size
layout.resizeMode = 1
} else {
this.space = layout["spacing" + this.__diConfig[1]]
}
3、监听scrollEvents事件
// diffPos:两次之间的坐标差, 由此判断用户是往上滑还是往下滑
let diffPos = pos - this.__lastPosition
if (diffPos < 0) {
// 向下滑动
// 到达顶部,并且保证data前面还有数据
if (realPos < 0 && this.__drawIndex > 0) {
this.showItem(true)
}
} else {
//向上滑动
let __maxDiff = this.__content.height - this.__content.parent.height
let index = this.__drawIndex + this.__showMaxCount
// 触底,并且保证data后面还有数据
if (realPos > __maxDiff && index < this.__data.length) {
this.showItem(false)
}
}
// showItem的实现
// 单个item的长度或高度
let intervalDisTance = this.item.data[this.__diConfig[2]] + this.space
// 如果是上滑触顶,则将底部的item移动到顶部并设置数据
// 如果是下滑触底,则将顶部的item移动到底部并设置数据
this.__drawIndex = this.__drawIndex + (flag ? -1 : 1)
let nextIndex = flag ? (this.__content.children.length - 1) : 0
let nextChild = this.__content.children[nextIndex]
let index = flag ? this.__drawIndex : (this.__drawIndex + this.__showMaxCount - 1)
let itemData = this.__data[index]
// 这里给item设置数据,并且设置zIndex
this.setItemDate(itemData, index, nextChild)
// 当然由于content边缘被塞入了item,会造成坐标跳跃,此时需要回归跳跃坐标
this.__content[this.__diConfig[0]] += (intervalDisTance * (flag ? 1 : -1))
// scrollview会根据zIndex自动更新children绘制顺序
// 并且由于layout组件存在,会对刷新后的item自动刷新坐标
this.__content.sortAllChildren()
至此scrollEvents的处理代码已经说明完毕,
上面的内容比较多,还是针对一些比较重要的点说明下
a、scrollEvent事件内,根据记录前后两次坐标差值,判断正在上滑还是下滑
b、如果下滑,并且content坐标为0,说明已经滑倒了顶部,
如果上滑,并且content坐标 大于最底部坐标,说明已经滑倒了底部,
此时只要inputData还有数据,则取出数据插入item
c、插入item的时候,
如果顶部插入,则拿最底部的item放到最顶部
如果底部插入,则拿最顶部的item放到最底部
根据inputData内数据的index值,按顺序设置item的zIndex值,然后this.__content.sortAllChildren()
那么scrollview将会立马根据index值刷新item的绘制顺序
而layout组件则会根据item的绘制顺序,自动排列item的坐标
d、假如目前顶部item是8,触顶后顶部item 7是突然插入进来的,那么整个content的坐标必然会自动跳跃1个itemDistance
所以content需要去修正1个itemDistance的距离,这样才不会造成视觉上的跳跃,整个滑动呈现是顺畅的
三、你有足够的理由使用这个方案
我推荐你使用我的方案去实现scrollview的性能优化,理由如下
1、核心逻辑,只会在滑动的时候触发,甚至我们可以再加上条件if (this.__scrollView.isScrolling(),让核心逻辑只在用户手动拖动scorllview的时候触发,这样scrollview回弹的时候不会浪费性能去计算核心逻辑
我当初打算做scrollview优化的时候,是打算用cocos论坛的一些高赞方案的。但实际看过源码之后还是决定按自己思路写,因为首先基本所有scrollview的优化组件是通过监听update事件完成的,update事件是每时每刻都执行的,其性能可能比优化前更差。而且组件代码动辄上1、2千行,维护成本实在过于庞大。我希望能够以简单、可读性高的代码,去解决一些技术难点。
2、由于layout的存在,我们减少了一些核心代码的编写,比如item坐标的维护,content大小的维护,对于代码维护是十分有帮助的,并且由于layout是引擎自带组件,我们可以信任它的稳定性。
3、由于在触顶和触底的时候,插入item的同时,会刷新content的坐标,
原本在触顶或触底导致不能继续滑动,但是在刷新坐标后,会多出一个itemDistance的滑动距离,所以不会造成scorllview触发边界回弹的现象,
而scrollEvents因为是毫秒级的,所以整个滑动是顺畅的
4、上述方案设计出的脚本使用及其简单,只需要将脚本放置于scrollview的节点之上,并且绑定item的prefab