软件编程
位置:首页>> 软件编程>> C#编程>> 解决Unity无限滚动复用列表的问题

解决Unity无限滚动复用列表的问题

作者:小紫苏  发布时间:2022-12-26 04:42:18 

标签:Unity,无限滚动,列表

无限滚动复用列表

Demo展示

解决Unity无限滚动复用列表的问题

解决Unity无限滚动复用列表的问题

前言

游戏中有非常多的下拉滚动菜单,比如成就列表,任务列表,以及背包仓库之类;如果列表内容非常丰富,会占用大量内存,这篇无限滚动复用ScrollView就是解决这种问题;还可以用来做朋友圈,聊天等;

一般情况,ScrollView中每个Item的大小是一直的,使用ContentSizeFillter组件足够解决大部分问题;

如果每个Item大小不一致,问题就复杂起来,需要做滚动位置判断,我这里做了大小适应;

设计思路

1.将数据部分和滚动逻辑部分分离开,数据设计成泛型类;

2.在ScrollView组件上添加ScrollView脚本,控制Item的添加和删除,分为头部和尾部;

3.在每个Item上添加ScrollItem脚本,重写更新数据方法,同时监听自身是否为头部或者尾部;

4.如果为头部或者尾部,且超界通过委托调用ScrollView脚本中的添加或删除Item方法;

关键基类

1.ScrollData

负责整个列表的数据管理,分为总数据和现实数据两个链表,增删查改方法;泛型类方便复用;

这里使用LinkedList方便查找并返回头尾节点;

全部代码:

public class ScrollData<T>
{
   public List<T> allDatas;
   public LinkedList<T> curDatas;

public ScrollData()
   {
       allDatas = new List<T>();
       curDatas = new LinkedList<T>();
       //加载数据;
   }
   //获取头数据
   public T GetHeadData()
       if(allDatas.Count == 0)
           return default(T);
       if (curDatas.Count == 0)
       {
           T head = allDatas[0];
           curDatas.AddFirst(head);
           return head;
       }
       T t = curDatas.First.Value;
       int index = allDatas.IndexOf(t);
       if (index != 0)
           T head = allDatas[index - 1];
       return default(T);
   //移出头数据
   public bool RemoveHeadData()
       if (curDatas.Count == 0 || curDatas.Count == 1)
           return false;
       curDatas.RemoveFirst();
       return true;
   //获取尾部数据
   public T GetEndData()
       if (allDatas.Count == 0)
           T end = allDatas[0];
           curDatas.AddLast(end);
           return end;
       T t = curDatas.Last.Value;

if (index != allDatas.Count - 1)
           T end = allDatas[index + 1];

//移出尾部数据
   public bool RemoveEndData()
       curDatas.RemoveLast();
   //添加数据,通过数组
   public void AddData(T[] t)
       allDatas.AddRange(t);
   //添加数据,通过链表
   public void AddData(List<T> t)
       allDatas.AddRange(t.ToArray());
   //添加单条数据
   public void AddData(T t)
       allDatas.Insert(0,t);
       curDatas.AddFirst(t);
   //情况当前显示节点
   public void ClearCurData()
       curDatas.Clear();
   //获取当前显示链表的第一个数据在总数据中的下标
   public int GetFirstIndex()
       return allDatas.IndexOf(t);
}

2.ScrollView

关键字段:

scrollItemGo//每个Item的预制体
content//scrollRect下的Content
spacing//每个Item的间隔
isStart//是否第一次加载

方法:

GetChildItem;

1.获取一个Item的预制体,先从content的子物体中寻找active为false的物体,如果没有则根据scrollItemGo克隆一个;

2.创建新Item时,获取ScrollItem组件,赋值其中的参数(四个委托),并初始化;

OnAddHead;OnRemoveHead;OnAddEnd;OnRemoveEnd;

委托方法:

1.调用ScrollData中GetHeadData方法,获得头数据;

2.找到content中第一个节点;

3.调用GetChildItem方法获得item的实例;

4.SetAsFirstSibling,将实例设置为首节点,同时调用RefreshData,刷新数据;

5.根据item 的宽度做自适应(item大小相同,只选挂载ContentSizeFitter);

全部代码:

