源码已放入我的 github,
地址:Unity-ListView
实现一个列表组件,表现方面最核心的部分就是重写布局(Layout)。
对于简单的列表,尤其是“Cell数量固定且较少、没有超页滚动展示”一类的需求,使用UGUI自带的布局组件进行布局即可。分别为:水平布局组件(Horizontal Layout Group)、竖直布局组件(Vertical Layout Group)、格子布局组件(Grid Layout Group)。
而当 “Cell的数量多而不定,需要超页滚动展示” 时,如果使用UGUI自带的布局组件,必须对所有物体全部实例化。从性能上考虑,这不是一个好的选择。所以,我们希望对Cell进行复用以优化性能。即Cell元素在滑入视口时出现,滑出视口时消失。
其实,我也设想过,可以在创建列表时,按照Cell的大小创建Cell总数个空的物体进行占位,滑动时,把需要显示的Cell挂到这些空物体上去,不需要显示的物体摘下来等待复用。这样既可以用UGUI自带的Layout组件进行布局,也可以让Cell进行复用。
但这种做法的性能实在不好评价(还是看具体使用),这里暂时也不详细讨论。
-------------------------------------------------NRatel割-------------------------------------------------
自己重写布局,虽然复杂,但不算难。主要是要把各种细节考虑清楚。
对于复杂的特殊需求(如不规则Cell元素、多种Cell元素、多行多列、滑动结束停靠在中心点、前后循环列表、翻页容器等),进行取舍和整合。
这里我考虑一步一步来,计划:
第一版:只做表现(不考虑数据)、不做Cell的复用,只做一个排布方式 “水平方向、从左往右”。
第二版:在第一版基础上,实现Cell复用。
第三版:在第二版基础上,处理其他排布方向。
第四版(标准成品):在第三版基础上,由数据驱动列表显示。
第五版(多种特殊分支):在某一版本基础上,整合各种特殊需求进去。
-------------------------------------------------NRatel割-------------------------------------------------
只做表现(不考虑数据)、不做Cell的复用,只做一个排布方式 “水平方向、从左往右”。
思路:直接创建出所有Cell,然后根据其索引进行排列。
源码地址:Unity-ListView
效果预览:
需要强调几个点:
ListView建立在 “滑动” 的基础上,需要依赖ScrollRect。
在ListView类上添加 [RequireComponent(typeof(ScrollRect))] 。
大小的计算有两种方式供选择:
①,RectTransform.rect :Cell本身Rect的大小
②,RectTransformUtility.CalculateRelativeRectTransformBounds() :Bounds,Cell及其所有子物体的总的包围Rect的大小。
一般情况下,应该选用第一种方式。
因为大多数情况下我们的需求是 “两个Cell本身保持其间距”。而如果使用Bounds, 当 “Cell的子物体超出Cell本身的Rect”时,想要继续保持两个Cell本身的间距,就要反过来计算调整spacing。这在实际使用中实在很难受。
----------------------------------------
位置的计算有两种方式供选择:
①,使用 localPosition(Cell的pivot 相对于 Content的pivot):Cell的pivot 和 Content的pivot 将影响Cell的位置计算。
②,使用 anchoredPosition(Cell的pivot 相对于 Cell的anchor):Cell的pivot 和 Cell的anchor 将影响Cell的位置计算。
这里我选择第二种方式。
选择的主要依据是,在UGUI中,应该优先使用RectTransform中的而不是Transform中的属性和方法,使用 anchoredPosition 比 localPosition 更高级。(UGUI自带的Layout组件,也强制改变了Cell的anchor,由此推算,它也是使用 anchoredPosition 进行处理的);另外,Content的pivot 和 Cell的anchor 都没有自主设置需求,(由排布方向可以确定,设成其他无实用意义)使用anchoredPosition不会有什么问题。
所以最终要做的是,根据方向强制设置 Cell的 anchor(注意只改对应方向)。
例如,在水平方向从左往右的情况下,把Cell的 anchorMin.x 和 anchorMax.x 强制设置为0(只改X,不改Y)。
由于是水平方向,从左往右排列,这里的大小只说宽度。
注意考虑极限情况。如果Cell的数量为0,那就没有边距和间距什么事了,Content的宽度直接就是0。
这样可以避免当视口较小时,“没有Cell但Content不为0,竟然可以滑动” 的问题。
//计算和设置Content总宽度
//当cellCount小于等于0时,Content总宽度 = 0
//当cellCount大于0时,Content总宽度 = 左边界间隙 + 所有Cell的宽度总和 + 相邻间距总和 + 右边界间隙
contentWidth = cellCount <= 0 ? 0 : paddingLeft + cellPrefabRT.rect.width * cellCount + spacingX * (cellCount - 1) + paddingRight;
contentRT.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, contentWidth);
在上面2中,我们已经发现:无论采用哪种计算位置的方式,Cell的轴心点(pivot)都会影响Cell 的位置。
简单来看,在从左往右排布的情况下,
如果Cell的 pivot.x 是0(左边界),则第一个Cell的坐标需要向右偏移的距离为:0。
如果Cell的 pivot.x 是0.5(中心),则第一个Cell的坐标需要向右偏移的距离为:Cell宽度的1/2 。
如果Cell的 pivot.x 是1(有边界),则第一个Cell的坐标需要向右偏移的距离为:Cell宽度。
实际情况是,轴心点的作用不仅代表物体的位置中心,也作为物体的旋转中心,还作为物体大小变化时进行对齐的参考点。
Cell 有其自主设置轴心点的需求。所以,不能在实例化Cell时强制修改或设置其轴心点。
那么,这个起始偏移值,就要根据物体的轴心点来计算。
好在规律很简单:在从左往右排布的情况下,这个起始偏移值,就是物体轴心点距离其左界的距离。
//计算由Cell的pivot决定的起始偏移值
pivotOffsetX = cellPrefabRT.pivot.x * cellPrefabRT.rect.width;
另外,值得一提的题外话:在改变物体宽高时,其总会保持轴心点位置不变,然后向四周等比例扩展或缩小,其变化前后,“左:右” 和“上:下” 的比例总是保持不变。如图,小矩形在改变大小变成大矩形时,其pivot左右两侧的宽度比例始终为4:6。
每个cell的位置,受其自身索引影响。
//计算和设置Cell的位置
//X = 左边界间隙 + 由Cell的pivot决定的起始偏移值 + 前面已有Cell的宽度总和 + 前面已有的间距总和
float x = paddingLeft + pivotOffsetX + cellPrefabRT.rect.width * index + spacingX * index;
RectTransform cellRT = cell.GetComponent();
cellRT.anchoredPosition = new Vector2(x, cellRT.anchoredPosition.y);
-------------------------------------------------NRatel割-------------------------------------------------
在第一版基础上,实现Cell复用。
思路:不再在开始时就创建出所有Cell,而是 先计算需要显示的索引集合,然后根据索引数据创建/复用、显示Cell列表。
源码地址:Unity-ListView
效果预览:
需要强调的有:
先上一张未处理前的图来说明问题,其中黑色区域是viewport。
我能想到两种方式来找出 “应显示的Cell的索引集合”:
①,遍历所有Cell的索引,找出 “Cell的右边界在viewport的左边界之右” 且 “Cell的左边界在viewport的右边界之左” 的所有Cell的Index,即为应显示的索引集合。如上图,索引 3、4、5、6、7、8 满足此条件,需要显示。
②,根据 content 相对于 viewport 的位移 来计算。
注意!这里必须选择第二种方式。
原因很明显,第一种方式虽然简单,但是非常粗暴。它需要一次遍历,如果Cell总数极大,将发生灾难。
在具体计算之前,需要强制设置 Content的 anchor 和 pivot(注意只改对应方向)。
例如,在水平方向从左往右的情况下,把Content的 anchorMin.x 和 anchorMax.x 强制设置为0;把Content的 pivot.x 强制设置为0。
原因:计算content 相对于 viewport 的位移。同样使用 anchoredPosition (依赖于自身的 anchor 和 pivot)。
1)、根据 “content左边界相对于viewport左边界的位移” 和 “content右边界相对于viewport右边界的位移” 分别计算出 “完全滑出viewport左边界和右边界的Cell的数量”。
2)、根据 “完全滑出viewport左边界和右边界的Cell的数量” 计算出 “需要显示的Cell的索引集合”。
3)、开始和滑动时,根据 “相邻两帧的需要显示的Cell的索引集合” 对比出 “将要出现的的Cell的索引集合” 和 “将要消失的Cell的索引集合”。
注意,新、旧、出现、消失 的索引集合,定义在全局,避免每帧创建,消耗堆内存。
注意,每次保存前一帧的 “需要显示的Cell的索引集合”时。可交换新旧集合的引用指向,反复使用。
Cell出现时,从池中取或创建(池中无可用时则创建),并使其显示出来(SetActive(true))。
Cell消失时,隐藏(SetActive(false))并放如池中。(注意,出入池都不更改其父物体)
Cell总数为0 时,content直接为0。不适用,直接return处理。
-------------------------------------------------NRatel割-------------------------------------------------
在第二版基础上,处理其他排布方向。逻辑完全一样,略。
考虑水平和竖直分为两个组件,方便某一方向上特殊需求的扩展(只处理需求所在方向)。
在第三版基础上,由数据驱动列表显示。
在某一版本基础上,整合各种特殊需求进去。