再谈UGUI列表

再谈UGUI列表

  在之前的两篇博客《UGUI的列表》和《UGUI列表进阶》中讨论了如何基于UGUI的ScollView通用可滑动组件设计一个功能相对窄化的列表组件方便使用,采用了类似Android系统中的ListView组件设计思想,抽象化的列表,剥离子项生成与列表本身,使用Adapter机制等等。
  两篇博客一共讨论了两种实现方式,非重用的和重用的,两者的最大区别在于生成的子项数目不同,非重用版本会根据列表的总项目数生成对应数量的子项,在遇到大型列表时效率不佳;重用版本会根据列表的显示区域能装下的子项数目生成对应数量的子项,效率固定,但内部实现更加复杂。
  虽然最后都设计出了满足基本需求的列表组件,但是它们的缺点依然明显,甚至有些致命。最大的一个缺点当属“仅支持子项类型唯一的列表”,这表示一个列表自始至终都只能显示一种子项,一旦有多种子项混合的列表需求,那么按照原本的设计几乎就不可能运行。
  因此,列表组件需要再次改进。
  改造的主要思想是明确列表类型,分为“子项类型唯一”和“子项类型不唯一”两种,理论上两种类型可以由同一种方式实现,但鉴于尽可能简化列表对Adapter的设计需求,最终还是采用了分两种类型实现的方式。
  第一个改动是针对原来方案中以实现IUpdateViews接口的方式与Adapter联系的情况,结合作者的博客《自定义UGUI界面抽象框架》和《自定义事件驱动系统》中提到的抽象框架与事件系统,对列表的抽象层次再次细分。
  新的设计关系图大致如下
  再谈UGUI列表_figure_0
  之后从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 ObjectTypeMismatchException(string msg, Exception innerException) : base(msg) {
            this.innerException = innerException;
            error = msg;
        }
        public string GetError() {
            return error;
        }
    }
}

  新的BaseAdapter中增加了两个虚方法,只要有多种类型子项需求的列表组件里都必须让Adapter重写这两个方法,否则抛出异常,无法正常运行。
  由此设计出的组件重点代码如下

public class CommonListView : AbstractListView {

    public const string TAG_COMMON_LIST_VIEW = "CommonListView"; // 组件名称

    private Transform cacheRoot; // 缓存对象挂载点

    private int overIndex; // 全局索引
    private int prevCount; // 缓存上一次的项目数量
    private float prevPos; // 缓存上一次的列表位置
    private float viewSize; // 保存列表视口尺寸
    private float curOffset; // 计算当前列表的高度

    private Dictionary<int, Queue<GameObject>> backgroundCache; // 列表项后台缓冲
    private Dictionary<int, Queue<GameObject>> foregroundCache; // 列表项前景缓存

    // ------ 公用设置变量
    public bool isHorizontal = false; // 设置是否水平布局(默认垂直布局,即普通列表)
    public RectOffset paddingOffset; // 设置项目边距
    public int spacing; // 设置项目间距

    ……

    // 设置更新标志,重置索引,启动更新过程
    public override void updateViews() {
        overIndex = 0;
        prepareCaches();
        notifyUpdate();
    }