public class ScrollView : MonoBehaviour
{
   public GameObject scrollItemGo;
   private RectTransform content;

[SerializeField]
   private float spacing;
   private bool isStart = true;
   void Start()
   {
       content = this.GetComponent<ScrollRect>().content;
       spacing = 15;
       OnAddHead();
   }
   private GameObject GetChildItem()
       //查找是否有未回收的子节点
       for (int i = 0; i < content.childCount; ++i)
       {
           GameObject tempGo = content.GetChild(i).gameObject;
           if (!tempGo.activeSelf)
           {
               tempGo.SetActive(true);
               return tempGo;
           }
       }
       //无创建新的
       GameObject childItem = GameObject.Instantiate<GameObject>(scrollItemGo,content.transform);
       ScrollViewItem scrollItem = childItem.GetComponent<ScrollViewItem>();
       if (scrollItem == null)
           scrollItem = childItem.AddComponent<ScrollViewItem>();

scrollItem.onAddHead += OnAddHead;
       scrollItem.onRemoveHead += OnRemoveHead;
       scrollItem.onAddEnd += OnAddEnd;
       scrollItem.onRemoveEnd += OnRemoveEnd;
       scrollItem.Init();
       childItem.GetComponent<RectTransform>().anchorMin = new Vector2(0.5f, 1);
       childItem.GetComponent<RectTransform>().anchorMax = new Vector2(0.5f, 1);
       childItem.GetComponent<RectTransform>().pivot = new Vector2(0, 1);
       childItem.transform.localScale = Vector3.one;
       childItem.transform.localPosition = Vector3.zero;
       //-----设置宽高——加载数据
       return childItem;
   private void OnAddHead()
       Data data = this.GetComponent<Test>().scrollData.GetHeadData();
       if (data != null)
           Transform first = FindFirst();
           //----first 不为 数据头---在data中做了
           GameObject obj = GetChildItem();
           obj.GetComponent<ScrollViewItem>().RefreshData(data);
           obj.transform.SetAsFirstSibling();
           RectTransform objRect = obj.GetComponent<RectTransform>();
           float height = objRect.sizeDelta.y;

if (first != null)
               obj.transform.localPosition = first.localPosition + new Vector3(0, height + spacing, 0);
           if (isStart)
               content.sizeDelta += new Vector2(0, height + spacing);
               isStart = false;
   private void OnRemoveHead()
       var scrollData = this.GetComponent<Test>().scrollData;
       if (scrollData.RemoveHeadData())
           Transform tf = FindFirst();
           if (tf != null)
               tf.gameObject.SetActive(false);
   private void OnAddEnd()
       Data data = this.GetComponent<Test>().scrollData.GetEndData();
           Transform end = FindEnd();
           //----end 不为 数据尾在data中做了
           obj.transform.SetAsLastSibling();
           float height = end.GetComponent<RectTransform>().sizeDelta.y;
           if (end != null)
               obj.transform.localPosition = end.localPosition - new Vector3(0, height + spacing, 0);
           //是否增加content高度
           if (IsAddContentH(obj.transform))
               float h = obj.GetComponent<RectTransform>().sizeDelta.y;
               content.sizeDelta += new Vector2(0, h + spacing);
   private void OnRemoveEnd()
       if (scrollData.RemoveEndData())
           Transform tf = FindEnd();
   private Transform FindFirst()
           if (content.GetChild(i).gameObject.activeSelf)
               return content.GetChild(i);
       return null;
   private Transform FindEnd()
       for (int i = content.childCount - 1; i >= 0; --i)
   private bool IsAddContentH(Transform tf)
       Vector3[] rectC = new Vector3[4];
       Vector3[] contentC = new Vector3[4];
       tf.GetComponent<RectTransform>().GetWorldCorners(rectC);
       content.GetWorldCorners(contentC);
       if (rectC[0].y < contentC[0].y)
           return true;
       return false;
}

3.ScrollItem

关键字段:四个委托

public Action onAddHead;
public Action onRemoveHead;
public Action onAddEnd;
public Action onRemoveEnd;

关键方法:

OnRecyclingItem;

1.判断自身是否为头尾节点;

2.判断自身是否超界,超界需要隐藏自身;

3.判断自身与边界距离,是否添加节点;

解决Unity无限滚动复用列表的问题

关键API:

RectTransform.GetWorldCorners(Vector3[4])

获取UI对象四个顶点的世界坐标,下标对应的位置;

解决Unity无限滚动复用列表的问题

全部代码:

