编辑
2023-01-11
Unity游戏开发
00
请注意,本文编写于 264 天前,最后修改于 113 天前,其中某些信息可能已经过时。

目录

效果展示
实现思路
使用说明
完整代码
Lua版本
数据量小/固定时的简单复用
Reference

当邮件中有1000封邮件,商店列表中有1000个物体,如果直接实例化1000条数据显示则会大大增加 DrawCall,而大量不可见的数据被 Mask 组件排除在可视范围之外,但他们依然存在,这时就需要考虑通过一个无限滑动的 ScrollView 来优化渲染性能,下面这种复用方式提供了一个通用的思路来处理此类问题。

效果展示

实现思路

通过头下标和尾下标记录当前实例化数据的最大最小索引,之后用Content的锚点位置与当头下标的锚点位置进行比较判断滑动的方向以及是否超出滑动范围,如果正方向滑动超出范围则将第一个元素移动到最后一个,如果反方向滑动超出范围则将最后一个元素移动到第一个,这样场景中始终存在5个实例化的元素,依次改变元素的位置和显示即可。

使用说明

  • 此功能脚本是对 ScrollRect 的扩展,所以必须添加 UGUI 提供的基础 Scroll View
  • Content 上必须添加 GridLayoutGroup 组件,通过 GridLayoutGroup 组件设计布局,(我在 代码中对 startCorner、startAxis、childAlignment 和 constraintCount 进行了限制,不需要对其设置)
  • 不能添加 ContentSizeFitter 组件
  • 先调用 SetTotalCount 方法设置总的数据数量再调用 Init 方法进行初始化
  • 根据需求修改 SetShow 方法体
  • 只适用于单向单列滑动的情况,不能满足竖直和水平同时滑动的需求,因为大多数无限滑动列表的使用场景都是单向的
  • 根据实际情况配置固定 item 数量,比如cell宽高、滑动区域的宽高等等