    // 每帧执行,根据更新标志进行更新
    protected override void execute() {
        if (baseAdapter != null && dataUpdateFlag) {
            if (baseAdapter.getCount() > 0) {
                int innerCount = baseAdapter.getCount() / 60 + (baseAdapter.getCount() % 60 == 0 ? 1 : 0); // 计算每帧需要更新的项目数
                innerCount += (innerCount == 0 ? 1 : 0);
                for (int i = 0; i < innerCount; i++) { // 内循环
                    int viewType = baseAdapter.getObjectType(overIndex);
                    GameObject obj = fromBackground(viewType); // 查询缓存
                    obj = baseAdapter.getObject(obj, overIndex); // getObject方法将会判断是否需要新建对象
                    obj.transform.SetParent(contentRoot);
                    obj.transform.localScale = Vector3.one;
                    obj.SetActive(true);
                    curOffset += isHorizontal ? ((RectTransform)obj.transform).sizeDelta.x : ((RectTransform)obj.transform).sizeDelta.y;
                    toForeground(viewType, obj);
                    overIndex++;
                    if (overIndex >= baseAdapter.getCount()) { // 全部项目更新完成
                        dataUpdateFlag = false;
                        prevCount = baseAdapter.getCount();
                        Vector3 newPos = contentRoot.localPosition; // 重设列表位置
                        if (curOffset >= prevPos + viewSize) {
                            if (isHorizontal) {
                                newPos.x = prevPos;
                            } else {
                                newPos.y = prevPos;
                            }
                        } else {
                            if (isHorizontal) {
                                newPos.x = curOffset - viewSize;
                            } else {
                                newPos.y = curOffset - viewSize;
                            }
                        }
                        contentRoot.localPosition = newPos;
                        break;
                    }
                }
            }
        }
    }
    /// <summary>
    /// 准备缓存空间以及相关参数
    /// </summary>
    private void prepareCaches() {
        prevPos = isHorizontal ? contentRoot.localPosition.x : contentRoot.localPosition.y;
        curOffset = 0;
        if (foregroundCache != null) { // 将所有在前台展示的对象清理掉
            foreach (int viewType in foregroundCache.Keys) {
                Queue<GameObject> foregroundQueue = foregroundCache[viewType];
                Queue<GameObject> backgroundQueue = backgroundCache.TryGetElement(viewType);
                if (backgroundQueue == null) {
                    backgroundQueue = new Queue<GameObject>();
                    backgroundCache[viewType] = backgroundQueue;
                }
                while (foregroundQueue.Count > 0) {
                    GameObject obj = foregroundQueue.Dequeue();
                    obj.SetActive(false);
                    obj.transform.SetParent(cacheRoot);
                    backgroundQueue.Enqueue(obj);
                }
            }
        }
    }
    ……
}

  通过代码可以看到,整个列表的刷新流程一共三步,先清理显示区域里的所有子项,将它们放入对应的缓存队列中,随后刷新数据,使用新的数据依次获取子项类型并从缓存里取得对象,放入显示区域;最后刷新列表的位置,如果不进行最后一步那么每次刷新数据都会导致列表位置归零,这是因为每次刷新都清空了显示区域造成的。
  实测表明这样的机制是可以工作的,在提前知道了子项类型并且在Adapter中编写了相关代码后,列表便可以支持显示多种不同的子项,而且随意刷新不会出错。

重用版本改造

  重用版本的列表组件比起非重用版本而言要复杂得多,尤其是当子项不唯一时,需要考虑和计算的数据很繁琐。但好处也是显而易见的,重用情况下列表不受到数据多寡的限制,效率稳定,也不会因为生成大量的对象而变得臃肿。

子项唯一的可重用列表组件

  在作者以前的博客中提到的可重用列表设计是非常简陋而且低效的,它仅仅做到了“可重用而且可运行”,但在面对很多现实需求时显得十分无力,比如项目间距,页边距等等。
  如果依然采用旧版的设计思路,那么在子项不唯一的情况下其实现难度非常之高,几乎可以说是不可能实现,这显然是不可接受的;因此在进入子项不唯一的重用列表设计之前,首先需要一条设计可重用列表的新思路。
  旧版思路是逐帧检查是否出现了显示区域子项的变化,比如向上或者向下推出超过阈值,单帧滑动极快等等,并根据检查结果进行子项移动以及重排等操作。思路本身没有问题,也成功实现了可重用列表,但是这种分散管理的方案实际上对代码编写是很大的障碍,查看原版的可重用列表代码就能看到,在这种思路下设计出的组件代码充满了常量,微调,以及让人摸不着头脑的位移,那个专门设计的CycleList循环列表使用也不清不楚。
  这样的组件不但功能不足,更是让使用者连通过代码理清其工作原理的心情都不会有。
  因此在新的列表组件里使用了另外一个思路,这个思路来自于采样窗口。
  在电子信号采样中,由于采样的时间不可能无限长,因此需要确定一个采样时间段,这就被称为“采样窗口”;基于类似的思想,重用列表本质上也就是一个采样窗口,它通过视口来对列表数据进行采样,列表的滑动就是采样内容的变化。
  新思路大概就是这样,使用一个比视口尺寸稍大的“窗口”来放置当前显示的所有子项,该“窗口”可以和非重用列表那样使用LayoutGroup和SizeFitter组件自动布局;所有的子项放入缓存和显示两个区域中,每当有滑动发生,同步检查是否需要将头部推出的子项放入缓存以及是否需要在即将推入的部分放入新的子项;如果单帧滑动距离过长则进行重新布局。
  重点代码如下