public class ScrollViewItem : MonoBehaviour
{
   private RectTransform viewRect;
   private RectTransform rect;
   [SerializeField]
   private float viewStart;
   [SerializeField]
   private float viewEnd;
   [SerializeField]
   private Vector3[] rectCorners;
   public Action onAddHead;
   public Action onRemoveHead;
   public Action onAddEnd;
   public Action onRemoveEnd;
   public Text nameT;
   public Text inputT;
   void Start()
   {
       Init();
   }
   public void Init()
   {
       viewRect = transform.parent.parent.GetComponent<RectTransform>();
       rect = this.GetComponent<RectTransform>();
       rectCorners = new Vector3[4];
       viewRect.GetWorldCorners(rectCorners);
       viewStart = rectCorners[1].y;
       viewEnd = rectCorners[0].y;
   }
   void Update()
   {
       OnRecyclingItem();
   }
   //超界变false;
   private void OnRecyclingItem()
   {
       rect = this.GetComponent<RectTransform>();
       rectCorners = new Vector3[4];
       rect.GetWorldCorners(rectCorners);
       if (IsFirst())
       {
           if (rectCorners[0].y > viewStart)
           {
               //隐藏头节点  
               if (onRemoveHead != null)
                   onRemoveHead();
           }
           if (rectCorners[1].y < viewStart)
           {
               //添加头节点-头节点不为数据起始点
               if (onAddHead != null)
                   onAddHead();
           }
       }
       if (IsLast())
       {
           if (rectCorners[0].y > viewEnd)
           {
               //添加尾节点-尾节点不为数据末尾
               if (onAddEnd != null)
                   onAddEnd();
           }
           if (rectCorners[1].y < viewEnd)
           {
               //隐藏尾节点
               if (onRemoveEnd != null)
                   onRemoveEnd();
           }
       }
   }
   private bool IsFirst()
   {
       for (int i = 0; i < transform.parent.childCount; ++i)
       {
           Transform tf = transform.parent.GetChild(i);
           if (tf.gameObject.activeSelf)
           {
               if (tf == this.transform)
               {
                   return true;
               }
               break;
           }
       }
       return false;
   }
   private bool IsLast()
   {
       for (int i = transform.parent.childCount-1; i >= 0 ; i--)
       {
           Transform tf = transform.parent.GetChild(i);
           if (tf.gameObject.activeSelf)
           {
               if (tf == this.transform)
               {
                   return true;
               }
               break;
           }
       }
       return false;
   }
   public bool IsInView()
   {
       rect = this.GetComponent<RectTransform>();
       rect.GetWorldCorners(rectCorners);
       if (rectCorners[1].y > viewEnd || rectCorners[0].y < viewStart)
           return false;
       return true;
   }
   public void RefreshData(Data da)
   {
       nameT.text = da.name;
       inputT.text = da.text;

Vector2 oldSize = rect.sizeDelta;
       rect.sizeDelta = new Vector2(oldSize.x, 200 + da.h);
   }
}

测试类

初始化数据,随机4中宽度的item;

void InitData()
{
   int[] hArr = new int[4];
   hArr[0] = 0;
   hArr[1] = 190;
   hArr[2] = 190 * 2;
   hArr[3] = 190 * 3;
   for (int i = 0; i < 30; ++i)
   {
       Data da = new Data();
       da.name = "小紫苏" + i.ToString();
       da.text = "000000" + i.ToString();
       int index = UnityEngine.Random.Range(0, 3);
       da.h = hArr[index];
       scrollData.allDatas.Add(da);
   }
}

添加三个按钮,及相应的响应方法;

1.添加20组数据

private void AddData()
{
   int[] hArr = new int[4];
   hArr[0] = 0;
   hArr[1] = 190;
   hArr[2] = 190 * 2;
   hArr[3] = 190 * 3;
   Data[] newData = new Data[20];
   for (int i = 0; i < 20; ++i)
   {
       Data da = new Data();
       da.name = "小紫苏" + i.ToString();
       da.text = "000000" + i.ToString();
       int index = UnityEngine.Random.Range(0, 3);
       da.h = hArr[index];
       newData[i] = da;
   }
   scrollDat

回到顶部或底部需要有过程,因此需要在update中运行,也可以用插值;

2.回到顶部

private void OnGoHead()
{
   if (isGoHead)
       isGoHead = false;
   else
       isGoHead = true;
}
private void OnGoLast()
{
   if (isGoLast)
       isGoLast = false;
   else
       isGoLast = true;
}

3.回到底部

private void GoHead()
{
   if (!isGoHead)
       return;

float curPos = scroll.verticalNormalizedPosition;
   if (curPos != 1)
   {
       curPos += 0.01f;
       if (curPos >= 1)
       {
           curPos = 1;
           isGoHead = false;
       }
       scroll.verticalNormalizedPosition = curPos;
   }
}
private void GoLast()
{
   if (!isGoLast)
       return;
   float curPos = scroll.verticalNormalizedPosition;
   if (curPos != 0)
   {
       curPos -= 0.01f;
       if (curPos <= 0)
       {
           curPos = 0;
           isGoLast = false;
       }
       scroll.verticalNormalizedPosition = curPos;
   }
}

坑点

1.ScrollView回滚设置延迟;

回滚判断是通过verticalNormalizedPosition的API,更改这个值后需要间隔一帧才会修改,因为可能导致判断两次;

解决方法,延迟调用1s&mdash;&mdash;Invoke;

2.锚点设置;

锚点的设置以及UI的自适应会直接影响项目回滚的方向和位置;

大部分位置出错都是因为锚点设置错误;

3.数据需要网络请求,自适应会失效;

网络数据一般都是异步,所以判断会做多次,因此数据上要求提前计算好item的宽度;

项目工程我上传到Gitee,可自行下载学习;https://gitee.com/small-perilla/scroll-view

以上是我对滚动复用组件的总结,如果有更好的意见,欢迎给作者评论留言;

来源:https://www.cnblogs.com/littleperilla/p/15348538.html

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com