【Unity3D】无限循环列表(扩展版)

时间:2024-12-20 07:41:05

  基础版:【Unity技术分享】UGUI之ScrollRect优化_ugui scrollrect 优化-****博客

using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;

public delegate void OnBaseLoopListItemCallback(GameObject cell, int index);
public class BaseLoopList : MonoBehaviour
{
    public int m_Row = 1; //排
    public bool m_IsVertical = true;
    public float m_SpacingX = 0f; //间距
    public float m_SpacingY = 0f; //间距
    public GameObject m_CellGameObject; //指定的cell

    private Dictionary<GameObject, int> m_GameObjectNumDict;


    //-> 回调相关        
    public event OnBaseLoopListItemCallback onItemShow; //Lua 回调
    public event OnBaseLoopListItemCallback onItemHide; //Lua 回调

    protected RectTransform rectTrans;


    protected float m_PlaneWidth;
    protected float m_PlaneHeight;


    protected float m_ContentWidth;
    protected float m_ContentHeight;


    protected float m_CellObjectWidth;
    protected float m_CellObjectHeight;


    protected GameObject m_Content;
    protected RectTransform m_ContentRectTrans;


    private bool m_isInited = false;


    //记录 物体的坐标 和 物体 
    protected struct CellInfo
    {
        public Vector3 pos;
        public GameObject obj;
    };
    protected CellInfo[] m_CellInfos;


    protected bool m_IsInited = false;


    protected ScrollRect m_ScrollRect;


    protected int m_MaxCount = -1; //列表数量


    protected int m_MinIndex = -1;
    protected int m_MaxIndex = -1;


    protected bool m_IsClearList = false; //是否清空列表

    protected Vector4 m_Padding = Vector4.zero; //(left,top,right,bottom) 左,上 偏移Item(左上角为锚点) 右,下 扩大Content(宽度和高度)


    //c# 初始化
    public virtual void Init()
    {
        if (m_isInited)
            return;

        m_isInited = true;

        m_GameObjectNumDict = new Dictionary<GameObject, int>();
        m_Content = this.GetComponent<ScrollRect>().content.gameObject;


        if (m_CellGameObject == null)
        {
            //m_CellGameObject = m_Content.transform.GetChild(0).gameObject;
            Debug.LogError("m_CellGameObject 不能为 null");
        }
        /* Cell 处理 */
        //m_CellGameObject.transform.SetParent(m_Content.transform.parent, false);
        //SetPoolsObj(m_CellGameObject);

        RectTransform cellRectTrans = m_CellGameObject.GetComponent<RectTransform>();
        cellRectTrans.pivot = new Vector2(0f, 1f);
        CheckAnchor(cellRectTrans);
        cellRectTrans.anchoredPosition = Vector2.zero;


        //记录 Cell 信息
        m_CellObjectHeight = cellRectTrans.rect.height;
        m_CellObjectWidth = cellRectTrans.rect.width;


        //记录 Plane 信息
        rectTrans = GetComponent<RectTransform>();
        Rect planeRect = rectTrans.rect;
        m_PlaneHeight = planeRect.height;
        m_PlaneWidth = planeRect.width;


        //记录 Content 信息
        m_ContentRectTrans = m_Content.GetComponent<RectTransform>();
        Rect contentRect = m_ContentRectTrans.rect;
        SetContentHeight(contentRect.height);
        SetContentWidth(contentRect.width);


        m_ContentRectTrans.pivot = new Vector2(0f, 1f);
        //m_ContentRectTrans.sizeDelta = new Vector2 (planeRect.width, planeRect.height);
        //m_ContentRectTrans.anchoredPosition = Vector2.zero;
        CheckAnchor(m_ContentRectTrans);


        m_ScrollRect = this.GetComponent<ScrollRect>();


        m_ScrollRect.onValueChanged.RemoveAllListeners();
        //添加滑动事件
        m_ScrollRect.onValueChanged.AddListener(delegate (Vector2 value) { ScrollRectListener(value); });
    }

