Unity-ScrollView无限循环
在Unity引擎中ScrollView组件是一个使用率比较高的组件,该组件能上下或者左右拖动的UI列表,背包、展示多个按钮等情况的时候会用到,在做排行榜类似界面时,item非常多,可能有几百个,一次创建这么多GameObject是非常卡的。为此,使用只创建可视区一共显示的个数,加上后置准备个数。
ItemArray
我们实现无限循环的核心思想是,当滚动框向左移动时,就判断左边的第一个Item距离显示框左边框的距离,是否大于Item的宽度,如果大于则把第一个Item放到最后的位置,当滚动框向右移动时就判断右边的Item距离右边框的距离情况,这样我们就可以在显示区域一直看到有Item的规则显示
按照上面的逻辑分析重写ScrollRect使其支持item回收模式。如果勾选isRecyItem则在运行时在Awake时自动执行回收模式的初始化,否则,需要手动调用,OnInitItemCompleteAct来注册Item的生成回调,用来执行指定item的目标操作。
注: 重写之后不能支持横向和纵向同时滑动
开袋即食的代码:
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class MyScrollView : ScrollRect
{
[HideInInspector]//是否开启循环Item
public bool isRecyItem = true;
[HideInInspector]//是否为横向滑动
public bool isHorizontal = false;
[HideInInspector]//Item预制体
public GameObject prefab;
[HideInInspector]//Item数量
public int itemCount=10;
[HideInInspector]//行
public int hang=1;
[HideInInspector]//列
public int lie=1;
[HideInInspector]//Item大小
public Vector2 itemSize;
[HideInInspector]//item间隔
public Vector2 itemSpace;
[HideInInspector]//补充组数
public int fillCount=1;
private static MyScrollView instance;
private RecyclerScrollViewBase recycler;
private bool startUpdate = false;
private Action OnUpdateAct;
protected override void Awake()
{
base.Awake();
instance = this;
if (isRecyItem&&Application.isPlaying)
{
InitRecycler(null);
}
}
/// <summary>
/// 初始化回收器
/// </summary>
public RecyclerScrollViewBase InitRecycler(Action<ItemMoudle> initItemAct)
{
if (recycler!=null)
{
recycler.Dispose();
recycler = null;
}
if (isHorizontal)
{
recycler=new HorizontalScrollView();
}
else
{
recycler=new VerticalScrollView();
}
recycler.OnInit(initItemAct);
isRecyItem = true;
return recycler;
}
public RecyclerScrollViewBase OnInitItemCompleteAct(Action<ItemMoudle> initItemAct)
{
recycler?.OnInitItemCompleteAct(initItemAct);
return recycler;
}
private void Update()
{
OnUpdateAct?.Invoke();
}
protected override void OnDestroy()
{
base.OnDestroy();
recycler?.Dispose();
}
/// <summary>
/// 回收模式的ScrollView基类
/// </summary>
public abstract class RecyclerScrollViewBase
{
/// <summary>
/// Grid上一帧所在的位置,用于校验滑动方向
/// </summary>
private Vector2 gridLastPos;
public enum MoveDir
{
Idle,
UP,
DOWN,
LEFT,
RIGHT
}
protected RectTransform grid;
protected RectTransform mask;
protected float maskWidth;
protected float maskHeight;
protected float gridWidth;
protected float gridHeight;
protected int viewCount;
protected float spaceX;
protected float spaceY;
protected float itemWidth;
protected float itemHeight;
protected int fillCount;
protected int itemCount;
protected GameObject prefab;
protected List<ItemMoudle> itemList;
protected int itemListCount;
/// <summary>
/// 滑动方向
/// </summary>
protected MoveDir moveDir;
protected Action<ItemMoudle> OnInitItemAct;
/// <summary>
/// 横向滑动时使用 表示一列有几个元素
/// </summary>
protected int hang;
/// <summary>
/// 纵向滑动时使用表示一行有几个元素
/// </summary>
protected int lie;
public RecyclerScrollViewBase()
{
grid = instance.content;
mask = instance.viewport;
maskWidth = mask.rect.width;
maskHeight = mask.rect.height;
spaceX = instance.itemSpace.x;
spaceY = instance.itemSpace.y;
itemWidth = instance.itemSize.x;
itemHeight = instance.itemSize.y;
itemCount = instance.itemCount;
prefab = instance.prefab;
hang = instance.hang;
lie = instance.lie;
fillCount = instance.fillCount;
InitGridSize();
viewCount=InitViewCount();
SetAnchors(grid);
grid.sizeDelta = InitGridSize();
instance.StopMovement();
grid.anchoredPosition=Vector2.zero;
gridWidth = grid.sizeDelta.x;
gridHeight = grid.sizeDelta.y;
}
/// <summary>
/// 初始化
/// </summary>
public void OnInit(Action<ItemMoudle> initItemAct)
{
OnInitItemAct = initItemAct;
itemList=new List<ItemMoudle>();
ClearGrid();
for (int i = 0; i < viewCount+fillCount; i++)
{
RectTransform trans=Instantiate(prefab, grid).GetComponent<RectTransform>();
trans.sizeDelta=new Vector2(itemWidth,itemHeight);
SetAnchors(trans);
ItemMoudle itemMoudle= new ItemMoudle(i,trans);
OnInitItemAct?.Invoke(itemMoudle);
itemList.Add(itemMoudle);
}
itemListCount = itemList.Count;
instance.OnUpdateAct = OnUpdate;
}
public void OnInitItemCompleteAct(Action<ItemMoudle> initItemAct)
{
OnInitItemAct = initItemAct;
if (itemList==null||itemList.Count==0)
{
return;
}
for (int i = 0; i < itemList.Count; i++)
{
ItemMoudle item = itemList[i];
OnInitItemAct?.Invoke(item);
}
}
/// <summary>
/// Update
/// </summary>
private void OnUpdate()
{
Vector2 gridCurPos = grid.anchoredPosition;
Vector2 dir = gridCurPos - gridLastPos;
if (dir.x>0)
{
moveDir = MoveDir.RIGHT;
}else if (dir.x<0)
{
moveDir = MoveDir.LEFT;
}else if (dir.y>0)
{
moveDir = MoveDir.UP;
}else if (dir.y<0)
{
moveDir = MoveDir.DOWN;
}else
{
moveDir = MoveDir.Idle;
}
gridLastPos = gridCurPos;
if (moveDir!=MoveDir.Idle)
{
OnMoving(moveDir);
}
}
/// <summary>
/// 设置锚点
/// </summary>
/// <param name="rect"></param>
protected abstract void SetAnchors(RectTransform rect);
/// <summary>
/// ScrollView正在滑动
/// </summary>
/// <param name="moveDir"></param>
protected abstract void OnMoving(MoveDir moveDir);
/// <summary>
/// 初始化Grid大小
/// </summary>
/// <returns></returns>
protected abstract Vector2 InitGridSize();
/// <summary>
/// 初始化可视区域Item的数量
/// </summary>
/// <returns></returns>
protected abstract int InitViewCount();
/// <summary>
/// 清空Grid下所有的子物体
/// </summary>
protected void ClearGrid()
{
if (grid==null||grid.childCount==0)
{
return;
}
for (int i = 0; i <grid.childCount; i++)
{
Destroy(grid.GetChild(i).gameObject);
}
}
public void Dispose()
{
instance.OnUpdateAct = null;
OnInitItemAct = null;
grid = null;
mask = null;
prefab = null;
if (itemList!=null&&itemList.Count>0)
{
for (int i = itemList.Count-1; i >=0; i--)
{
itemList[i].Dispose();
itemList[i] = null;
}
itemList.Clear();
}
}
}
/// <summary>
/// 横向ScrollView
/// </summary>
public class HorizontalScrollView:RecyclerScrollViewBase
{
public HorizontalScrollView():base()
{
fillCount = fillCount * hang;
}
protected override Vector2 InitGridSize()
{
float height = grid.sizeDelta.y;
int grid_lie = itemCount % hang == 0 ? itemCount / hang : itemCount / hang + 1;
float width = itemWidth*grid_lie+spaceX*(grid_lie-1);
return new Vector2(width,height);
}
protected override void SetAnchors(RectTransform rect)
{
rect.anchorMin=new Vector2(0, 0.5f);
rect.anchorMax=new Vector2(0, 0.5f);
rect.pivot= new Vector2(0,0.5f);
}
protected override void OnMoving(MoveDir moveDir)
{
if (moveDir==MoveDir.LEFT)
{
if (itemList[itemListCount-1].index==itemCount-1)
{
return;
}
MoveToLeft();
}else if (moveDir==MoveDir.RIGHT)
{
if (itemList[0].index==0)
{
return;
}
MoveToRight();
}
}
/// <summary>
/// Item 距离左侧的距离
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
protected float ItemDisLeft(ItemMoudle item)
{
float itemX = item.position.x;
float gridX = -grid.anchoredPosition.x;
return gridX - itemX - itemWidth;
}
/// <summary>
/// Item 距离右侧的距离
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
protected float ItemDisRight(ItemMoudle item)
{
float itemX = item.position.x;
float gridX = -grid.anchoredPosition.x;
return itemX-gridX-maskWidth;
}
private void MoveToLeft()
{
ItemMoudle firstItem =itemList[0];
float itemDisViewportX = ItemDisLeft(firstItem);
while (itemDisViewportX>=0)
{
ItemMoudle lastItem = itemList[itemListCount-1];
bool target_isIntactGroup = firstItem.isIntactGroup;
for (int i = 0; i < hang; i++)//每次移动一排
{
int targetIndex = lastItem.index + 1;
if (targetIndex>=itemCount)
{
return;
}
firstItem.ChangeIndex(targetIndex);
OnInitItemAct?.Invoke(firstItem);
itemList.RemoveAt(0);
itemList.Insert(itemListCount-1,firstItem);
lastItem=itemList[itemListCount-1];
firstItem =itemList[0];
if (!target_isIntactGroup&&firstItem.isIntactGroup)
{
break;
}
}
itemDisViewportX = ItemDisLeft(firstItem);
}
}
private void MoveToRight()
{
ItemMoudle lastItem = itemList[itemListCount-1];
float itemDisViewportX = ItemDisRight(lastItem);
while (itemDisViewportX>=0)
{
bool target_isIntactGroup = lastItem.isIntactGroup;
for (int i = 0; i < hang; i++)
{
int targetIndex = itemList[0].index - 1;
if (targetIndex<0)
{
return;
}
lastItem.ChangeIndex(targetIndex);
OnInitItemAct?.Invoke(lastItem);
itemList.RemoveAt(itemListCount-1);
itemList.Insert(0,lastItem);
lastItem = itemList[itemListCount-1];
if (!target_isIntactGroup&&lastItem.isIntactGroup)
{
break;
}
}
itemDisViewportX = ItemDisRight(lastItem);
}
}
protected override int InitViewCount()
{
return hang*(int)Math.Ceiling((maskWidth + spaceX) / (itemWidth + spaceX));
}
}
/// <summary>
/// 纵向滑动的ScrollView
/// </summary>
public class VerticalScrollView:RecyclerScrollViewBase
{
public VerticalScrollView():base()
{
fillCount = fillCount * lie;
}
protected override Vector2 InitGridSize()
{
int grid_hang = itemCount % lie == 0 ? itemCount / lie : itemCount / lie + 1;
float height = itemHeight*grid_hang+spaceY*(grid_hang-1);
float width = grid.sizeDelta.x;
return new Vector2(width,height);
}
protected override void SetAnchors(RectTransform rect)
{
rect.anchorMin=new Vector2(0.5f, 1);
rect.anchorMax=new Vector2(0.5f, 1);
rect.pivot= new Vector2(0.5f,1);
}
protected override int InitViewCount()
{
return lie*(int)Math.Ceiling((maskHeight+ spaceY) / (itemHeight + spaceY));
}
protected override void OnMoving(MoveDir moveDir)
{
if (moveDir==MoveDir.UP)
{
if (itemList[itemListCount-1].index==itemCount-1)
{
return;
}
MoveToUp();
}else if (moveDir==MoveDir.DOWN)
{
if (itemList[0].index==0)
{
return;
}
MoveToDown();
}
}
/// <summary>
/// Item 距离上边框的距离
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
protected float ItemDisTop(ItemMoudle item)
{
float itemY = -item.position.y;
return grid.anchoredPosition.y - itemY - itemHeight;
}
/// <summary>
/// item 距离下边框的位置
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
protected float ItemDisBottom(ItemMoudle item)
{
float itemY = -item.position.y;
float contentY = grid.anchoredPosition.y;
return itemY-contentY-maskHeight;
}
/// <summary>
/// 向上移动
/// </summary>
private void MoveToUp()
{
ItemMoudle firstItem =itemList[0];
float itemDisViewportY = ItemDisTop(firstItem);
while (itemDisViewportY>=0)
{
bool target_isIntactGroup = firstItem.isIntactGroup;//当前检查的item是不是属于一个完整组
ItemMoudle lastItem = itemList[itemListCount-1];
for (int i = 0; i < lie; i++)
{
int targetIndex = lastItem.index + 1;
if (targetIndex>=itemCount)
{
return;
}
firstItem.ChangeIndex(targetIndex);
OnInitItemAct?.Invoke(firstItem);
itemList.RemoveAt(0);
itemList.Insert(itemListCount-1,firstItem);
lastItem = itemList[itemListCount-1];
firstItem =itemList[0];
if (!target_isIntactGroup&&firstItem.isIntactGroup)
{
break;
}
}
itemDisViewportY = ItemDisTop(firstItem);
}
}
/// <summary>
/// 向下移动
/// </summary>
private void MoveToDown()
{
ItemMoudle lastItem = itemList[itemListCount-1];
float itemDisViewportY = ItemDisBottom(lastItem);
while (itemDisViewportY>=0)
{
bool target_isIntactGroup = lastItem.isIntactGroup;
for (int i = 0; i < lie; i++)
{
int targetIndex = itemList[0].index - 1;
if (targetIndex<0)
{
return;
}
lastItem.ChangeIndex(targetIndex);
OnInitItemAct?.Invoke(lastItem);
itemList.RemoveAt(itemListCount-1);
itemList.Insert(0,lastItem);
lastItem = itemList[itemListCount-1];
if (!target_isIntactGroup&&lastItem.isIntactGroup)
{
break;
}
}
itemDisViewportY =ItemDisBottom(lastItem);
}
}
}
/// <summary>
/// Item模型
/// </summary>
public class ItemMoudle
{
private float itemWidth;
private float itemHeight;
private float spaceX;
private float spaceY;
private bool isHorizontal;
private int hang;
private int lie;
private int m_index;
private RectTransform m_transform;
private bool m_isIntactGroup;
/// <summary>
/// 纵向滑动时每一列的x坐标数组
/// </summary>
private static float[] xPosArr;
/// <summary>
/// 横向滑动时每一排的y坐标数组
/// </summary>
private static float[] yPosArr;
/// <summary>
/// 坐标数组是不是已经初始化
/// </summary>
private static bool initPosArrEnd = false;
/// <summary>
/// 卡牌索引
/// </summary>
public int index
{
get => m_index;
private set => m_index = value;
}
/// <summary>
/// 卡牌坐标
/// </summary>
public Vector2 position;
public RectTransform transform
{
private set => m_transform = value;
get => m_transform;
}
/// <summary>
/// 是不是一个完整组
/// </summary>
public bool isIntactGroup
{
get => m_isIntactGroup;
private set=> m_isIntactGroup= value;
}
public ItemMoudle(int _index,RectTransform _rectTransform)
{
itemWidth = instance.itemSize.x;
itemHeight = instance.itemSize.y;
spaceX = instance.itemSpace.x;
spaceY = instance.itemSpace.y;
isHorizontal = instance.isHorizontal;
hang = instance.hang;
lie = instance.lie;
if (!initPosArrEnd)
{
InitPosArr();
}
index = _index;
position=GetItemLocalPosition();
if (_rectTransform)
{
transform = _rectTransform;
transform.anchoredPosition = position;
transform.name = index.ToString();
}
isIntactGroup = CheckIntactGroup();
}
/// <summary>
/// 改变item索引
/// </summary>
/// <param name="_index">item目标索引</param>
public void ChangeIndex(int _index)
{
index = _index;
position=GetItemLocalPosition();
transform.name = index.ToString();
transform.anchoredPosition = position;
isIntactGroup = CheckIntactGroup();
}
public void Dispose()
{
initPosArrEnd = false;
transform = null;
xPosArr = null;
yPosArr = null;
}
/// <summary>
/// 获取当前item的坐标位置
/// </summary>
/// <returns></returns>
private Vector2 GetItemLocalPosition()
{
float x, y;
if (isHorizontal)
{
Vector2Int pos = GetGridPos();
x = (pos.y)*(itemWidth+spaceX);
y =yPosArr[pos.x];
return new Vector2(x,y);
}
else
{
Vector2Int pos = GetGridPos();
y = -(pos.y)*(itemHeight+spaceY);
x = xPosArr[pos.x];
return new Vector2(x,y);
}
}
/// <summary>
/// 获取当前item的Grid位置(第几行?第几列?)
/// </summary>
/// <returns>v2Int x=>列 y=>行</returns>
private Vector2Int GetGridPos()
{
int grid_hang, grid_lie;
if (isHorizontal)
{
grid_hang = (index) / hang;
grid_lie = (index) % hang;
}
else
{
grid_hang = (index) / lie;
grid_lie = (index) % lie;
}
return new Vector2Int(grid_lie,grid_hang);
}
/// <summary>
/// 初始化xPosArr和yPosArr
/// </summary>
private void InitPosArr()
{
initPosArrEnd = true;
bool isOddNumber;//数组长度是否为奇数
float startValue;
float itemFullSpace;
if (isHorizontal)
{
isOddNumber = hang % 2 == 1;
itemFullSpace = itemHeight + spaceY;
if (isOddNumber)
{
startValue = (hang / 2)*(itemFullSpace);
}
else
{
startValue = (hang / 2-0.5f) * (itemFullSpace);
}
yPosArr = new float[hang];
for (int i = 0; i < hang; i++)
{
yPosArr[i] = startValue - i * itemFullSpace;
}
}else
{
isOddNumber = lie % 2 == 1;
itemFullSpace = itemWidth + spaceX;
if (isOddNumber)
{
startValue = -(lie / 2)*(itemFullSpace);
}
else
{
startValue = -(lie / 2-0.5f) * (itemFullSpace);//-itemFullSpace;
}
xPosArr=new float[lie];
for (int i = 0; i < lie; i++)
{
xPosArr[i] = startValue + i * itemFullSpace;
}
}
}
/// <summary>
/// 检查是不是一个完整组
/// </summary>
bool CheckIntactGroup()
{
int itemCount = instance.itemCount;
if (isHorizontal)
{
int v=itemCount % hang;
if (v==0||index<itemCount-v)
{
return true;
}
return false;
}
else
{
int v=itemCount % lie;
if (v==0||index<itemCount-v)
{
return true;
}
return false;
}
}
}
}
Inspector拓展
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(MyScrollView))]
public class MyScrollViewEditor : Editor
{
private MyScrollView scrollView;
private SerializedProperty prefab;
private void Awake()
{
scrollView=target as MyScrollView;
prefab = this.serializedObject.FindProperty("prefab");
}
public override void OnInspectorGUI()
{
scrollView.isRecyItem = EditorGUILayout.Toggle("回收模式(isRecyItem)", scrollView.isRecyItem);
if (scrollView.isRecyItem)
{
EditorGUILayout.BeginHorizontal();
scrollView.isHorizontal=EditorGUILayout.Toggle("是否为横向滑动(isHorizontal)", scrollView.isHorizontal);
scrollView.horizontal = scrollView.isHorizontal;
scrollView.vertical = !scrollView.horizontal;
if (scrollView.isHorizontal)
{
scrollView.hang = EditorGUILayout.IntField("行数", scrollView.hang);
}else
{
scrollView.lie = EditorGUILayout.IntField("列数", scrollView.lie);
}
EditorGUILayout.EndHorizontal();
scrollView.itemSize = EditorGUILayout.Vector2Field("ItemSize", scrollView.itemSize);
scrollView.itemSpace = EditorGUILayout.Vector2Field("ItemSpace", scrollView.itemSpace);
scrollView.itemCount = EditorGUILayout.IntField("元素数量(itemCount)", scrollView.itemCount);
scrollView.fillCount = EditorGUILayout.IntField("补充组数(fillCount)", scrollView.fillCount);
EditorGUILayout.PropertyField(serializedObject.FindProperty("prefab"),new GUIContent("预制体"));
if (GUI.changed)
{
serializedObject.ApplyModifiedProperties();
}
}
base.OnInspectorGUI();
}
}
使用
public MyScrollView myScrollView;
myScrollView.OnInitItemCompleteAct((MyScrollView.ItemMoudle item) =>
{
Text t = item.transform.Find("Text").GetComponent<Text>();
t.text = item.index.ToString();
});