Unity

您所在的位置:网站首页 unity滑动列表在顶处 Unity

Unity

2024-06-19 19:16| 来源: 网络整理| 查看: 265

无限滚动复用列表 一、Demo展示

12

二、前言

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

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

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

三、设计思路

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

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

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

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

四、关键基类 1.ScrollData

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

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

全部代码:

public class ScrollData { public List allDatas; public LinkedList curDatas; public ScrollData() { allDatas = new List(); curDatas = new LinkedList(); //加载数据; } //获取头数据 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]; curDatas.AddFirst(head); return head; } 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) return default(T); if (curDatas.Count == 0) { T end = allDatas[0]; curDatas.AddLast(end); return end; } T t = curDatas.Last.Value; int index = allDatas.IndexOf(t); if (index != allDatas.Count - 1) { T end = allDatas[index + 1]; curDatas.AddLast(end); return end; } return default(T); } //移出尾部数据 public bool RemoveEndData() { if (curDatas.Count == 0 || curDatas.Count == 1) return false; curDatas.RemoveLast(); return true; } //添加数据,通过数组 public void AddData(T[] t) { allDatas.AddRange(t); } //添加数据,通过链表 public void AddData(List 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() { T t = curDatas.First.Value; 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().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(scrollItemGo,content.transform); ScrollViewItem scrollItem = childItem.GetComponent(); if (scrollItem == null) scrollItem = childItem.AddComponent(); scrollItem.onAddHead += OnAddHead; scrollItem.onRemoveHead += OnRemoveHead; scrollItem.onAddEnd += OnAddEnd; scrollItem.onRemoveEnd += OnRemoveEnd; scrollItem.Init(); childItem.GetComponent().anchorMin = new Vector2(0.5f, 1); childItem.GetComponent().anchorMax = new Vector2(0.5f, 1); childItem.GetComponent().pivot = new Vector2(0, 1); childItem.transform.localScale = Vector3.one; childItem.transform.localPosition = Vector3.zero; //-----设置宽高——加载数据 return childItem; } private void OnAddHead() { Data data = this.GetComponent().scrollData.GetHeadData(); if (data != null) { Transform first = FindFirst(); //----first 不为 数据头---在data中做了 GameObject obj = GetChildItem(); obj.GetComponent().RefreshData(data); obj.transform.SetAsFirstSibling(); RectTransform objRect = obj.GetComponent(); 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().scrollData; if (scrollData.RemoveHeadData()) { Transform tf = FindFirst(); if (tf != null) tf.gameObject.SetActive(false); } } private void OnAddEnd() { Data data = this.GetComponent().scrollData.GetEndData(); if (data != null) { Transform end = FindEnd(); //----end 不为 数据尾在data中做了 GameObject obj = GetChildItem(); obj.transform.SetAsLastSibling(); obj.GetComponent().RefreshData(data); float height = end.GetComponent().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().sizeDelta.y; content.sizeDelta += new Vector2(0, h + spacing); } } } private void OnRemoveEnd() { var scrollData = this.GetComponent().scrollData; if (scrollData.RemoveEndData()) { Transform tf = FindEnd(); if (tf != null) tf.gameObject.SetActive(false); } } private Transform FindFirst() { for (int i = 0; i < content.childCount; ++i) { if (content.GetChild(i).gameObject.activeSelf) { return content.GetChild(i); } } return null; } private Transform FindEnd() { for (int i = content.childCount - 1; i >= 0; --i) { if (content.GetChild(i).gameObject.activeSelf) { return content.GetChild(i); } } return null; } private bool IsAddContentH(Transform tf) { Vector3[] rectC = new Vector3[4]; Vector3[] contentC = new Vector3[4]; tf.GetComponent().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.判断自身与边界距离,是否添加节点;

image-20210928155648075

关键API:

RectTransform.GetWorldCorners(Vector3[4])

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

image-20210928155948328

全部代码:

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(); rect = this.GetComponent(); 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(); 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(); 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


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3