Unity UGUI ScrollView性能优化,滑动列表的item/cell重复利用

unity中利用ugui制作scrollview有多个格子滑动时,最直接的做法是创建对应数量个格子节点,利用GameObject.Instanate创建节点本身就是性能开销很大的,如果有500个,1000个或者更多数据要显示,要创建这么多个节点,那么这卡顿一定很明显,这个数量级用这个做法实为下策。
如果接触过安卓/iOS原生app开发的应该记得它们的Scrollview / Tableview是有一套Item/Cell的复用机制的,就是当某个节点滑动出Scrollview 范围时,消失了不显示了,那么则移动到新的等待重新进入Scrollview 视野的位置重复利用,填充新的数据来显示,而不是创建新的节点来显示新的数据,从而节约性能的开销,所以即使显示十万条数据也不会卡顿。
通过这个思路,用Unity的UGUI实现了一遍,以此来提高显示大量数据时的Scrollview性能,这是十分有效的。
缺点:但也要注意的问题是,每个节点显示的数据总是随着Scrollview的滑动而变化的,也就是说节点和并不是某条数据绑定,而是动态变化的。所以,操作cell节点的UI引起数据变化时,需要我们谨慎操作,考虑到UI的cell节点所对应的的数据是哪条。


首先,项目源码地址:https://github.com/HengyuanLee/UGUIScrollGrid
**
实现过程:

在这里插入图片描述
在这里插入图片描述

用法:
在Canvas节点下创建ScrollGridVertical节点,挂上ScrollGridVerticalTest.cs,然后创建cell,挂到ScrollGridVerticalTest.cs脚本下即可运行。
在这里插入图片描述
ScrollGridVerticalTest.cs

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

public class ScrollGridVerticalTest : MonoBehaviour
{
    
    
    //将模板cell的GameObject的节点拉到这里。
    public GameObject tempCell;
    void Start()
    {
    
    
        ScrollGridVertical scrollGridVertical = gameObject.AddComponent<ScrollGridVertical>();
        //步骤一:设置模板cell。
        scrollGridVertical.tempCell = tempCell;
        //步骤二:设置cell刷新的事件监听。
        scrollGridVertical.AddCellListener(this.OnCellUpdate);
        //步骤三:设置数据总数。
        //如果数据有新的变化,重新直接设置即可。
        scrollGridVertical.SetCellCount(183);
    }
    /// <summary>
    /// 监听cell的刷新消息,修改cell的数据。
    /// </summary>
    /// <param name="cell"></param>
    private void OnCellUpdate(ScrollGridCell cell) {
    
    
        cell.gameObject.GetComponentInChildren<Text>().text = cell.index.ToString();
    }
}

ScrollGridHorizontalTest.cs

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class ScrollGridHorizontalTest : MonoBehaviour
{
    
    
    public GameObject tempCell;
    void Start()
    {
    
    
        ScrollGridHorizontal scrollGridVertical = gameObject.AddComponent<ScrollGridHorizontal>();
        scrollGridVertical.tempCell = tempCell;
        scrollGridVertical.AddCellListener(this.OnCellUpdate);
        scrollGridVertical.SetCellCount(153);
    }

    private void OnCellUpdate(ScrollGridCell cell)
    {
    
    
        cell.gameObject.GetComponentInChildren<Text>().text = cell.index.ToString();
    }
}

ScrollGridCell.cs

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

public class ScrollGridCell : MonoBehaviour
{
    
    
    private int _x;
    private int _y;
    private int _index;
    private int _objIndex;

    public int x {
    
     get {
    
     return _x; } }
    public int y {
    
     get {
    
     return _y; } }
    /// <summary>
    /// ScrollView滑动时,根据所显示数据而刷新变化的数据索引index,不断变化。
    /// </summary>
    public int index {
    
     get {
    
     return _index; } }
    /// <summary>
    /// 每个克隆出来的cell的GameObject所标记的唯一且固定的objIndex,确定后不再变化。
    /// </summary>
    public int objIndex {
    
     get {
    
     return _objIndex; } }

