直接贴代码
using System.Collections.Generic;
using UnityEngine;
//using UnityEngine.EventSystems;
using UnityEngine.UI;
using DG.Tweening;
/// <summary>
/// 虚拟列表1
/// </summary>
public class VirtualScroll : ScrollRect
{
public delegate void ListItemRenderer(int index, GameObject item);
/// <summary>
/// 刷新项的回调函数
/// </summary>
public ListItemRenderer itemRenderer;
/// <summary>
/// 最大项数
/// </summary>
private int _numItems;
[Header("自定义项")]
public RectTransform Item;
[Tooltip("项的大小")]
public Vector2 ItemSize;
[Tooltip("间距")]
public float Spacing;
[Tooltip("边距")]
public RectOffset Padding;
/// <summary>
/// 当前组件
/// </summary>
private VirtualScroll _parent;
/// <summary>
/// 创建的项列表,有序排列和显示顺序一致
/// </summary>
private List<ItemInfo> _virtualItems = new List<ItemInfo>();
/// <summary>
/// 方向
/// </summary>
private enum Direction
{
Horizontal,
Vertical
}
/// <summary>
/// 自己的滑动方向
/// </summary>
private Direction _direction;
/// <summary>
/// 项信息
/// </summary>
private class ItemInfo
{
/// <summary>
/// 项
/// </summary>
public RectTransform item;
/// <summary>
/// 列表索引0开始
/// </summary>
public int index;
}
protected override void Awake()
{
base.Awake();
if (transform.parent)
{
_parent = transform.GetComponentInParent<VirtualScroll>();
}
_direction = horizontal ? Direction.Horizontal : Direction.Vertical;
}
/// <summary>
/// 变化值
/// </summary>
private void ListenerChange(Vector2 changeV)
{
RefreshItemInfo();
}
#region 计算个数
/// <summary>
/// 计算最大个数
/// </summary>
private void CalcuglateCount()
{
if (_direction == Direction.Vertical)//垂直滑动
{
///初始化容器
_parent.content.anchorMin = new Vector2(0, 1);
_parent.content.anchorMax = new Vector2(1, 1);
_parent.content.anchoredPosition = new Vector2(_parent.content.anchoredPosition.x, 0);
float ContentHeight = ItemSize.y * _numItems + (_numItems - 1) * Spacing + Padding.top + Padding.bottom;
_parent.content.sizeDelta = new Vector2(_parent.content.sizeDelta.x, ContentHeight);//设置大小 项越多值越大
//初始化项 对齐顶上
Item.pivot = new Vector2(0.5f, 1);
Item.anchorMin = new Vector2(0.5f, 1);
Item.anchorMax = new Vector2(0.5f, 1);
//计算可显示的项数
float height = _parent.GetComponent<RectTransform>().rect.height;
height -= (Padding.top + Padding.bottom);
//最少项数
int LeastItemCount = Mathf.CeilToInt(height / (ItemSize.y + Spacing));
LeastItemCount += 2;
LeastItemCount = LeastItemCount > _numItems ? _numItems : LeastItemCount;
int MaxNum = LeastItemCount - _virtualItems.Count;
//生成项
if (MaxNum > 0)
{
for (int i = _virtualItems.Count; i < LeastItemCount; i++)
{
GameObject item = GameObject.Instantiate(Item.gameObject);
item.transform.SetParent(_parent.content);
ItemInfo itemInfo = new ItemInfo();
itemInfo.item = item.GetComponent<RectTransform>();
itemInfo.index = i;
_virtualItems.Add(itemInfo);
item.SetActive(false);
item.GetComponent<RectTransform>().localScale = Vector3.one;
}
}
else
{
MaxNum = -MaxNum;
for (int i = 0; i < MaxNum; i++)
{
GameObject gameObject = _virtualItems[_virtualItems.Count - 1].item.gameObject;
_virtualItems.RemoveAt(_virtualItems.Count - 1);
Destroy(gameObject);
}
}
if (_parent)
{
_parent.onValueChanged.RemoveAllListeners();
_parent.onValueChanged.AddListener(ListenerChange);
}
}
else
{
_parent.content.anchorMin = new Vector2(0, 0);
_parent.content.anchorMax = new Vector2(0, 1);
_parent.content.anchoredPosition = new Vector2(0, _parent.content.anchoredPosition.y);
float ContentHeight = ItemSize.x * _numItems + (_numItems - 1) * Spacing + Padding.left + Padding.right;
_parent.content.sizeDelta = new Vector2(ContentHeight, _parent.content.sizeDelta.y);//设置大小 项越多值越大
//初始化项 对齐顶上
Item.pivot = new Vector2(0f, 0.5f);
Item.anchorMin = new Vector2(0f, 0.5f);
Item.anchorMax = new Vector2(0f, 0.5f);
//计算可显示的项数
float height = _parent.GetComponent<RectTransform>().rect.width;
height -= (Padding.left + Padding.right);
//最少项数
int LeastItemCount = Mathf.CeilToInt(height / (ItemSize.x + Spacing));
LeastItemCount += 2;
LeastItemCount = LeastItemCount > _numItems ? _numItems : LeastItemCount;
int MaxNum = LeastItemCount - _virtualItems.Count;
//生成项
if (MaxNum > 0)
{
for (int i = _virtualItems.Count; i < LeastItemCount; i++)
{
GameObject item = GameObject.Instantiate(Item.gameObject);
item.transform.SetParent(_parent.content);
ItemInfo itemInfo = new ItemInfo();
itemInfo.item = item.GetComponent<RectTransform>();
itemInfo.item.anchoredPosition = new Vector2(0,0);
itemInfo.index = i;
_virtualItems.Add(itemInfo);
item.SetActive(false);
item.GetComponent<RectTransform>().localScale = Vector3.one;
}
}
else
{
MaxNum = -MaxNum;
for (int i = 0; i < MaxNum; i++)
{
GameObject gameObject = _virtualItems[_virtualItems.Count - 1].item.gameObject;
_virtualItems.RemoveAt(_virtualItems.Count - 1);
Destroy(gameObject);
}
}
if (_parent)
{
_parent.onValueChanged.RemoveAllListeners();
_parent.onValueChanged.AddListener(ListenerChange);
}
}
}
#endregion
/// <summary>
/// 设置总项数,虚拟列表创建显示个数
/// 最大个数必须大于可视化个数1倍
/// </summary>
public int numItems
{
get { return _numItems; }
set
{
if (Padding==null)
throw new System.Exception("VirtualScroll: Set Padding first!");
if (itemRenderer == null)
throw new System.Exception("VirtualScroll: Set itemRenderer first!");
_numItems = value;
CalcuglateCount();
RefreshList();
}
}
/// <summary>
/// 刷新 无方向
/// </summary>
private void RefreshItem()
{
//直接刷新就全部刷新
for (int i = 0; i < _virtualItems.Count; i++)
{
if (!_virtualItems[i].item.gameObject.activeSelf)
{
_virtualItems[i].item.gameObject.SetActive(true);
}
CalibrationPos(_virtualItems[i]);
itemRenderer?.Invoke(_virtualItems[i].index, _virtualItems[i].item.gameObject);//
}
}
/// <summary>
/// 校准位置
/// 向上对齐,所以索引越大,负数越大
/// </summary>
private void CalibrationPos(ItemInfo itemInfo)
{
if (_direction == Direction.Vertical)
{
float _oldPos = itemInfo.item.anchoredPosition.y;
float Y = Padding.top + itemInfo.index * ItemSize.y + itemInfo.index * Spacing;
Y *= -1;
if (!Mathf.Approximately(_oldPos, Y))
{
itemInfo.item.anchoredPosition = new Vector2(itemInfo.item.anchoredPosition.x, Y);
}
}
else
{
float _oldPos = itemInfo.item.anchoredPosition.x;
float Y = Padding.left + itemInfo.index * ItemSize.x + itemInfo.index * Spacing;
if (!Mathf.Approximately(_oldPos, Y))
{
itemInfo.item.anchoredPosition = new Vector2(Y, itemInfo.item.anchoredPosition.y);
}
}
}
#region 刷新项信息
private void RefreshItemInfo()
{
if (_direction == Direction.Vertical)
{
RefreshVItem();
}
else
{
RefreshHItem();
}
}
/// <summary>
/// 刷新垂直
/// </summary>
private void RefreshVItem()
{
float contentY = _parent.content.anchoredPosition.y;
contentY -= Padding.top;
//当前可显示的下标
int index = (int)Mathf.Floor(contentY / (ItemSize.y + Spacing));
index = index < 0 ? 0 : index;
int VirtualIndex = _virtualItems[0].index;
if (index == VirtualIndex)//第一索引和当前索引一致就返回过滤
{
return;
}
else if (index < VirtualIndex)
{
//根据索引移动元素
for (int i = 0; i < _virtualItems.Count; i++)
{
VirtualIndex -= 1;
ItemInfo itemInfo = _virtualItems[_virtualItems.Count - 1];
GameObject gameObject = itemInfo.item.gameObject;
_virtualItems.RemoveAt(_virtualItems.Count - 1);
_virtualItems.Insert(0, itemInfo);
if (VirtualIndex == index)
{
break;
}
}
}
else
{
for (int i = 0; i < _virtualItems.Count; i++)
{
VirtualIndex += 1;
ItemInfo itemInfo = _virtualItems[0];
GameObject gameObject = itemInfo.item.gameObject;
_virtualItems.RemoveAt(0);
_virtualItems.Add(itemInfo);
if (VirtualIndex == index)
{
break;
}
}
}
//获取位置为负数
float startY = (Padding.top + index * (ItemSize.y + Spacing)) * -1;
int itemIndex = 0;
for (int i = index; i < _numItems; i++)
{
if (_virtualItems.Count <= itemIndex)//最多只允许刷新项那么多
{
break;
}
if (!_virtualItems[itemIndex].item.gameObject.activeSelf)
{
_virtualItems[itemIndex].item.gameObject.SetActive(true);
}
if (_virtualItems[itemIndex].index != i)//只有索引不一致才刷新
{
_virtualItems[itemIndex].index = i;
_virtualItems[itemIndex].item.anchoredPosition = new Vector2(_virtualItems[itemIndex].item.anchoredPosition.x, startY);
itemRenderer?.Invoke(i, _virtualItems[itemIndex].item.gameObject);
}
startY -= ItemSize.y + Spacing;
itemIndex++;
}
//多余的项隐藏
//for (int i = itemIndex; i < _virtualItems.Count; i++)
//{
// if (_virtualItems[i].item.gameObject.activeSelf)
// {
// _virtualItems[i].item.gameObject.SetActive(false);
// }
//}
}
/// <summary>
/// 刷新水平
/// </summary>
private void RefreshHItem()
{
float contentY = _parent.content.anchoredPosition.x;
contentY += Padding.left;
contentY = -contentY;
//当前可显示的数据下标
int index = (int)Mathf.Floor(contentY / (ItemSize.x + Spacing));
index = index < 0 ? 0 : index;
int VirtualIndex = _virtualItems[0].index;
if (index == VirtualIndex)//第一索引和当前索引一致就返回过滤
{
return;
}
else if (index < VirtualIndex)
{
//根据索引移动元素
for (int i = 0; i < _virtualItems.Count; i++)
{
VirtualIndex -= 1;
ItemInfo itemInfo = _virtualItems[_virtualItems.Count - 1];
GameObject gameObject = itemInfo.item.gameObject;
_virtualItems.RemoveAt(_virtualItems.Count - 1);
_virtualItems.Insert(0, itemInfo);
if (VirtualIndex == index)
{
break;
}
}
}
else
{
for (int i = 0; i < _virtualItems.Count; i++)
{
VirtualIndex += 1;
ItemInfo itemInfo = _virtualItems[0];
GameObject gameObject = itemInfo.item.gameObject;
_virtualItems.RemoveAt(0);
_virtualItems.Add(itemInfo);
if (VirtualIndex == index)
{
break;
}
}
}
//获取位置为负数
float startY = Padding.left + index * (ItemSize.x + Spacing);
int itemIndex = 0;
for (int i = index; i < _numItems; i++)
{
if (_virtualItems.Count <= itemIndex)//最多只允许刷新项那么多
{
break;
}
if (!_virtualItems[itemIndex].item.gameObject.activeSelf)
{
_virtualItems[itemIndex].item.gameObject.SetActive(true);
}
if (_virtualItems[itemIndex].index != i)//只有索引不一致才刷新
{
_virtualItems[itemIndex].index = i;
_virtualItems[itemIndex].item.anchoredPosition = new Vector2(startY, _virtualItems[itemIndex].item.anchoredPosition.y);
itemRenderer?.Invoke(i, _virtualItems[itemIndex].item.gameObject);
}
startY += ItemSize.x + Spacing;
itemIndex++;
}
//多余的项隐藏
//for (int i = itemIndex; i < _virtualItems.Count; i++)
//{
// if (_virtualItems[i].item.gameObject.activeSelf)
// {
// _virtualItems[i].item.gameObject.SetActive(false);
// }
//}
}
#endregion
//===========================================外部调用
/// <summary>
/// 校准刷新项
/// </summary>
public void CalibrationRefresh()
{
RefreshItemInfo();
}
/// <summary>
/// 获取指定索引的项
/// </summary>
public GameObject GetListAt(int index)
{
ItemInfo itemInfo = _virtualItems.Find(item=>(item.index == index));
if (itemInfo!=null)
{
return itemInfo.item.gameObject;
}
return null;
}
/// <summary>
/// 刷新当前显示列表的数据
/// </summary>
public void RefreshList()
{
RefreshItem();
}
/// <summary>
/// 刷新指定索引的数据
/// </summary>
public void RefreshListIndex(int index)
{
ItemInfo itemInfo = _virtualItems.Find(item => (item.index == index));
if (itemInfo==null)
{
throw new System.Exception("RefreshListIndex: index out:"+ index);
}
else
{
itemRenderer?.Invoke(index, itemInfo.item.gameObject);
}
}
/// <summary>
/// 跳转到指定索引
/// </summary>
/// <param name="first"></param>
public void ScrollToView(ScrollTo scrollTo,bool anim =false)
{
if (scrollTo == ScrollTo.First)
{
ScrollToView(0, anim);
}
else if (scrollTo== ScrollTo.Last)
{
ScrollToView(_numItems-1, anim);
}
else
{
throw new System.Exception("ScrollToView: ScrollTo is null!");
}
}
/// <summary>
/// 跳转到指定索引
/// </summary>
public void ScrollToView(int index, bool anim = false)
{
if (index < 0)
{
throw new System.Exception("ScrollToView: index < 0!");
}
else if (index > (_numItems - 1))
{
throw new System.Exception("ScrollToView: index > Max Value!");
}
else
{
if (_direction == Direction.Vertical)
{
float height = (ItemSize.y + Spacing) * index;//+ Padding.top
//
float MaxHeight = _parent.content.rect.height - _parent.viewport.rect.height - Padding.bottom;
if (height > MaxHeight)
{
height = MaxHeight;
}
if (anim)
{
_parent.content.DOAnchorPosY(height, 0.5f, true);
}
else
{
_parent.content.anchoredPosition = new Vector2(_parent.content.anchoredPosition.x, height);
}
}
else
{
float height = (ItemSize.x + Spacing) * index;//+ Padding.left
float MaxHeight = _parent.content.rect.width - _parent.viewport.rect.width - Padding.right;
if (height > MaxHeight)
{
height = MaxHeight;
}
if (anim)
{
_parent.content.DOAnchorPosX(-height, 0.5f, true);
}
else
{
_parent.content.anchoredPosition = new Vector2(-height, _parent.content.anchoredPosition.y);
}
}
}
}
}
/// <summary>
/// 跳转到
/// </summary>
public enum ScrollTo
{
/// <summary>
/// 第一
/// </summary>
First,
/// <summary>
/// 最后
/// </summary>
Last
}
还需要在Editor文件夹下创建一个脚本用于编辑器显示
using UnityEditor;
[CustomEditor(typeof(VirtualScroll))]
public class CustomVirtualScroll : Editor
{
}
显示出来是这样的
下面参数是新加的,类似GridLayoutGroup组件参数,上面参数是原本就有的
参数都可以代码加载,需要自己去添加方法
使用方式也很简单,和FGUI列表使用类似
VirtualScroll VScrollRect;
public void Init()
{
VScrollRect.itemRenderer = ItemRenderer;
VScrollRect.numItems = MaxNum;
//VScrollRect.RefreshList();//全部刷新
}
/// <summary>
/// 渲染刷新
/// </summary>
/// <param name="index">当前索引</param>
/// <param name="item">返回的对象</param>
public void ItemRenderer(int index,GameObject item)
{
//处理逻辑
}
这个只适用于项的大小一致,不一致的需要自己去修改,我的代码有点乱,不太好看,里面用了DG.Tweening来跳转,不使用的直接注释掉就行了