再谈UGUI列表
在之前的两篇博客《UGUI的列表》和《UGUI列表进阶》中讨论了如何基于UGUI的ScollView通用可滑动组件设计一个功能相对窄化的列表组件方便使用,采用了类似Android系统中的ListView组件设计思想,抽象化的列表,剥离子项生成与列表本身,使用Adapter机制等等。
两篇博客一共讨论了两种实现方式,非重用的和重用的,两者的最大区别在于生成的子项数目不同,非重用版本会根据列表的总项目数生成对应数量的子项,在遇到大型列表时效率不佳;重用版本会根据列表的显示区域能装下的子项数目生成对应数量的子项,效率固定,但内部实现更加复杂。
虽然最后都设计出了满足基本需求的列表组件,但是它们的缺点依然明显,甚至有些致命。最大的一个缺点当属“仅支持子项类型唯一的列表”,这表示一个列表自始至终都只能显示一种子项,一旦有多种子项混合的列表需求,那么按照原本的设计几乎就不可能运行。
因此,列表组件需要再次改进。
改造的主要思想是明确列表类型,分为“子项类型唯一”和“子项类型不唯一”两种,理论上两种类型可以由同一种方式实现,但鉴于尽可能简化列表对Adapter的设计需求,最终还是采用了分两种类型实现的方式。
第一个改动是针对原来方案中以实现IUpdateViews接口的方式与Adapter联系的情况,结合作者的博客《自定义UGUI界面抽象框架》和《自定义事件驱动系统》中提到的抽象框架与事件系统,对列表的抽象层次再次细分。
新的设计关系图大致如下
之后从AbstractListView派生不同类型的列表组件即可
非重用版本改造
非重用版本的列表组件分为两类,子项唯一和子项不唯一
子项唯一的列表组件
设计方案和以前博客中讨论过的类似,重点代码如下
public class PlainListView : AbstractListView {
public const string TAG_PLAIN_LIST_VIEW = "PlainListView"; // 组件名称
protected int overIndex; // 全局索引
protected int prevCount; // 缓存上一次的项目数量
protected Dictionary<int, GameObject> objectCache; // 列表项目缓存,改善效率
// ------ 公用设置变量
public bool isHorizontal = false; // 设置是否水平布局(默认垂直布局,即普通列表)
public RectOffset paddingOffset; // 设置项目边距
public int spacing; // 设置项目间距
……
// 每帧执行,根据更新标志进行更新
protected override void execute() {
if (baseAdapter != null && dataUpdateFlag) {
if (baseAdapter.getCount() > 0) {
int innerCount = baseAdapter.getCount() / 30 + (baseAdapter.getCount() % 30 == 0 ? 1 : 0); // 计算每帧需要更新的项目数
innerCount += (innerCount == 0 ? 1 : 0);
innerUpdate(innerCount);
} else {
foreach (GameObject obj in objectCache.Values) {
if (obj.activeSelf) {
obj.SetActive(false);
}
}
dataUpdateFlag = false;
}
}
}
/// <summary>
/// 内循环更新
/// </summary>
/// <param name="innerCount">内循环更新次数</param>
private void innerUpdate(int innerCount) {
for (int i = 0; i < innerCount; i++) { // 内循环
fromCache(overIndex);
overIndex++;
if (overIndex >= baseAdapter.getCount()) { // 全部项目更新完成
dataUpdateFlag = false;
while (overIndex < prevCount) { // 更新项目少于已有的项目
toCache(overIndex++);
}
prevCount = baseAdapter.getCount();
break;
}
}
}
……
}
基本设计理念没有变化,还是使用LayoutGroup来自动排列子项,使用缓存来优化列表内容变更的效率,优缺点也是一样明确,对大型列表效果不佳。
子项不唯一的列表组件
该版本的设计思想是按照子项类型分别缓存到不同的队列中,因为对于非重用版本的列表而言,子项唯一与否并不直接影响列表本身的展示,因为要使用列表组件的前提已经是子项有LayoutElement组件描述其尺寸了,ContentRoot的宽高是自适应的,因此子项究竟是什么样子与列表本身的实现无关。
但是子项类型不唯一会影响到缓存机制,因为当子项唯一时采用的缓存机制是将子项与其索引位置绑定在一起,这样的缓存机制简单而且速度快;可这样的机制面对不同的子项类型时将会失效,当列表数据刷新时无法保证在指定索引上的子项类型满足需求,因此缓存机制必须做出改变。
改变的方法就是使用两个对象区域,一个保存所有正在展示的对象,另一个保存所有被隐藏掉的对象;为了保证机制的正确性,通过代码在Viewport对象下新建一个CacheRoot对象,将所有缓存下来并隐藏的对象都挂到CacheRoot下,保证不会影响ContentRoot中的子项排列。
而每次发生数据刷新时会清理所有在前台显示的对象并放入缓存,然后才根据新的数据将对应所需的对象取出来放入显示区域中,如果发现缓存中没有可取得的对象了则立刻新建一个。
在实际设计编写列表组件之前,为了支持这样的子项,首先要有合适的Adapter,因此需要修改BaseAdapter代码
public abstract class BaseAdapter : IAdapter {
/// <summary>
/// 列表刷新回调接口引用
/// </summary>
protected IUpdateViews listReference;
/// <summary>
/// 关联对象的标签字符串
/// </summary>
protected string relatedObject;
/// <summary>
/// 关联的列表对象名字
/// </summary>
protected string listObjectName;
……
……
/// <summary>
/// 通过索引值获取项目类型
/// </summary>
/// <param name="index">索引值</param>
/// <returns>项目类型</returns>
public virtual int getObjectType(int index) {
throw new ObjectTypeMismatchException("No Object Type can be used!");
}
/// <summary>
/// 通过项目类型获取尺寸
/// </summary>
/// <param name="index">项目类型</param>
/// <returns>尺寸数值</returns>
public virtual float getSizeByType(int viewType) {
throw new NotImplementedException("Cannot get size by item type, no such implementation!");
}
/// <summary>
/// 多种子项类型的列表中必须知道索引对应的子项的尺寸,如果未重写getObjectType则抛出该异常
/// </summary>
public class ObjectTypeMismatchException : ApplicationException {
private string error;
private Exception innerException;
//无参数构造函数
public ObjectTypeMismatchException() {
error = "Object Type Mismatch! Please check the adapter implementation!";
}
//带一个字符串参数的构造函数,作用:当程序员用Exception类获取异常信息而非ObjectTypeMismatchException时把自定义异常信息传递过去
public ObjectTypeMismatchException(string msg) : base(msg) {
error = msg;
}
//带有一个字符串参数和一个内部异常信息参数的构造函数
public