    /// <summary>
    /// 更新cell所滑动到的新的位置。
    /// </summary>
    /// <param name="x"></param>
    /// <param name="y"></param>
    /// <param name="index"></param>
    public void UpdatePos(int x, int y, int index)
    {
    
    
        this._x = x;
        this._y = y;
        this._index = index;
    }
    public void SetObjIndex(int objIndex) {
    
    
        this._objIndex = objIndex;
    }
}

ScrollGridVertical.cs

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


public class ScrollGridVertical : MonoBehaviour
{
    
    
    public GameObject tempCell;//模板cell,以此为目标,克隆出每个cell。
    private int cellCount;//要显示数据的总数。
    private float cellWidth;
    private float cellHeight;

    private List<System.Action<ScrollGridCell>> onCellUpdateList = new List<System.Action<ScrollGridCell>>();
    private ScrollRect scrollRect;

    private int row;//克隆cell的GameObject数量的行。
    private int col;//克隆cell的GameObject数量的列。

    private List<GameObject> cellList = new List<GameObject>();
    private bool inited;

    public void AddCellListener(System.Action<ScrollGridCell> call)
    {
    
    
        this.onCellUpdateList.Add(call);
        this.RefreshAllCells();
    }
    public void RemoveCellListener(System.Action<ScrollGridCell> call)
    {
    
    
        this.onCellUpdateList.Remove(call);
    }
    /// <summary>
    /// 设置ScrollGrid要显示的数据数量。
    /// </summary>
    /// <param name="count"></param>
    public void SetCellCount(int count)
    {
    
    
        this.cellCount = Mathf.Max(0, count);

        if (this.inited == false)
        {
    
    
            this.Init();
        }
        //重新调整content的高度,保证能够包含范围内的cell的anchoredPosition,这样才有机会显示。
        float newContentHeight = this.cellHeight * Mathf.CeilToInt((float)cellCount / this.col);
        float newMinY = -newContentHeight + this.scrollRect.viewport.rect.height;
        float maxY = this.scrollRect.content.offsetMax.y;
        newMinY += maxY;//保持位置
        newMinY = Mathf.Min(maxY, newMinY);//保证不小于viewport的高度。
        this.scrollRect.content.offsetMin = new Vector2(0, newMinY);
        this.CreateCells();
    }

