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);
}
}
}
}