    public virtual void Destroy()
    {
        if (m_CellInfos != null)
        {
            for (int i = 0; i < m_CellInfos.Length; i++)
            {
                SetPoolsObj(m_CellInfos[i].obj);
                m_CellInfos[i].obj = null;
            }
        }
        m_GameObjectNumDict.Clear();
    }

    private void DisposeAll()
    {
        m_Padding = Vector4.zero;
        onItemShow = null;
        onItemHide = null;
        if (poolsObj != null)
        {
            foreach (var v in poolsObj)
            {
                if (v != null)
                {
                    GameObject.Destroy(v);
                }
            }
            poolsObj = null;
        }
        if (m_CellInfos != null)
        {
            foreach (var v in m_CellInfos)
            {
                if (v.obj != null)
                {
                    GameObject.Destroy(v.obj);
                }
            }
            m_CellInfos = null;
        }
    }

    //检查 Anchor 是否正确
    private void CheckAnchor(RectTransform rectTrans)
    {
        if (m_IsVertical)
        {
            if (!((rectTrans.anchorMin == new Vector2(0, 1) && rectTrans.anchorMax == new Vector2(0, 1)) ||
                     (rectTrans.anchorMin == new Vector2(0, 1) && rectTrans.anchorMax == new Vector2(1, 1))))
            {
                rectTrans.anchorMin = new Vector2(0, 1);
                rectTrans.anchorMax = new Vector2(1, 1);
            }
        }
        else
        {
            if (!((rectTrans.anchorMin == new Vector2(0, 1) && rectTrans.anchorMax == new Vector2(0, 1)) ||
                     (rectTrans.anchorMin == new Vector2(0, 0) && rectTrans.anchorMax == new Vector2(0, 1))))
            {
                rectTrans.anchorMin = new Vector2(0, 0);
                rectTrans.anchorMax = new Vector2(0, 1);
            }
        }
    }

    public void SetPadding(float left, float top, float right, float bottom)
    {
        m_Padding = new Vector4(left, top, right, bottom);
        SetContentWidth(m_ContentWidth);
        SetContentHeight(m_ContentHeight);
    }

    private float GetPaddingX()
    {
        return m_Padding.x;
    }

    private float GetPaddingY()
    {
        return m_Padding.y;
    }

    private float GetExpandWidth()
    {
        return m_Padding.z;
    }

    private float GetExpandHeight()
    {
        return m_Padding.w;
    }

    public void SetContentWidth(float value)
    {
        m_ContentWidth = value + GetPaddingX() + GetExpandWidth();
        m_ContentWidth = m_ContentWidth < rectTrans.rect.width ? rectTrans.rect.width : m_ContentWidth;
    }

    public void SetContentHeight(float value)
    {
        m_ContentHeight = value + GetPaddingY() + GetExpandHeight();
        m_ContentHeight = m_ContentHeight < rectTrans.rect.height ? rectTrans.rect.height : m_ContentHeight;
    }

