源码已放入我的 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元素、多行多列、滑动结束停靠在中心点、前后循环列表、翻页容器等),进行取舍和整合。

这里我考虑一步一步来。

-------------------------------------------------NRatel割-------------------------------------------------

第一版

    只做表现(不考虑数据)、不做Cell的复用,只做一个排布方式 “水平方向、从左往右”。

    思路:直接创建出所有Cell,然后根据其索引进行排列。

    源码地址:Unity-ListView   

    效果预览:

  

需要强调几个点:

1、依赖ScrollRect

ListView建立在 “滑动” 的基础上,需要依赖ScrollRect。

    在ListView类上添加 [RequireComponent(typeof(ScrollRect))] 。

2、确定“计算Cell 大小和位置”的方式

大小的计算有两种方式供选择:

    ①,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的位置计算。

这里我选择第二种方式。

   选择的主要依据是,anchoredPosition 比 localPosition 更 “高级”(为UI而生)、更具“适配性”。(UGUI自带的Layout组件,也强制改变了Cell的anchor,由此推算,它也是使用 anchoredPosition 进行处理的);
  另外,Content 的 pivot 和 Cell 的 anchor 都没有因业务改变的需求,(由排布方向可以确定,设成其他无实用意义)使用anchoredPosition不会有什么问题。

   所以最终要做的是,根据方向强制设置 Cell 的 anchor(注意只改对应方向即可)。
   例如,在水平方向从左往右的情况下,把 Cell 的 anchorMin.x 和 anchorMax.x 强制设置为0(只改X,不改Y)。

3、计算Content的大小

   由于是水平方向,从左往右排列,这里的大小只说宽度。

   注意考虑极限情况。如果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);

4、计算由Cell的pivot决定的起始偏移值

   在上面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。

5、计算和设置每个Cell的位置

   每个cell的位置,受其自身索引影响。

//计算和设置Cell的位置
//X = 左边界间隙 + 由Cell的pivot决定的起始偏移值 + 前面已有Cell的宽度总和 + 前面已有的间距总和
float x = paddingLeft + pivotOffsetX + cellPrefabRT.rect.width * index + spacingX * index;
RectTransform cellRT = cell.GetComponent<RectTransform>();
cellRT.anchoredPosition = new Vector2(x, cellRT.anchoredPosition.y);

-------------------------------------------------NRatel割-------------------------------------------------

第二版

    在第一版基础上,实现Cell复用。

    思路:不再在开始时就创建出所有Cell,而是 先计算需要显示的索引集合,然后根据索引数据创建/复用、显示Cell列表。

    源码地址:Unity-ListView   

    效果预览:

需要强调的有:

1、先确定求 “应显示的Cell的索引集合” 的方式。

   先上一张未处理前的图来说明问题,其中黑色区域是viewport。

我能想到两种方式来找出 “应显示的Cell的索引集合”:

    ①,遍历所有Cell的索引,找出 “Cell的右边界在viewport的左边界之右” 且 “Cell的左边界在viewport的右边界之左” 的所有Cell的Index,即为应显示的索引集合。如上图,索引 3、4、5、6、7、8 满足此条件,需要显示。

    ②,根据  content 相对于 viewport 的位移 来计算。

注意!这里必须选择第二种方式。

原因很明显,第一种方式虽然简单,但是非常粗暴。它需要一次遍历,如果Cell总数极大,将发生灾难。

2、 强制设置 Content的 anchor 和 pivot

   在具体计算之前,需要强制设置 Content的 anchor 和 pivot(注意只改对应方向)。

   例如,在水平方向从左往右的情况下,把Content的 anchorMin.x 和 anchorMax.x 强制设置为0;把Content的 pivot.x 强制设置为0。

   原因:计算content 相对于 viewport 的位移。同样使用 anchoredPosition (依赖于自身的 anchor 和 pivot)

3、计算索引的具体思路

    1)、根据 “content左边界相对于viewport左边界的位移” 和 “content右边界相对于viewport右边界的位移” 分别计算出 “完全滑出viewport左边界和右边界的Cell的数量”。

    2)、根据 “完全滑出viewport左边界和右边界的Cell的数量” 计算出 “需要显示的Cell的索引集合”。

    3)、开始和滑动时,根据 “相邻两帧的需要显示的Cell的索引集合” 对比出 “将要出现的的Cell的索引集合” 和 “将要消失的Cell的索引集合”。

    注意,新、旧、出现、消失 的索引集合,定义在全局,避免每帧创建,消耗堆内存。
    注意,每次保存前一帧的 “需要显示的Cell的索引集合”时。可交换新旧集合的引用指向,反复使用。

4、Cell复用的具体过程

   Cell出现时,从池中取或创建(池中无可用时则创建),并使其显示出来(SetActive(true))。

   Cell消失时,隐藏(SetActive(false))并放如池中。(注意,出入池都不更改其父物体)

5、考虑一下可能的极限情况

   Cell总数为0 时,content直接为0。不适用,直接return处理。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