在UI功能开发实践中,列表UI容器是我们经常使用一种UI容器组件。这种组件就根据输入的数据集合生成对应数据项目。从显示的方向来说,一般就分为水平排布和垂直排布的列表容器两种。列表容器为了在有限的界面空间中显示全部的数据,都会搭配使用UGUI的ScrollRect和Mask组件,我们只需要上下滑动,就可以浏览所要呈现的信息。但是,在UGUI中有几条数据就生成对应条目数的数据视图项,未免有些太过于奢侈。因为,每个数据项目不仅仅是一个UGUI的显示组件,而是多个显示组件(比如几个Text和Image)构成,最终,瞬时需要生成的GameObject总数目将会是数据总是的倍数。比如,我们有100条联系人数据,需要显示人的名称、电话、拨号按钮,背景Image直接挂在作为排列计算的母节点上,那么一个数据项目就需要至少4个GameObject表达。100条的数据将会导致一般的列表容器UI不得不同时生成400个GameObject,而同一时刻生成这么的GameObject,可以想象,Unity的运行时,将迎来一个不小的性能尖峰时刻,甚至我们能感受到界面进入假死状态。针对这个问题,一种比较理想的方案,就是用少量的GameObject去表示庞大的数据,随着滑动Scroll,mask对应的可显示区域,始终都是使用这几个GameObject条目显示当前的数据项。我们只需要不断的更新GameObject对应的列表的位置,已经及时设置起数据内容,就可以达成我们的目的。
原理是这样,那么现在谈及具体的实施细节。要实现这个功能,我们就需要确定用什么样的特征去衡量当前列表的状态。也就是,如何确保我们要怎样移动这些列表项的GameObject(后文将用Cell来指代这个条目的根节点GameObject)到达当前Scroll Position对应的位置,以及当前显示又是哪个条目的数据。
我们先设定列表项目的UI层级结构:
ScrollRect
-> Mask
--> Content
--> Cell 1
--> Cell 2
.........
--> Cell n
ScrollRect设定是只有垂直方向的移动能力,手动添加几个项目到Content中,上下移动ScrollRect,我们就发现Content的Position就会对应有一个移动操作。那么,事情就简单了,Content位移量和Cell的大小我们可以知道。对于垂直方向的表单,如果想知道当前显示的Cell是第几个,只需要拿Content的y值去除以于Cell的高度。那么我们只需要记下当前的index,一旦发现当前index发生变化,那么就可以使用这个变化,去移动Cell到需要的位置,并且也能做到最优先移动和更新看不见的Cell,原有可用的Cell维持不变。
那具体怎么看搬迁的方法,我们先看scroll向上的滑动。显然,这种状态下就是当前第一个cell的index小于Content能够被mask显示项目的currentIndex,那么就是找出哪些都是小于currentIndex的cell,并出队搬迁到cell列表的尾部,然后计算从哪个cell开始需要重新如何新的输入,并计算正确的偏移坐标。同理,scroll向下滑动也一样,只不过要补偿计算mask当前能显示多少个cell,从mask的底部开始计算找出可复用的cell节点。
OK,简单说完方法,我就放出演示的gif动画和代码。这个代码仅仅是实现上上述的功能,但是代码组织上还有可以迭代改进的空间,比如,可否把滑动向上向下的处理合二为一,或者是赋值重新排布的代码统一成一个等等。
using System;
using System.Collections.Generic;
using Neurons.SaturnUI.Data;
using Neurons.Util;
using UnityEngine;
using UnityEngine.UI; namespace Neurons.SaturnUI.UGUI
{
[RequireComponent(typeof(ScrollRect))]
public class SaturnVerticalRecycleListLayoutGroup : MonoBehaviour, ICollectionUIHandler
{
[SerializeField]
private UICell uiCellPrefab;
[SerializeField]
private RectTransform contentRect;
[SerializeField]
private float spacing = 5f; private Vector2 layoutSize;
private Vector2 cellSize;
private int displayContentCount; private List<UIData> datas = new List<UIData>(); private List<UICell> cellCaches = new List<UICell>();
private List<UICell> tmpRemoveds = new List<UICell>(); private int indexOfList; public void SetUIData(List<UIData> newDatas)
{
this.datas.Clear();
this.datas.AddRange(newDatas); UpdateUI();
} public void AddUIData(UIData newData)
{
this.datas.Add(newData); UpdateUI();
} public void RemoveUIData(UIData data)
{
this.RemoveUIData(data); UpdateUI();
} public void CleanUIDatas()
{
this.datas.Clear(); UpdateUI();
} public bool HasUIDatas()
{
return datas != null && datas.Count > ;
} public void UpdateUI()
{
this.contentRect.sizeDelta = new Vector2(this.contentRect.sizeDelta.x, (spacing + cellSize.y) * datas.Count); var currentIndex = (int)(contentRect.localPosition.y / (cellSize.y + spacing));
currentIndex = Math.Min(currentIndex, datas.Count - ); var j = currentIndex;
for (var i = ; j < datas.Count && i < cellCaches.Count; i++, j++)
{
var newCellGo = cellCaches[i]; newCellGo.gameObject.SetActiveEffectively(true);
newCellGo.transform.localPosition = new Vector3(, -j * (spacing + cellSize.y), ); newCellGo.SetUIData(datas[j]);
} indexOfList = currentIndex;
} private void Awake()
{
cellSize = uiCellPrefab.GetComponent<RectTransform>().sizeDelta;
layoutSize = this.GetComponent<RectTransform>().sizeDelta;
displayContentCount = (int)(layoutSize.y / cellSize.y); CreateCellCaches();
} private void Update()
{
var currentIndex = (int)(contentRect.localPosition.y / (cellSize.y + spacing)); if (indexOfList != currentIndex && currentIndex >= )
{
UpdateCellUI(currentIndex);
indexOfList = currentIndex;
}
} private void UpdateCellUI(int currentIndex)
{
if (cellCaches.Count > )
{
var cellIndex = (int)Math.Abs(cellCaches[].transform.localPosition.y / (cellSize.y + spacing)); if (cellIndex < currentIndex)
{
MoveForDown(currentIndex);
}
else if (cellIndex > currentIndex)
{
MoveForUp(currentIndex);
}
}
} private void MoveForUp(int currentIndex)
{
tmpRemoveds.Clear(); for (var i = cellCaches.Count - ; i >= ; i--)
{
var cell = cellCaches[i];
var cellIndex = (int)Math.Abs(cell.transform.localPosition.y / (cellSize.y + spacing)); if (cellIndex > currentIndex + displayContentCount)
{
tmpRemoveds.Add(cell);
}
} for (var i = ; i < tmpRemoveds.Count; i++)
{
cellCaches.Remove(tmpRemoveds[i]);
cellCaches.Insert(, tmpRemoveds[i]);
} var j = ;
for (var i = currentIndex; i < datas.Count && j < cellCaches.Count; i++, j++)
{
var cell = cellCaches[j];
cell.gameObject.SetActiveEffectively(true);
cell.SetUIData(datas[i]);
cell.transform.localPosition = new Vector3(, -i * (spacing + cellSize.y), );
} for (; j < cellCaches.Count; j++)
{
cellCaches[j].gameObject.SetActiveEffectively(false);
}
} private void MoveForDown(int currentIndex)
{
tmpRemoveds.Clear(); for (var i = ; i < cellCaches.Count; i++)
{
var cell = cellCaches[i];
var cellIndex = (int)Math.Abs(cell.transform.localPosition.y / (cellSize.y + spacing)); if (cellIndex < currentIndex)
{
tmpRemoveds.Add(cell);
}
} for (var i = ; i < tmpRemoveds.Count; i++)
{
cellCaches.Remove(tmpRemoveds[i]);
cellCaches.Add(tmpRemoveds[i]);
} var j = cellCaches.Count - tmpRemoveds.Count;
for (var i = currentIndex + cellCaches.Count - tmpRemoveds.Count; i < datas.Count && j < cellCaches.Count; i++, j++)
{
var cell = cellCaches[j];
cell.gameObject.SetActiveEffectively(true);
cell.SetUIData(datas[i]);
cell.transform.localPosition = new Vector3(, -i * (spacing + cellSize.y), );
} for (; j < cellCaches.Count; j++)
{
cellCaches[j].gameObject.SetActiveEffectively(false);
}
} private void CreateCellCaches()
{
var cacheCount = (int)(layoutSize.y / cellSize.y + ); for (var i = ; i < cacheCount; i++)
{
var newCellGo = uiCellPrefab.gameObject.Clone(false); newCellGo.DockTo(contentRect, true);
newCellGo.transform.localPosition = new Vector3(, -i * (spacing + cellSize.y), ); var cell = newCellGo.GetComponent<UICell>();
cellCaches.Add(cell); newCellGo.gameObject.SetActiveEffectively(false);
}
}
}
}
作者:雨天