    public virtual void SetCount(int num)
    {
        m_MinIndex = -1;
        m_MaxIndex = -1;


        //-> 计算 Content 尺寸
        if (m_IsVertical)
        {
            float contentSize = (m_SpacingY + m_CellObjectHeight) * Mathf.CeilToInt((float)num / m_Row);
            SetContentHeight(contentSize);
            SetContentWidth(m_ContentRectTrans.rect.width);
            m_ContentRectTrans.sizeDelta = new Vector2(m_ContentWidth, m_ContentHeight);
            if (num != m_MaxCount)
            {
                m_ContentRectTrans.anchoredPosition = new Vector2(m_ContentRectTrans.anchoredPosition.x, 0);
            }
        }
        else
        {
            float contentSize = (m_SpacingX + m_CellObjectWidth) * Mathf.CeilToInt((float)num / m_Row);
            SetContentWidth(contentSize);
            SetContentHeight(m_ContentRectTrans.rect.height);
            m_ContentRectTrans.sizeDelta = new Vector2(m_ContentWidth, m_ContentHeight);
            if (num != m_MaxCount)
            {
                m_ContentRectTrans.anchoredPosition = new Vector2(0, m_ContentRectTrans.anchoredPosition.y);
            }
        }

        //-> 计算 开始索引
        int lastEndIndex = 0;


        //-> 过多的物体 扔到对象池 ( 首次调 ShowList函数时 则无效 )
        if (m_IsInited)
        {
            lastEndIndex = num - m_MaxCount > 0 ? m_MaxCount : num;
            lastEndIndex = m_IsClearList ? 0 : lastEndIndex;


            int count = m_IsClearList ? m_CellInfos.Length : m_MaxCount;
            for (int i = lastEndIndex; i < count; i++)
            {
                if (m_CellInfos[i].obj != null)
                {
                    SetPoolsObj(m_CellInfos[i].obj);
                    if (onItemHide != null)
                    {
                        int objNum = 0;
                        m_GameObjectNumDict.TryGetValue(m_CellInfos[i].obj, out objNum);
                        onItemHide.Invoke(m_CellInfos[i].obj, objNum);
                    }
                    m_CellInfos[i].obj = null;
                }
            }
        }


        //-> 以下四行代码 在for循环所用
        CellInfo[] tempCellInfos = m_CellInfos;
        m_CellInfos = new CellInfo[num];


        //-> 1: 计算 每个Cell坐标并存储 2: 显示范围内的 Cell
        for (int i = 0; i < num; i++)
        {
            // * -> 存储 已有的数据 ( 首次调 ShowList函数时 则无效 )
            if (m_MaxCount != -1 && i < lastEndIndex)
            {
                CellInfo tempCellInfo = tempCellInfos[i];
                //-> 计算是否超出范围
                float rPos = m_IsVertical ? tempCellInfo.pos.y : tempCellInfo.pos.x;
                if (!IsOutRange(rPos))
                {
                    //-> 记录显示范围中的 首位index 和 末尾index
                    m_MinIndex = m_MinIndex == -1 ? i : m_MinIndex; //首位index
                    m_MaxIndex = i; // 末尾index


                    if (tempCellInfo.obj == null)
                    {
                        tempCellInfo.obj = GetPoolsObj();
                    }
                    tempCellInfo.obj.transform.GetComponent<RectTransform>().anchoredPosition = tempCellInfo.pos;
                    if (!m_GameObjectNumDict.ContainsKey(tempCellInfo.obj))
                    {
                        m_GameObjectNumDict.Add(tempCellInfo.obj, i);
                    }
                    else
                    {
                        m_GameObjectNumDict[tempCellInfo.obj] = i;
                    }
                    tempCellInfo.obj.SetActive(true);


                    //-> 回调 Lua 函数
                    Func(tempCellInfo.obj);
                }
                else
                {
                    if (tempCellInfo.obj != null)
                    {
                        SetPoolsObj(tempCellInfo.obj);
                        if (onItemHide != null)
                        {
                            int objNum = 0;
                            m_GameObjectNumDict.TryGetValue(tempCellInfo.obj, out objNum);
                            onItemHide.Invoke(tempCellInfo.obj, objNum);
                        }
                        tempCellInfo.obj = null;
                    }
                }
                m_CellInfos[i] = tempCellInfo;
                continue;
            }


            CellInfo cellInfo = new CellInfo();


            float pos = 0;  //坐标( isVertical ? 记录Y : 记录X )
            float rowPos = 0; //计算每排里面的cell 坐标


            // * -> 计算每个Cell坐标
            if (m_IsVertical)
            {
                pos = m_CellObjectHeight * Mathf.FloorToInt(i / m_Row) + m_SpacingY * Mathf.FloorToInt(i / m_Row);
                rowPos = m_CellObjectWidth * (i % m_Row) + m_SpacingX * (i % m_Row);
                cellInfo.pos = new Vector3(rowPos + GetPaddingX(), -pos - GetPaddingY(), 0);
            }
            else
            {
                pos = m_CellObjectWidth * Mathf.FloorToInt(i / m_Row) + m_SpacingX * Mathf.FloorToInt(i / m_Row);
                rowPos = m_CellObjectHeight * (i % m_Row) + m_SpacingY * (i % m_Row);
                cellInfo.pos = new Vector3(pos + GetPaddingX(), -rowPos - GetPaddingY(), 0);
            }


            //-> 计算是否超出范围
            float cellPos = m_IsVertical ? cellInfo.pos.y : cellInfo.pos.x;
            if (IsOutRange(cellPos))
            {
                cellInfo.obj = null;
                m_CellInfos[i] = cellInfo;
                continue;
            }


            //-> 记录显示范围中的 首位index 和 末尾index
            m_MinIndex = m_MinIndex == -1 ? i : m_MinIndex; //首位index
            m_MaxIndex = i; // 末尾index


            //-> 取或创建 Cell
            GameObject cell = GetPoolsObj();
            cell.transform.GetComponent<RectTransform>().anchoredPosition = cellInfo.pos;
            if (!m_GameObjectNumDict.ContainsKey(cell.gameObject))
            {
                m_GameObjectNumDict.Add(cell.gameObject, i);
            }
            else
            {
                m_GameObjectNumDict[cell.gameObject] = i;
            }


            //-> 存数据
            cellInfo.obj = cell;
            m_CellInfos[i] = cellInfo;


            //-> 回调 Lua 函数
            Func(cell);
        }


        m_MaxCount = num;
        m_IsInited = true;


    }