    private void Init()
    {
    
    
        if (tempCell == null) {
    
    
            Debug.LogError("tempCell不能为空!");
            return;
        }
        this.inited = true;
        this.tempCell.SetActive(false);

        //创建ScrollRect下的viewpoint和content节点。
        this.scrollRect = gameObject.AddComponent<ScrollRect>();
        this.scrollRect.vertical = true;
        this.scrollRect.horizontal = false;
        GameObject viewport = new GameObject("viewport", typeof(RectTransform));
        viewport.transform.parent = transform;
        this.scrollRect.viewport = viewport.GetComponent<RectTransform>();
        GameObject content = new GameObject("content", typeof(RectTransform));
        content.transform.parent = viewport.transform;
        this.scrollRect.content = content.GetComponent<RectTransform>();

        //设置视野viewport的宽高和根节点一致。
        this.scrollRect.viewport.localScale = Vector3.one;
        this.scrollRect.viewport.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, 0);
        this.scrollRect.viewport.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, 0);
        this.scrollRect.viewport.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Right, 0, 0);
        this.scrollRect.viewport.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Bottom, 0, 0);
        this.scrollRect.viewport.anchorMin = Vector2.zero;
        this.scrollRect.viewport.anchorMax = Vector2.one;

        //设置viewpoint的mask。
        this.scrollRect.viewport.gameObject.AddComponent<Mask>().showMaskGraphic = false;
        Image image = this.scrollRect.viewport.gameObject.AddComponent<Image>();
        Rect viewRect = this.scrollRect.viewport.rect;
        image.sprite = Sprite.Create(new Texture2D(1, 1), new Rect(Vector2.zero, Vector2.one), Vector2.zero);

        //获取模板cell的宽高。
        Rect tempRect = tempCell.GetComponent<RectTransform>().rect;
        this.cellWidth = tempRect.width;
        this.cellHeight = tempRect.height;

        //设置viewpoint约束范围内的cell的GameObject的行列数。
        this.col = (int)(this.scrollRect.viewport.rect.width / this.cellWidth);
        this.col = Mathf.Max(1, this.col);
        this.row = Mathf.CeilToInt(this.scrollRect.viewport.rect.height / this.cellHeight);

        //初始化content。
        this.scrollRect.content.localScale = Vector3.one;
        this.scrollRect.content.offsetMax = new Vector2(0, 0);
        this.scrollRect.content.offsetMin = new Vector2(0, 0);
        this.scrollRect.content.anchorMin = Vector2.zero;
        this.scrollRect.content.anchorMax = Vector2.one;
        this.scrollRect.onValueChanged.AddListener(this.OnValueChange);
        this.CreateCells();

    }
    /// <summary>
    /// 刷新每个cell的数据
    /// </summary>
    public void RefreshAllCells()
    {
    
    
        foreach (GameObject cell in this.cellList)
        {
    
    
            this.cellUpdate(cell);
        }
    }
    /// <summary>
    /// 创建每个cell,并且根据行列定它们的位置,最多创建能够在视野范围内看见的个数,加上一行隐藏待进入视野的cell。
    /// </summary>
    private void CreateCells() {
    
    
        for (int r = 0; r < this.row + 1; r++)
        {
    
    
            for (int l = 0; l < this.col; l++)
            {
    
    
                int index = r * this.col + l;
                if (index < this.cellCount)
                {
    
    
                    if (this.cellList.Count <= index)
                    {
    
    
                        GameObject newcell = GameObject.Instantiate<GameObject>(this.tempCell);
                        newcell.SetActive(true);
                        //cell节点锚点强制设为左上角,以此方便算出位置。
                        RectTransform cellRect = newcell.GetComponent<RectTransform>();
                        cellRect.anchorMin = new Vector2(0, 1);
                        cellRect.anchorMax = new Vector2(0, 1);

                        //分别算出每个cell的位置。
                        float x = this.cellWidth / 2 + l * this.cellWidth;
                        float y = -r * this.cellHeight - this.cellHeight / 2;
                        cellRect.SetParent(this.scrollRect.content);
                        cellRect.localScale = Vector3.one;
                        cellRect.anchoredPosition = new Vector3(x, y);
                        newcell.AddComponent<ScrollGridCell>().SetObjIndex(index);
                        this.cellList.Add(newcell);
                    }
                }
            }
        }
        this.RefreshAllCells();
    }

    /// <summary>
    /// 滚动过程中,重复利用cell
    /// </summary>
    /// <param name="pos"></param>
    private void OnValueChange(Vector2 pos)
    {
    
    
        foreach (GameObject cell in this.cellList)
        {
    
    
            RectTransform cellRect = cell.GetComponent<RectTransform>();
            float dist = this.scrollRect.content.offsetMax.y + cellRect.anchoredPosition.y;
            float maxTop = this.cellHeight / 2;
            float minBottom = -((this.row + 1) * this.cellHeight) + this.cellHeight / 2;
            if (dist > maxTop)
            {
    
    
                float newY = cellRect.anchoredPosition.y - (this.row + 1) * this.cellHeight;
                //保证cell的anchoredPosition只在content的高的范围内活动,下同理
                if (newY > -this.scrollRect.content.rect.height)
                {
    
    
                    //重复利用cell,重置位置到视野范围内。
                    cellRect.anchoredPosition = new Vector3(cellRect.anchoredPosition.x, newY);
                    this.cellUpdate(cell);
                }

            }
            else if (dist < minBottom)
            {
    
    
                float newY = cellRect.anchoredPosition.y + (this.row + 1) * this.cellHeight;
                if (newY < 0)
                {
    
    
                    cellRect.anchoredPosition = new Vector3(cellRect.anchoredPosition.x, newY);
                    this.cellUpdate(cell);
                }
            }
        }
    }
    /// <summary>
    /// 所有的数据的真实行数
    /// </summary>
    private int allRow {
    
     get {
    
     return Mathf.CeilToInt((float)this.cellCount / this.col); } }
    /// <summary>
    /// cell被刷新时调用,算出cell的位置并调用监听的回调方法(Action)。
    /// </summary>
    /// <param name="cell"></param>
    private void cellUpdate(GameObject cell)
    {
    
    
        RectTransform cellRect = cell.GetComponent<RectTransform>();
        int x = Mathf.CeilToInt((cellRect.anchoredPosition.x - this.cellWidth / 2) / this.cellWidth);
        int y = Mathf.Abs(Mathf.CeilToInt((cellRect.anchoredPosition.y + this.cellHeight / 2) / this.cellHeight));
        int index = y * this.col + x;
        ScrollGridCell scrollGridCell = cell.GetComponent<ScrollGridCell>();
        scrollGridCell.UpdatePos(x, y, index);
        if (index >= cellCount || y >= this.allRow)
        {
    
    
            //超出数据范围
            cell.SetActive(false);
        }
        else
        {
    
    
            if (cell.activeSelf == false)
            {
    
    
                cell.SetActive(true);
            }
            foreach (var call in this.onCellUpdateList)
            {
    
    
                call(scrollGridCell);
            }
        }

    }
}