public class PlainRecycleListView : AbstractListView {

    public const string TAG_PLAIN_RECYCLE_LIST_VIEW = "PlainRecycleListView"; // 组件名称

    private float ITEM_SHIFT_LIMIT = 20f;

    private Transform cacheRoot; // 缓存区域根对象
    private RectTransform sliderRoot; // 滑动区根对象

    private Deque<Tuples.BiTuple<int, GameObject>> sliderContentQueue; // 滑动区域双端队列缓存,二元组(索引,对象)
    private List<Tuples.BiTuple<float, float>> listItemSizeList; // 列表全体子项尺寸列表,二元组(当前项目尺寸,所处位置)
    private Queue<GameObject> backgroundQueue; // 后台缓冲队列

    private float prevPos; // 缓存上一次的列表位置
    private float itemSize; // 单个列表项的尺寸
    private float viewSize; // 视口尺寸
    private float listSize; // 列表尺寸
    private float leftPadding; // 左间隔
    private float rightPadding; // 右间隔
    private float topPadding; // 上间隔
    private float bottomPadding; // 下间隔
    private float startPosition; // 起始位置,用于设置边距时

    // ------ 公用变量
    public bool isHorizontal = false; // 是否水平方向的列表
    public RectOffset paddingOffset; // 设置项目边距
    public int spacing; // 间距

    ……

    /// <summary>
    /// 每帧执行方法
    /// </summary>
    protected override void execute() {
        if (baseAdapter != null) {
            float offsetPos = isHorizontal ? -contentRoot.localPosition.x : contentRoot.localPosition.y;
            if (dataUpdateFlag) {
                localUpdate(offsetPos);
            } else {
                checkRecycle(offsetPos);
            }
        }
    }