    //实时刷新列表时用
    public virtual void UpdateList()
    {
        NormalPerformanceMode();
    }


    //刷新某一项
    public void UpdateCell(int index)
    {
        CellInfo cellInfo = m_CellInfos[index - 1];
        if (cellInfo.obj != null)
        {
            float rangePos = m_IsVertical ? cellInfo.pos.y : cellInfo.pos.x;
            if (!IsOutRange(rangePos))
            {
                Func(cellInfo.obj);
            }
        }
    }


    // 更新滚动区域的大小
    public void UpdateSize()
    {
        Rect rect = GetComponent<RectTransform>().rect;
        m_PlaneHeight = rect.height;
        m_PlaneWidth = rect.width;
    }


    //滑动事件
    protected virtual void ScrollRectListener(Vector2 value)
    {
        NormalPerformanceMode(); //普通性能模式
    }

    //普通性能模式
    private void NormalPerformanceMode()
    {
        if (m_CellInfos == null)
            return;

        //检查超出范围
        for (int i = 0, length = m_CellInfos.Length; i < length; i++)
        {
            CellInfo cellInfo = m_CellInfos[i];
            GameObject obj = cellInfo.obj;
            Vector3 pos = cellInfo.pos;


            float rangePos = m_IsVertical ? pos.y : pos.x;
            //判断是否超出显示范围
            if (IsOutRange(rangePos))
            {
                //把超出范围的cell 扔进 poolsObj里
                if (obj != null)
                {
                    SetPoolsObj(obj);
                    if (onItemHide != null)
                    {
                        int objNum = 0;
                        m_GameObjectNumDict.TryGetValue(obj, out objNum);
                        onItemHide.Invoke(obj, objNum);
                    }
                    m_CellInfos[i].obj = null;
                }
            }
            else
            {
                if (obj == null)
                {
                    //优先从 poolsObj中 取出 (poolsObj为空则返回 实例化的cell)
                    GameObject cell = GetPoolsObj();
                    cell.transform.localPosition = pos;
                    if (!m_GameObjectNumDict.ContainsKey(cell.gameObject))
                    {
                        m_GameObjectNumDict.Add(cell.gameObject, i);
                    }
                    else
                    {
                        m_GameObjectNumDict[cell.gameObject] = i;
                    }
                    m_CellInfos[i].obj = cell;


                    Func(cell);
                }
            }
        }
    }