ScrollGridHorizontal.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class ScrollGridHorizontal : MonoBehaviour
{
    
    
    public GameObject tempCell;
    private int cellCount;
    private float cellWidth;
    private float cellHeight;
    private List<System.Action<ScrollGridCell>> onCellUpdateList = new List<System.Action<ScrollGridCell>>();

    private ScrollRect scrollRect;

    private int row;
    private int col;

    private bool inited;
    private List<GameObject> cellList = new List<GameObject>();

    public void AddCellListener(System.Action<ScrollGridCell> call)
    {
    
    
        this.onCellUpdateList.Add(call);
        this.RefreshAllCells();
    }
    public void RemoveCellListener(System.Action<ScrollGridCell> call)
    {
    
    
        this.onCellUpdateList.Remove(call);
    }
    public void SetCellCount(int count)
    {
    
    
        this.cellCount = Mathf.Max(0, count);

        if (this.inited == false)
        {
    
    
            this.Init();
        }
        float newContentWidth = this.cellWidth * Mathf.CeilToInt((float)this.cellCount / this.row);
        float newMaxX = newContentWidth - this.scrollRect.viewport.rect.width;//当minX==0时maxX的位置
        float minX = this.scrollRect.content.offsetMin.x;
        newMaxX += minX;
        newMaxX = Mathf.Max(minX, newMaxX);

        this.scrollRect.content.offsetMax = new Vector2(newMaxX, 0);
        this.CreateCells();
    }
    public void Init() {
    
     
        if (tempCell == null) {
    
    
            Debug.LogError("tempCell不能为空!");
            return;
        }
        this.inited = true;
        this.tempCell.SetActive(false);

        this.scrollRect = gameObject.AddComponent<ScrollRect>();
        this.scrollRect.vertical = false;
        this.scrollRect.horizontal = true;
        GameObject viewport = new GameObject("viewport", typeof(RectTransform));
        viewport.transform.parent = transform;
        this.scrollRect.viewport = viewport.GetComponent<RectTransform>();
        GameObject content = new GameObject("content", typeof(RectTransform));
        content.transform.parent = viewport.transform;
        this.scrollRect.content = content.GetComponent<RectTransform>();

        this.scrollRect.viewport.localScale = Vector3.one;
        this.scrollRect.viewport.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, 0);
        this.scrollRect.viewport.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, 0);
        this.scrollRect.viewport.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Right, 0, 0);
        this.scrollRect.viewport.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Bottom, 0, 0);
        this.scrollRect.viewport.anchorMin = Vector2.zero;
        this.scrollRect.viewport.anchorMax = Vector2.one;

        this.scrollRect.viewport.gameObject.AddComponent<Mask>().showMaskGraphic = false;
        Image image = this.scrollRect.viewport.gameObject.AddComponent<Image>();
        Rect viewRect = this.scrollRect.viewport.rect;
        image.sprite = Sprite.Create(new Texture2D(1, 1), new Rect(Vector2.zero, Vector2.one), Vector2.zero);
        Rect tempRect = tempCell.GetComponent<RectTransform>().rect;
        this.cellWidth = tempRect.width;
        this.cellHeight = tempRect.height;
        this.row = Mathf.FloorToInt(this.scrollRect.viewport.rect.height / this.cellHeight);
        this.row = Mathf.Max(1, this.row);
        this.col = Mathf.CeilToInt(this.scrollRect.viewport.rect.width / this.cellWidth);
        this.scrollRect.content.localScale = Vector3.one;
        this.scrollRect.content.offsetMax = new Vector2(0, 0);
        this.scrollRect.content.offsetMin = new Vector2(0, 0);
        this.scrollRect.content.anchorMin = Vector2.zero;
        this.scrollRect.content.anchorMax = Vector2.one;
        this.scrollRect.onValueChanged.AddListener(this.OnValueChange);
        this.CreateCells();
    }
    public void RefreshAllCells() {
    
    
        foreach (GameObject cell in this.cellList)
        {
    
    
            this.cellUpdate(cell);
        }
    }
    private void CreateCells()
    {
    
    
        for (int r = 0; r < this.row; r++)
        {
    
    
            for (int l = 0; l < this.col + 1; l++)
            {
    
    
                int index = r * (this.col+1) + l;
                if (index < this.cellCount)
                {
    
    
                    if (this.cellList.Count <= index)
                    {
    
    
                        GameObject newcell = GameObject.Instantiate<GameObject>(this.tempCell);
                        newcell.SetActive(true);
                        RectTransform cellRect = newcell.GetComponent<RectTransform>();
                        cellRect.anchorMin = new Vector2(0, 1);
                        cellRect.anchorMax = new Vector2(0, 1);

                        float x = this.cellWidth / 2 + l * this.cellWidth;
                        float y = -r * this.cellHeight - this.cellHeight / 2;
                        cellRect.SetParent(this.scrollRect.content);
                        cellRect.localScale = Vector3.one;
                        cellRect.anchoredPosition = new Vector3(x, y);
                        newcell.AddComponent<ScrollGridCell>().SetObjIndex(index);
                        this.cellList.Add(newcell);
                    }
                }
            }
        }
        this.RefreshAllCells();
    }

    private void OnValueChange(Vector2 pos)
    {
    
    
        foreach (GameObject cell in this.cellList)
        {
    
    
            RectTransform cellRect = cell.GetComponent<RectTransform>();
            float dist = this.scrollRect.content.offsetMin.x + cellRect.anchoredPosition.x;
            float minLeft =  -this.cellWidth / 2;
            float maxRight = this.col * this.cellWidth + this.cellWidth / 2;
            //限定复用边界
            if (dist < minLeft)
            {
    
    
                //控制cell的anchoredPosition在content的范围内才重复利用。
                float newX = cellRect.anchoredPosition.x + (this.col + 1) * this.cellWidth;
                if (newX < this.scrollRect.content.rect.width)
                {
    
    
                    cellRect.anchoredPosition = new Vector3(newX, cellRect.anchoredPosition.y);
                    this.cellUpdate(cell);
                }
            }
            if (dist > maxRight)
            {
    
    
                float newX = cellRect.anchoredPosition.x - (this.col + 1) * this.cellWidth;
                if (newX > 0) {
    
    
                    cellRect.anchoredPosition = new Vector3(newX, cellRect.anchoredPosition.y);
                    this.cellUpdate(cell);
                }
            }
        }
    }
    private int allCol{
    
     get {
    
     return Mathf.CeilToInt((float)this.cellCount / this.row); } }
    private void cellUpdate(GameObject cell)
    {
    
    
        RectTransform cellRect = cell.GetComponent<RectTransform>();
        int x = Mathf.CeilToInt((cellRect.anchoredPosition.x - cellWidth / 2) / cellWidth);
        int y = Mathf.Abs(Mathf.CeilToInt((cellRect.anchoredPosition.y + cellHeight / 2) / cellHeight));

        int index = y * allCol + x;
        ScrollGridCell scrollGridCell = cell.GetComponent<ScrollGridCell>();
        scrollGridCell.UpdatePos(x, y, index);
        if (index >= cellCount || x >= this.allCol)
        {
    
    
            cell.SetActive(false);
        }
        else
        {
    
    
            if (cell.activeSelf == false)
            {
    
    
                cell.SetActive(true);
            }
            foreach (var call in this.onCellUpdateList)
            {
    
    
                call(scrollGridCell);
            }
        }

    }
}

猜你喜欢

转载自blog.csdn.net/u012740992/article/details/102994534