    /// <summary>
    /// 本地刷新方法,用于外部调用Adapter.notifyDataChanged方法时进行刷新
    /// </summary>
    /// <param name="offsetPos">当前列表的偏移量</param>
    private void localUpdate(float offsetPos) {
        // 查询子项尺寸
        if (baseAdapter.getCount() > 0) {
            GameObject obj = baseAdapter.getObject(null, 0);
            LayoutElement ele = obj.GetComponent<LayoutElement>();
            itemSize = isHorizontal ? ele.preferredWidth : ele.preferredHeight;
            ITEM_SHIFT_LIMIT = itemSize / 3;
            toBackground(obj);
            Vector2 size = contentRoot.sizeDelta;
            float sizeSum = 0f;
            listItemSizeList.Clear();
            sizeSum += (isHorizontal ? leftPadding : topPadding);
            for (int i = 0; i < baseAdapter.getCount(); i++) { // 生成列表项目的尺寸以及所在位置集合
                float iSize = itemSize;
                Tuples.BiTuple<float, float> itemTuple = new Tuples.BiTuple<float, float>();
                if (i < baseAdapter.getCount() - 1) {
                    iSize += spacing;
                }
                itemTuple.E1 = iSize;
                itemTuple.E2 = sizeSum;
                listItemSizeList.Add(itemTuple);
                sizeSum += iSize;
            }
            listSize = sizeSum;
            if (isHorizontal) {
                size.x = listSize + rightPadding;
            } else {
                size.y = listSize + bottomPadding;
            }
            contentRoot.sizeDelta = size;
        }
        while (sliderContentQueue.Count > 0) {
            Tuples.BiTuple<int, GameObject> objTuple = sliderContentQueue.DequeueHead();
            toBackground(objTuple.E2);
        }
        int offsetIndex = FindIndexByOffset(offsetPos);
        int initIndex = 0;
        if (offsetPos > listSize - viewSize) { // 重设位置,在刷新时需要
            // 超过底端,要调整位置
            int avaIndex = FindIndexByOffset(listSize - viewSize);
            initIndex = avaIndex;
            changeSliderPos(listItemSizeList[avaIndex].E2);
            prevPos = listSize - viewSize;
        } else {
            initIndex = offsetIndex;
            changeSliderPos(listItemSizeList[0].E2);
            prevPos = offsetPos;
        }
        while (initIndex < baseAdapter.getCount() && listItemSizeList[initIndex].E2 <= listSize) { // 补足显示的子项
            wakeupItemToPosition(initIndex);
            Tuples.BiTuple<float, float> checkTuple = listItemSizeList[initIndex];
            initIndex++;
            if (checkTuple.E2 + checkTuple.E1 >= prevPos + viewSize) {
                break;
            }
        }
        dataUpdateFlag = false;
    }