c#
void Start() { // 先设置数量,再初始化 other.SetTotalCount(200); other.Init(); // TODO ... other.DestoryAll(); }

完整代码

将InfiniteScrollView脚本挂载到ScrollView上

c#
using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using System.Linq; /// <summary> /// 无限滑动列表 /// </summary> public class InfiniteScrollView : MonoBehaviour { private ScrollRect scrollRect;//滑动框组件 private RectTransform content;//滑动框的Content private GridLayoutGroup layout;//布局组件 [Header("滑动类型")] public ScrollType scrollType; [Header("固定的Item数量")] public int fixedCount; [Header("Item的预制体")] public GameObject itemPrefab; private int totalCount;//总的数据数量 private List<RectTransform> dataList = new List<RectTransform>();//数据实体列表 private int headIndex;//头下标 private int tailIndex;//尾下标 private Vector2 firstItemAnchoredPos;//第一个Item的锚点坐标 #region Init /// <summary> /// 实例化Item /// </summary> private void InitItem() { for (int i = 0; i < fixedCount; i++) { GameObject tempItem = Instantiate(itemPrefab, content); dataList.Add(tempItem.GetComponent<RectTransform>()); SetShow(tempItem.GetComponent<RectTransform>(), i); } } /// <summary> /// 设置Content大小 /// </summary> private void SetContentSize() { content.sizeDelta = new Vector2 ( layout.padding.left + layout.padding.right + totalCount * (layout.cellSize.x + layout.spacing.x) - layout.spacing.x - content.rect.width, layout.padding.top + layout.padding.bottom + totalCount * (layout.cellSize.y + layout.spacing.y) - layout.spacing.y ); } /// <summary> /// 设置布局 /// </summary> private void SetLayout() { layout.startCorner = GridLayoutGroup.Corner.UpperLeft; layout.startAxis = GridLayoutGroup.Axis.Horizontal; layout.childAlignment = TextAnchor.UpperLeft; layout.constraintCount = 1; if (scrollType == ScrollType.Horizontal) { scrollRect.horizontal = true; scrollRect.vertical = false; layout.constraint = GridLayoutGroup.Constraint.FixedRowCount; } else if (scrollType == ScrollType.Vertical) { scrollRect.horizontal = false; scrollRect.vertical = true; layout.constraint = GridLayoutGroup.Constraint.FixedColumnCount; } } /// <summary> /// 得到第一个数据的锚点位置 /// </summary> private void GetFirstItemAnchoredPos() { firstItemAnchoredPos = new Vector2 ( layout.padding.left + layout.cellSize.x / 2, -layout.padding.top - layout.cellSize.y / 2 ); } #endregion #region Main /// <summary> /// 滑动中 /// </summary> private void OnScroll(Vector2 v) { if (dataList.Count == 0) { Debug.LogWarning("先调用SetTotalCount方法设置数据总数量再调用Init方法进行初始化"); return; } if (scrollType == ScrollType.Vertical) { //向上滑 while (content.anchoredPosition.y >= layout.padding.top + (headIndex + 1) * (layout.cellSize.y + layout.spacing.y) && tailIndex != totalCount - 1) { //将数据列表中的第一个元素移动到最后一个 RectTransform item = dataList[0]; dataList.Remove(item); dataList.Add(item); //设置位置 SetPos(item, tailIndex + 1); //设置显示 SetShow(item, tailIndex + 1); headIndex++; tailIndex++; } //向下滑 while (content.anchoredPosition.y <= layout.padding.top + headIndex * (layout.cellSize.y + layout.spacing.y) && headIndex != 0) { //将数据列表中的最后一个元素移动到第一个 RectTransform item = dataList.Last(); dataList.Remove(item); dataList.Insert(0, item); //设置位置 SetPos(item, headIndex - 1); //设置显示 SetShow(item, headIndex - 1); headIndex--; tailIndex--; } } else if (scrollType == ScrollType.Horizontal) { //向左滑 while (content.anchoredPosition.x <= -layout.padding.left - (headIndex + 1) * (layout.cellSize.x + layout.spacing.x) && tailIndex != totalCount - 1) { //将数据列表中的第一个元素移动到最后一个 RectTransform item = dataList[0]; dataList.Remove(item); dataList.Add(item); //设置位置 SetPos(item, tailIndex + 1); //设置显示 SetShow(item, tailIndex + 1); headIndex++; tailIndex++; } //向右滑 while (content.anchoredPosition.x >= -layout.padding.left - headIndex * (layout.cellSize.x + layout.spacing.x) && headIndex != 0) { //将数据列表中的最后一个元素移动到第一个 RectTransform item = dataList.Last(); dataList.Remove(item); dataList.Insert(0, item); //设置位置 SetPos(item, headIndex - 1); //设置显示 SetShow(item, headIndex - 1); headIndex--; tailIndex--; } } } #endregion #region Tool /// <summary> /// 设置位置 /// </summary> private void SetPos(RectTransform trans, int index) { if (scrollType == ScrollType.Horizontal) { trans.anchoredPosition = new Vector2 ( index == 0 ? layout.padding.left + firstItemAnchoredPos.x : layout.padding.left + firstItemAnchoredPos.x + index * (layout.cellSize.x + layout.spacing.x), firstItemAnchoredPos.y ); } else if (scrollType == ScrollType.Vertical) { trans.anchoredPosition = new Vector2 ( firstItemAnchoredPos.x, index == 0 ? -layout.padding.top + firstItemAnchoredPos.y : -layout.padding.top + firstItemAnchoredPos.y - index * (layout.cellSize.y + layout.spacing.y) ); } } #endregion #region 外部调用 /// <summary> /// 初始化 /// </summary> public void Init() { scrollRect = GetComponent<ScrollRect>(); content = scrollRect.content; layout = content.GetComponent<GridLayoutGroup>(); scrollRect.onValueChanged.AddListener((Vector2 v) => OnScroll(v)); //设置布局 SetLayout(); //设置头下标和尾下标 headIndex = 0; tailIndex = fixedCount - 1; //设置Content大小 SetContentSize(); //实例化Item InitItem(); //得到第一个Item的锚点位置 GetFirstItemAnchoredPos(); } /// <summary> /// 设置显示 /// </summary> public void SetShow(RectTransform trans, int index) { //=====根据需求进行编写 trans.GetComponentInChildren<Text>().text = index.ToString(); trans.name = index.ToString(); } /// <summary> /// 设置总的数据数量 /// </summary> public void SetTotalCount(int count) { totalCount = count; } /// <summary> /// 销毁所有的元素 /// </summary> public void DestoryAll() { for (int i = dataList.Count - 1; i >= 0; i--) { DestroyImmediate(dataList[i].gameObject); } dataList.Clear(); } #endregion } /// <summary> /// 滑动类型 /// </summary> public enum ScrollType { Horizontal,//竖直滑动 Vertical,//水平滑动 }

Lua版本

由于实际使用上还是用 Lua,于是根据原作者提供的思路和 c# 代码,翻译成了 Lua脚本:

lua
local class = require("middleclass") local MyScrollRectView = class("MyScrollRectView") local ScrollTypeEnum = { HORIZONTAL = 0, VERTICAL = 1 } ---@param parentTransform CS.UnityEngine.RectTransform 父布局 function MyScrollRectView:initialize(parentTransform) --- TODO 包含 ScrollView 的根布局(结构参考Gif) ---@type CS.UnityEngine.GameObject self.uiRoot = nil self:_initField() self.uiRoot.transform:SetParent(parentTransform.transform, false) end function MyScrollRectView:_initField() --- TODO 要展示的 ItemPrefab ---@type CS.UnityEngine.GameObject self.itemPrefab = nil ---@type CS.UnityEngine.UI.ScrollRect 滑动框组件 self.scrollRect = self.uiRoot:GetComponent(typeof(CS.UnityEngine.UI.ScrollRect)) ---@type CS.UnityEngine.RectTransform 滑动框的Content self.content = self.scrollRect.content self.content.anchorMax = CS.UnityEngineVector2(0.5, 1) self.content.anchorMin = CS.UnityEngineVector2(0.5, 1) self.content.pivot = CS.UnityEngineVector2(0, 1) ---@type CS.UnityEngine.UI.GridLayoutGroup 布局组件 self.layout = self.uiRoot.transform:Find("Viewport/Content").gameObject:GetComponent(typeof(CS.UnityEngine.UI.GridLayoutGroup)) ---@type number 滑动方式 0表示水平 1表示竖直 self.scrollType = ScrollTypeEnum.VERTICAL ---@type number 固定的 Item 数量 self.fixedCount = 0 -- 总的数据数量 self.totalCount = 0 -- 数据实体列表 self.dataList = {} -- 头下标 self.headIndex = 0 -- 尾下标 self.tailIndex = 0 -- 第一个Item的锚点坐标 ---@type CS.UnityEngine.Vector2 self.firstItemAnchoredPos = CS.UnityEngine.Vector2.zero end function MyScrollRectView:_InitItem() ---@type CS.UnityEngine.GameObject local tempItem for i = 0, self.fixedCount - 1 do tempItem = CS.UnityEngine.GameObject.Instantiate(self.itemPrefab, self.content) table.insert(self.dataList, tempItem:GetComponent(typeof(CS.UnityEngine.RectTransform))) self:_SetShow(tempItem:GetComponent(typeof(CS.UnityEngine.RectTransform)), i) end end function MyScrollRectView:_SetLayout() self.layout.startCorner = CS.UnityEngine.UI.GridLayoutGroup.Corner.UpperLeft self.layout.startAxis = CS.UnityEngine.UI.GridLayoutGroup.Axis.Horizontal self.layout.childAlignment = CS.UnityEngine.TextAnchor.UpperLeft self.layout.constraintCount = 1 -- ScrollType.Horizontal if self.scrollType == ScrollTypeEnum.HORIZONTAL then self.scrollRect.horizontal = true self.scrollRect.vertical = false self.layout.constraint = CS.UnityEngine.UI.GridLayoutGroup.Constraint.FixedRowCount elseif self.scrollType == ScrollTypeEnum.VERTICAL then self.scrollRect.horizontal = false self.scrollRect.vertical = true self.layout.constraint = CS.UnityEngine.UI.GridLayoutGroup.Constraint.FixedColumnCount end end function MyScrollRectView:_OnScroll(v) if #self.dataList == 0 then print("先调用 SetDataLength 方法设置数据总数量,再调用 Init 方法进行初始化") return end if self.scrollType == ScrollTypeEnum.VERTICAL then -- Vertical -- 向上滑 while (self.content.anchoredPosition.y >= (self.layout.padding.top + (self.headIndex + 1) * (self.layout.cellSize.y + self.layout.spacing.y))) and (self.tailIndex ~= self.totalCount - 1) do -- 将数据列表中的第一个元素移动到最后一个 local item = self.dataList[1] table.remove(self.dataList, 1) table.insert(self.dataList, item) -- 设置位置 self:_SetPos(item, self.tailIndex + 1) --设置显示 self:_SetShow(item, self.tailIndex + 1) self.headIndex = self.headIndex + 1 self.tailIndex = self.tailIndex + 1 end -- 向下滑 while (self.content.anchoredPosition.y <= self.layout.padding.top + self.headIndex * (self.layout.cellSize.y + self.layout.spacing.y)) and self.headIndex ~= 0 do -- 将数据列表中的最后一个元素移动到第一个 local item = self.dataList[#self.dataList] table.remove(self.dataList, #self.dataList) table.insert(self.dataList, 1, item) -- 设置位置 self:_SetPos(item, self.headIndex - 1) --设置显示 self:_SetShow(item, self.headIndex - 1) self.headIndex = self.headIndex - 1 self.tailIndex = self.tailIndex - 1 end elseif self.scrollType == ScrollTypeEnum.HORIZONTAL then -- 水平滑动 -- 向左滑 while (self.content.anchoredPosition.x <= -self.layout.padding.left - (self.headIndex + 1) * (self.layout.cellSize.x + self.layout.spacing.x) and self.tailIndex ~= self.totalCount - 1) do --将数据列表中的第一个元素移动到最后一个 local item = self.dataList[1] table.remove(self.dataList, 1) table.insert(self.dataList, item) --设置位置 self:_SetPos(item, self.tailIndex + 1) --设置显示 self:_SetShow(item, self.tailIndex + 1) self.headIndex = self.headIndex + 1 self.tailIndex = self.tailIndex + 1 end --向右滑 while (self.content.anchoredPosition.x >= -self.layout.padding.left - self.headIndex * (self.layout.cellSize.x + self.layout.spacing.x) and self.headIndex ~= 0) do --将数据列表中的第一个元素移动到最后一个 local item = self.dataList[1] table.remove(self.dataList, 1) table.insert(self.dataList, item) --设置位置 self:_SetPos(item, self.headIndex - 1) --设置显示 self:_SetShow(item, self.headIndex - 1) self.headIndex = self.headIndex - 1 self.tailIndex = self.tailIndex - 1 end end end ---@param item CS.UnityEngine.GameObject function MyScrollRectView:_SetPos(item, index) local x = 0 local y = 0 ---@type CS.UnityEngine.RectTransform local rt = item.transform if self.scrollType == ScrollTypeEnum.HORIZONTAL then x = index == 0 and (self.layout.padding.left + self.firstItemAnchoredPos.x) or (self.layout.padding.left + self.firstItemAnchoredPos.x + index * (self.layout.cellSize.x + self.layout.spacing.x)) y = self.firstItemAnchoredPos.y rt.anchoredPosition = CS.UnityEngine.Vector2(x, y) elseif self.scrollType == ScrollTypeEnum.VERTICAL then x = self.firstItemAnchoredPos.x y = index == 0 and (-self.layout.padding.top + self.firstItemAnchoredPos.y) or (-self.layout.padding.top + self.firstItemAnchoredPos.y - index * (self.layout.cellSize.y + self.layout.spacing.y)) rt.anchoredPosition = CS.UnityEngine.Vector2(x, y) end end function MyScrollRectView:_SetContentSize() if self.scrollType == ScrollTypeEnum.VERTICAL then local x = self.layout.padding.left + self.layout.padding.right + self.layout.cellSize.x + self.layout.spacing.x local y = self.layout.padding.top + self.layout.padding.bottom + self.totalCount * (self.layout.cellSize.y + self.layout.spacing.y) - self.layout.spacing.y self.content.sizeDelta = CS.UnityEngine.Vector2(x, y) elseif self.scrollType == ScrollTypeEnum.HORIZONTAL then local x = self.layout.padding.left + self.layout.padding.right + self.layout.cellSize.x + self.layout.spacing.x local y = self.layout.padding.top + self.layout.padding.bottom + self.totalCount * (self.layout.cellSize.y + self.layout.spacing.y) - self.layout.spacing.y self.content.sizeDelta = CS.UnityEngine.Vector2(x, y) end end function MyScrollRectView:_GetFirstItemAnchoredPos() self.firstItemAnchoredPos = CS.UnityEngine.Vector2(self.layout.padding.left + self.layout.cellSize.x / 2, -self.layout.padding.top - self.layout.cellSize.y / 2) end --- #外部调用 function MyScrollRectView:SetDataLength(length) --- 设置总数据量 self.totalCount = length self:Init() end function MyScrollRectView:Init() if self.totalCount <= 0 then return elseif self.totalCount < 10 then self.fixedCount = self.totalCount else self.fixedCount = 10 end if self.scrollRect["onValueChanged"] ~= nil then self.scrollRect["onValueChanged"]:RemoveAllListeners() end self.commonService:AddEventListener(self.scrollRect, "onValueChanged", function(vector2) self:_OnScroll(vector2) end) --设置布局 self:_SetLayout() --设置头下标和尾下标 self.headIndex = 0 self.tailIndex = self.fixedCount - 1 -- 设置Content大小 self:_SetContentSize() --实例化Item self:_InitItem() --得到第一个Item的锚点位置 self:_GetFirstItemAnchoredPos() --- Content位置复位 self.content.anchoredPosition = CS.UnityEngine.Vector2(self.content.anchoredPosition.x, 0) end function MyScrollRectView:DestoryAll() -- print("执行DestoryAll, #self.dataList = ", #self.dataList) for i = #self.dataList, 0, -1 do if self.dataList[i] then -- print("销毁: ", self.dataList[i].gameObject) CS.UnityEngine.GameObject.Destroy(self.dataList[i].gameObject) end end self.dataList = {} end --- #外部调用 --- --- 关闭的时销毁 GameObject function MyScrollRectView:Close() self:DestoryAll() end --- 属于业务相关方法 ---@param i number index ---@param trans CS.UnityEngine.RectTransform function MyScrollRectView:_SetShow(trans, i) --- TODO Item如何展示 example code trans.name = i .. "" ---@type CS.UnityEngine.UI.RawImage local raw = trans.gameObject:GetComponent("RawImage") raw.color = CS.UnityEngine.Color(1, 1, 1, 0) end return MyScrollRectView

所以核心思想就是根据划进划出把头部的 GameObject 添加到尾部或者把尾部 GameObject 添加到头部!

数据量小/固定时的简单复用

如果数据量很小而且Item数量比较固定的时候直接简单复用即可:

lua
---列表item复用 items = {} ---show function for i = 1, #dataList do ---@type CS.UnityEngine.GameObject local itemRootUI = nil if items[i] then itemRootUI = items[i] else itemRootUI = GameObject.Instantiate(itemPrefab, contentRectTransform, false) table.insert(items, itemRootUI) end itemRootUI:SetActive(true) --- TODO show... end

Reference

《Unity中实现无限滑动的ScrollView》

本文作者:Tim

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!