    //判断是否超出显示范围
    protected bool IsOutRange(float pos)
    {
        Vector3 listP = m_ContentRectTrans.anchoredPosition;
        if (m_IsVertical)
        {
            if (pos + listP.y > m_CellObjectHeight || pos + listP.y < -rectTrans.rect.height)
            {
                return true;
            }
        }
        else
        {
            if (pos + listP.x < -m_CellObjectWidth || pos + listP.x > rectTrans.rect.width)
            {
                return true;
            }
        }
        return false;
    }


    //对象池 机制  (存入, 取出) cell
    protected Stack<GameObject> poolsObj = new Stack<GameObject>();
    //取出 cell
    protected virtual GameObject GetPoolsObj()
    {
        GameObject cell = null;
        if (poolsObj.Count > 0)
        {
            cell = poolsObj.Pop();
        }


        if (cell == null)
        {
            cell = Instantiate(m_CellGameObject) as GameObject;
        }
        cell.transform.SetParent(m_Content.transform);
        cell.transform.localScale = Vector3.one;
        SetActive(cell, true);


        return cell;
    }
    //存入 cell
    protected virtual void SetPoolsObj(GameObject cell)
    {
        if (cell != null)
        {
            poolsObj.Push(cell);
            SetActive(cell, false);
        }
    }


    //回调
    protected void Func(GameObject selectObject)
    {
        int num = 0;
        m_GameObjectNumDict.TryGetValue(selectObject, out num);
        onItemShow?.Invoke(selectObject, num);
    }

    void SetActive(GameObject cell, bool isShow)
    {
        cell.SetActive(isShow);
    }

    protected void OnDestroy()
    {
        DisposeAll();
    }
}

其他2个使用案例C#脚本代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LoopListManager : MonoBehaviour
{
    public BaseLoopList baseLoopList;
    private Dictionary<int, BaseLoopListItem> loopListItemDict;

    void Start()
    {
        baseLoopList.onItemShow += OnShow;
        baseLoopList.onItemHide += OnHide;
        loopListItemDict = new Dictionary<int, BaseLoopListItem>();
        baseLoopList.Init();
        //baseLoopList.SetPadding(20, 30, 0, 0);//支持Padding四个方向的偏移
        baseLoopList.SetCount(50);
    }

    private BaseLoopListItem GetItem(GameObject cell, int index)
    {
        BaseLoopListItem item;
        loopListItemDict.TryGetValue(index, out item);
        if (item == null)
        {
            item = cell.GetComponent<BaseLoopListItem>();
            loopListItemDict.Add(index, item);
        }
        return item;
    }

    public void OnShow(GameObject cell, int index)
    {
        Debug.Log("Show:" + index);
        BaseLoopListItem item = GetItem(cell, index);
        item.OnShow(index);
    }

    public void OnHide(GameObject cell, int index)
    {
        Debug.Log("Hide:" + index);
        BaseLoopListItem item = GetItem(cell, index);
        item.OnHide(index);

        //注意: 无限循环列表的Item是重复利用的物体,逻辑上Hide之后必须从缓存中移除,否则会一定会出现Item刷新异常;
        //原因: 若不移除会出现Show(index物体)时会拿到一个不正确位置的物体, 甚至很大可能是一个已显示中的物体(形成覆盖效果)
        loopListItemDict.Remove(index);
    }
}
using UnityEngine;
using TMPro;
public class BaseLoopListItem : MonoBehaviour
{
    public TextMeshProUGUI text;

    public void OnShow(int index)
    {
        text.text = index.ToString();
    }

    public void OnHide(int index)
    {
        text.text = "";
    }
}

注意事项:无限循环列表的Item物体是重复利用的,因此OnHide回调后必须将传递进来的物体所有相关的缓存引用清空,例如案例是将其从字典移除loopListItemDict.Remove(index);

Content选择左上角锚点(看情况可选其他,但不要用自适应stretch) 以及不要挂任何自动控制布局组件,如:VerticalLayoutGroup、HorizontalLayoutGroup、GridLayoutGroup、ContentSizeFitter等...(因为会破坏无限循环列表对Content的控制)