    /// <summary>
    /// 每帧检查重用状态
    /// </summary>
    /// <param name="offsetPos">当前列表偏移量</param>
    private void checkRecycle(float offsetPos) {
        if (listSize > viewSize && baseAdapter.getCount() > 0) {
            Tuples.BiTuple<int, GameObject> headTuple = sliderContentQueue.PeekHead();
            Tuples.BiTuple<int, GameObject> tailTuple = sliderContentQueue.PeekTail();
            Tuples.BiTuple<float, float> headSize = listItemSizeList[headTuple.E1];
            Tuples.BiTuple<float, float> tailSize = listItemSizeList[tailTuple.E1];
            if (offsetPos > prevPos && offsetPos <= listSize - viewSize) { // 列表被向上推,顶端推出,底端推入;限制在触底后不进行处理
                if (offsetPos - prevPos <= headSize.E1) { // 单帧滑动距离在一个子项范围内时
                    if (offsetPos > headSize.E2 + headSize.E1 + ITEM_SHIFT_LIMIT) { // 顶部最上的元素的底已经被推出视口之外且多移动了预定的距离
                        sliderContentQueue.DequeueHead();
                        toBackground(headTuple.E2);
                        changeSliderPos(listItemSizeList[headTuple.E1 + 1].E2);
                    }
                    float botPos = offsetPos + viewSize;
                    if (botPos > tailSize.E2 + ITEM_SHIFT_LIMIT) { // 底部最底元素的顶露出预定距离后
                        int newIndex = tailTuple.E1 + 1;
                        if (newIndex < baseAdapter.getCount()) {
                            wakeupItemToPosition(newIndex);
                        }
                    }
                } else { // 否则找到推出项目数,重排队列和重设滑动区域位置
                    int curIndex = FindIndexByOffset(offsetPos);
                    for (int i = headTuple.E1; i <= curIndex; i++) {
                        if (sliderContentQueue.Count > 0) {
                            headTuple = sliderContentQueue.DequeueHead();
                            toBackground(headTuple.E2);
                        }
                    }
                    if (sliderContentQueue.Count > 0) { // 是否已经滑过整个滑动区域的距离
                        int headIndex = sliderContentQueue.PeekHead().E1;
                        int cusorIndex = tailTuple.E1 + 1;
                        headSize = listItemSizeList[headIndex];
                        changeSliderPos(listItemSizeList[headIndex].E2);
                        while (cusorIndex < baseAdapter.getCount() && listItemSizeList[cusorIndex].E2 <= listSize) {
                            wakeupItemToPosition(cusorIndex);
                            Tuples.BiTuple<float, float> checkTuple = listItemSizeList[cusorIndex];
                            cusorIndex++;
                            if (checkTuple.E2 + checkTuple.E1 >= offsetPos + viewSize) {
                                break;
                            }
                        }
                    } else { // 单帧内就滑过了整个滑动区域的距离,需要重新排队
                        int headIndex = Mathf.Max(curIndex - 1, 0);
                        int cusorIndex = headIndex;
                        headSize = listItemSizeList[headIndex];
                        changeSliderPos(listItemSizeList[headIndex].E2);
                        while (cusorIndex < baseAdapter.getCount() && listItemSizeList[cusorIndex].E2 <= listSize) {
                            wakeupItemToPosition(cusorIndex);
                            Tuples.BiTuple<float, float> checkTuple = listItemSizeList[cusorIndex];
                            cusorIndex++;
                            if (checkTuple.E2 + checkTuple.E1 >= offsetPos + viewSize) {
                                break;
                            }
                        }
                    }
                }
            } else if (offsetPos < prevPos && offsetPos >= 0) { // 列表被向下推,顶端推入,底端推出;限制在触顶后不进行处理
                if (prevPos - offsetPos <= tailSize.E1) { // 单帧滑动距离在一个子项范围内时
                    float botPos = offsetPos + viewSize;
                    if (botPos < tailSize.E2 - ITEM_SHIFT_LIMIT) { // 底部最下元素的顶已经推出视口之外且多移动了预定的距离
                        sliderContentQueue.DequeueTail();
                        toBackground(tailTuple.E2);
                    }
                    if (offsetPos < headSize.E2 + ITEM_SHIFT_LIMIT) { // 顶部最上的元素的顶接近视口达到预定距离
                        int newIndex = headTuple.E1 - 1;
                        if (newIndex >= 0) {
                            wakeupItemToPosition(newIndex, true);
                            changeSliderPos(listItemSizeList[newIndex].E2);
                        }
                    }
                } else { // 否则找到推出项目数,重排队列和重设滑动区域位置
                    int curIndex = FindIndexByOffset(offsetPos + viewSize) + 1;
                    for (int i = curIndex; i <= tailTuple.E1; i++) {
                        if (sliderContentQueue.Count > 0) {
                            tailTuple = sliderContentQueue.DequeueTail();
                            toBackground(tailTuple.E2);
                        }
                    }
                    if (sliderContentQueue.Count > 0) { // 是否已经滑过整个滑动区域的距离
                        int headIndex = FindIndexByOffset(offsetPos) - 1;
                        headIndex = Mathf.Max(headIndex, 0);
                        int cusorIndex = headTuple.E1 - 1;
                        headSize = listItemSizeList[headIndex];
                        changeSliderPos(listItemSizeList[headIndex].E2);
                        while (cusorIndex >= headIndex) {
                            wakeupItemToPosition(cusorIndex, true);
                            cusorIndex--;
                        }
                    } else { // 单帧内就滑过了整个滑动区域的距离,需要重新排队
                        int headIndex = FindIndexByOffset(offsetPos) - 1;
                        headIndex = Mathf.Max(headIndex, 0);
                        int cusorIndex = headIndex;
                        headSize = listItemSizeList[headIndex];
                        changeSliderPos(listItemSizeList[headIndex].E2);
                        while (cusorIndex < baseAdapter.getCount() && listItemSizeList[cusorIndex].E2 <= listSize) {
                            wakeupItemToPosition(cusorIndex);
                            Tuples.BiTuple<float, float> checkTuple = listItemSizeList[cusorIndex];
                            cusorIndex++;
                            if (checkTuple.E2 + checkTuple.E1 >= offsetPos + viewSize) {
                                break;
                            }
                        }
                    }
                }
            }
            prevPos = offsetPos;
        }
    }

    ……
}

  代码的核心部分在localUpdate和checkRecycle两个方法中,Deque是自定义的双端队列类,内部采用泛型List结构实现,效率不高,但在重用列表的场合下不成问题;所有的Tuples相关类都是自定义的泛型多元组,仅仅为了方便代码编写,将其拆分为多个List并没有实质的区别。
  实测中这样的设计能很好地兼顾可重用列表以及对边距和间距的支持,刷新也没有问题。

子项不唯一的可重用列表

  有了前文的设计思路,这个组件就是细节区别了,不同类型的组件尺寸不同因此需要考虑的条件也有区别,所需的缓存结构也有不同,但整体设计是类似的。
  重点代码如下

public class CommonRecycleListView : AbstractListView {

    public const string TAG_COMMON_RECYCLE_LIST_VIEW = "CommonRecycleListView"; // 组件名称
    private const float ITEM_SHIFT_LIMIT = 20f;

    private Transform cacheRoot; // 缓存区域根对象
    private RectTransform sliderRoot; // 滑动区根对象

    private Deque<Tuples.TriTuple<int, int, GameObject>> sliderContentQueue; // 滑动区域双端队列缓存,三元组(索引,类型,对象)
    private Dictionary<int, float> itemTypeSize; // 子项类型和尺寸映射表
    private List<Tuples.TriTuple<int, float, float>> listItemSizeList; // 列表全体子项尺寸列表,三元组(类型,当前项目尺寸,所处位置)
    private Dictionary<int, List<GameObject>> backgroundCache; // 列表项后台缓冲
    private Dictionary<int, List<GameObject>> foregroundCache; // 列表项前景缓存

    private float prevPos; // 缓存上一次的列表位置
    private float viewSize; // 视口尺寸
    private float listSize; // 列表尺寸
    private float leftPadding; // 左间隔
    private float rightPadding; // 右间隔
    private float topPadding; // 上间隔
    private float bottomPadding; // 下间隔

    // ------ 公用变量
    public bool isHorizontal = false; // 是否水平方向的列表
    public RectOffset paddingOffset; // 设置项目边距,在水平列表中仅有top和bot生效,在垂直列表中则仅有left和right生效
    public int spacing; // 间距

    ……

    /// <summary>
    /// 每帧执行方法
    /// </summary>
    protected override void execute() {
        if (baseAdapter != null) {
            float offsetPos = isHorizontal ? -contentRoot.localPosition.x : contentRoot.localPosition.y;
            if (dataUpdateFlag) {
                localUpdate(offsetPos);
            } else {
                checkRecycle(offsetPos);
            }
        }
    }

    /// <summary>
    /// 本地刷新方法,用于数据列表变化时进行刷新
    /// </summary>
    /// <param name="offsetPos">当前列表的偏移量</param>
    private void localUpdate(float offsetPos) {
        // 就地刷新
        Vector2 size = contentRoot.sizeDelta;
        float sizeSum = 0f;
        listItemSizeList.Clear();
        sizeSum += (isHorizontal ? leftPadding : topPadding);
        for (int i = 0; i < baseAdapter.getCount(); i++) {
            int viewType = baseAdapter.getObjectType(i);
            float itemSize = CheckSizeByType(viewType);
            Tuples.TriTuple<int, float, float> itemTuple = new Tuples.TriTuple<int, float, float>();
            if(i < baseAdapter.getCount() - 1) {
                itemSize += spacing;
            }
            itemTuple.E1 = viewType;
            itemTuple.E2 = itemSize;
            itemTuple.E3 = sizeSum;
            listItemSizeList.Add(itemTuple);
            sizeSum += itemSize;
        }
        listSize = sizeSum;
        if (isHorizontal) {
            size.x = listSize + rightPadding;
        } else {
            size.y = listSize + bottomPadding;
        }
        contentRoot.sizeDelta = size;
        while(sliderContentQueue.Count > 0) {
            Tuples.TriTuple<int, int, GameObject> objTuple = sliderContentQueue.DequeueHead();
            toBackground(objTuple.E2, objTuple.E3);
        }
        int offsetIndex = FindIndexByOffset(offsetPos);
        int initIndex = 0;
        if (offsetPos > listSize - viewSize) {
            // 超过底端,要调整位置
            int avaIndex = FindIndexByOffset(listSize - viewSize);
            initIndex = avaIndex;
            changeSliderPos(listItemSizeList[avaIndex].E3);
            prevPos = listSize - viewSize;
        } else {
            initIndex = offsetIndex;
            changeSliderPos(listItemSizeList[0].E3);
            prevPos = offsetPos;
        }
        while (initIndex < baseAdapter.getCount() && listItemSizeList[initIndex].E3 <= listSize) {
            wakeupItemToPosition(initIndex);
            Tuples.TriTuple<int, float, float> checkTuple = listItemSizeList[initIndex];
            initIndex++;
            if(checkTuple.E3 + checkTuple.E2 >= prevPos + viewSize) {
                break;
            }
        }
        dataUpdateFlag = false;
    }

    /// <summary>
    /// 每帧检查重用状态
    /// </summary>
    /// <param name="offsetPos">当前列表偏移量</param>
    private void checkRecycle(float offsetPos) {
        if (listSize > viewSize && baseAdapter.getCount() > 0) {
            Tuples.TriTuple<int, int, GameObject> headTuple = sliderContentQueue.PeekHead();
            Tuples.TriTuple<int, int, GameObject> tailTuple = sliderContentQueue.PeekTail();
            Tuples.TriTuple<int, float, float> headSize = listItemSizeList[headTuple.E1];
            Tuples.TriTuple<int, float, float> tailSize = listItemSizeList[tailTuple.E1];
            if(offsetPos > prevPos && offsetPos <= listSize - viewSize) { // 列表被向上推,顶端推出,底端推入;限制在触底后不进行处理
                if (offsetPos - prevPos <= headSize.E2) { // 单帧滑动距离在一个子项范围内时
                    if (offsetPos > headSize.E3 + headSize.E2 + ITEM_SHIFT_LIMIT) { // 顶部最上的元素的底已经被推出视口之外且多移动了预定的距离
                        sliderContentQueue.DequeueHead();
                        toBackground(headTuple.E2, headTuple.E3);
                        changeSliderPos(listItemSizeList[headTuple.E1 + 1].E3);
                    }
                    float botPos = offsetPos + viewSize;
                    if (botPos > tailSize.E3 + ITEM_SHIFT_LIMIT) { // 底部最底元素的顶露出预定距离后
                        int newIndex = tailTuple.E1 + 1;
                        if (newIndex < baseAdapter.getCount()) {
                            wakeupItemToPosition(newIndex);
                        }
                    }
                } else { // 否则找到推出项目数,重排队列和重设滑动区域位置
                    int curIndex = FindIndexByOffset(offsetPos);
                    for(int i = headTuple.E1; i <= curIndex; i++) {
                        if (sliderContentQueue.Count > 0) {
                            headTuple = sliderContentQueue.DequeueHead();
                            toBackground(headTuple.E2, headTuple.E3);
                        }
                    }
                    if(sliderContentQueue.Count > 0) { // 是否已经滑过整个滑动区域的距离
                        int headIndex = sliderContentQueue.PeekHead().E1;
                        int cusorIndex = tailTuple.E1 + 1;
                        headSize = listItemSizeList[headIndex];
                        changeSliderPos(listItemSizeList[headIndex].E3);
                        while (cusorIndex < baseAdapter.getCount() && listItemSizeList[cusorIndex].E3 <= listSize) {
                            wakeupItemToPosition(cusorIndex);
                            Tuples.TriTuple<int, float, float> checkTuple = listItemSizeList[cusorIndex];
                            cusorIndex++;
                            if (checkTuple.E3 + checkTuple.E2 >= offsetPos + viewSize) {
                                break;
                            }
                        }
                    } else { // 单帧内就滑过了整个滑动区域的距离,需要重新排队
                        int headIndex = Mathf.Max(curIndex - 1, 0);
                        int cusorIndex = headIndex;
                        headSize = listItemSizeList[headIndex];
                        changeSliderPos(listItemSizeList[headIndex].E3);
                        while (cusorIndex < baseAdapter.getCount() && listItemSizeList[cusorIndex].E3 <= listSize) {
                            wakeupItemToPosition(cusorIndex);
                            Tuples.TriTuple<int, float, float> checkTuple = listItemSizeList[cusorIndex];
                            cusorIndex++;
                            if (checkTuple.E3 + checkTuple.E2 >= offsetPos + viewSize) {
                                break;
                            }
                        }
                    }
                }
            } else if(offsetPos < prevPos && offsetPos >= 0) { // 列表被向下推,顶端推入,底端推出;限制在触顶后不进行处理
                if (prevPos - offsetPos <= tailSize.E2) { // 单帧滑动距离在一个子项范围内时
                    float botPos = offsetPos + viewSize;
                    if (botPos < tailSize.E3 - ITEM_SHIFT_LIMIT) { // 底部最下元素的顶已经推出视口之外且多移动了预定的距离
                        sliderContentQueue.DequeueTail();
                        toBackground(tailTuple.E2, tailTuple.E3);
                    }
                    if (offsetPos < headSize.E3 + ITEM_SHIFT_LIMIT) { // 顶部最上的元素的顶接近视口达到预定距离
                        int newIndex = headTuple.E1 - 1;
                        if (newIndex >= 0) {
                            wakeupItemToPosition(newIndex, true);
                            changeSliderPos(listItemSizeList[newIndex].E3);
                        }
                    }
                } else { // 否则找到推出项目数,重排队列和重设滑动区域位置
                    int curIndex = FindIndexByOffset(offsetPos + viewSize) + 1;
                    for (int i = curIndex; i <= tailTuple.E1; i++) {
                        if (sliderContentQueue.Count > 0) {
                            tailTuple = sliderContentQueue.DequeueTail();
                            toBackground(tailTuple.E2, tailTuple.E3);
                        }
                    }
                    if(sliderContentQueue.Count > 0) { // 是否已经滑过整个滑动区域的距离
                        int headIndex = FindIndexByOffset(offsetPos) - 1;
                        headIndex = Mathf.Max(headIndex, 0);
                        int cusorIndex = headTuple.E1 - 1;
                        headSize = listItemSizeList[headIndex];
                        changeSliderPos(listItemSizeList[headIndex].E3);
                        while(cusorIndex >= headIndex) {
                            wakeupItemToPosition(cusorIndex, true);
                            cusorIndex--;
                        }
                    } else { // 单帧内就滑过了整个滑动区域的距离,需要重新排队
                        int headIndex = FindIndexByOffset(offsetPos) - 1;
                        headIndex = Mathf.Max(headIndex, 0);
                        int cusorIndex = headIndex;
                        headSize = listItemSizeList[headIndex];
                        changeSliderPos(listItemSizeList[headIndex].E3);
                        while (cusorIndex < baseAdapter.getCount() && listItemSizeList[cusorIndex].E3 <= listSize) {
                            wakeupItemToPosition(cusorIndex);
                            Tuples.TriTuple<int, float, float> checkTuple = listItemSizeList[cusorIndex];
                            cusorIndex++;
                            if (checkTuple.E3 + checkTuple.E2 >= offsetPos + viewSize) {
                                break;
                            }
                        }
                    }
                }
            }
            prevPos = offsetPos;
        }
    }

    ……
}

  至此列表组件的新思路就介绍完毕,实测表明四个组件代码都能正常运行,无论是水平列表还是垂直列表;刷新,重用,多种子项,间距和边距等等特性的支持也正常,但没有经过实际项目的考验,只能说初步判断可用。

猜你喜欢

转载自blog.csdn.net/soul900524/article/details/79663210