本文仅作学习笔记与交流,不作任何商业用途
本文包括但不限于unity官方手册,唐老狮,麦扣教程知识,引用会标记,如有不足还请斧正
1.什么是ScrollBar
滚动块:Unity - Manual: Scrollbar
2.重要参数
该笔记来源唐老狮:
3.什么是ScrollView
滚动视图:Unity - 手动:Scroll Rect
重要参数
简单内容
内容大小,是否能水平拖动,是否能竖直拖动
Movement Type (内容移动类型)
该笔记来源唐老狮:
Inertia(内容移动惯性)
Scroll Sensitivity(鼠标中键滚动灵敏度)
其它参数
4.实现一个横向无限滚动的列表
下面为实现思路,画图的话大致如此:
具体数值还要看项目
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class InfiniteScroll : MonoBehaviour
{
public ScrollRect scrollRect;//控制滑动速度
public RectTransform ViewPort;//控制可见区域
public RectTransform Content;//调整位置
public HorizontalLayoutGroup HorizontalLayG;//自动排列
public RectTransform[] ItemList;
private Vector2 OldVelocity;
private bool isUpdated;
private float itemWidth;
private float spacing;
private float contentWidth;
private float viewportWidth;
void Start()
{
isUpdated = false;
OldVelocity = Vector2.zero;
itemWidth = ItemList[0].rect.width; //元素宽度= 300
spacing = HorizontalLayG.spacing;//元素距离 =140
contentWidth = ItemList.Length * (itemWidth + spacing);//内容总宽度=4*(300+140)=1760
viewportWidth = ViewPort.rect.width;//蒙版宽度 =1255
//所需添加元素数
int itemsToAdd = Mathf.CeilToInt(viewportWidth / (itemWidth + spacing));
// 向后添加元素
for (int i = 0; i < itemsToAdd; i++)
{
RectTransform rt = Instantiate(ItemList[i % ItemList.Length], Content);
rt.SetAsLastSibling();
}
// 向前添加元素
for (int i = 0; i < itemsToAdd; i++)
{
int num = ItemList.Length - i - 1;
while (num < 0) num += ItemList.Length;
RectTransform rt = Instantiate(ItemList[num], Content);
rt.SetAsFirstSibling();
}
// 初始化 Content 位置
Content.localPosition = new Vector3(
-(itemWidth + spacing) * itemsToAdd,//-440*3 =-1320
Content.localPosition.y,
Content.localPosition.z
);
}
void Update()
{
float currentPosition = Content.localPosition.x;//
// 处理向右滚动超出边界
if (currentPosition > 0)
{
Canvas.ForceUpdateCanvases();
OldVelocity = scrollRect.velocity;
Content.localPosition -= new Vector3(contentWidth, 0, 0);
isUpdated = true;
}
// 处理向左滚动超出边界
if (currentPosition < -contentWidth)
{
Canvas.ForceUpdateCanvases();
OldVelocity = scrollRect.velocity;
Content.localPosition += new Vector3(contentWidth, 0, 0);
isUpdated = true;
}
// 恢复滚动速度
if (isUpdated)
{
isUpdated = false;
scrollRect.velocity = OldVelocity;
}
}
}
这个是写死的方法 有很多不方便的地方
(第一个缺点: 预先 生成连接元素 导致不支持超级多的内容
解决方法就是可以用对象池适当回收,只展示渲染的内容
第二个缺点:update去检测会导致Canvas.ForceUpdateCanvases()以及条件判断在快速操作时发生卡顿和不准确的情况
解决方法是可以通过事件驱动的方式去优化,因为scroll view提供了这个方式)
下面是deepseek修改的,还是存在重大瑕疵,越界检测的时候直接停止刷新,导致出现不是无限滚动的感觉 但是写法可以参考
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic; // 需要这个命名空间使用队列
[RequireComponent(typeof(ScrollRect))] // 强制要求组件存在
public class OptimizedInfiniteScroll : MonoBehaviour
{
public enum ScrollDirection { Horizontal, Vertical }
[Header("Base Settings")]
public ScrollDirection direction = ScrollDirection.Horizontal;
public RectTransform viewPort;
public RectTransform content;
public RectTransform[] itemPrefabs; // 项目的不同预制体类型
[Header("Advanced")]
[Tooltip("视口外缓冲的项目数量")]
public int bufferZone = 2;
public float loadingDelay = 0.1f; // 异步加载间隔(秒)
// 布局相关计算参数
private float _itemSize;
private float _spacing;
private float _contentSize;
private float _viewportSize;
// 对象池相关
private Queue<RectTransform> _pool = new Queue<RectTransform>();
private List<RectTransform> _activeItems = new List<RectTransform>();
// 数据相关
private int _totalItems = 100; // 示例数据总量
private Vector2 _lastScrollPos;
/* ▶ 不常见API说明:
- Tooltip:在Inspector面板显示字段提示
- RequireComponent:自动添加依赖组件
- ContentSizeFitter:自动调整内容区域大小的组件
*/
void Start()
{
// ► 组件校验
if (itemPrefabs.Length == 0)
Debug.LogError("需要至少一个预制体!", this);
// ► 自动获取必要组件
var scrollRect = GetComponent<ScrollRect>();
var layoutGroup = GetComponent<HorizontalOrVerticalLayoutGroup>();
// ► 方向配置
scrollRect.horizontal = direction == ScrollDirection.Horizontal;
scrollRect.vertical = direction == ScrollDirection.Vertical;
// ► 尺寸计算
_itemSize = direction == ScrollDirection.Horizontal
? itemPrefabs[0].rect.width
: itemPrefabs[0].rect.height;
_spacing = layoutGroup.spacing;
_viewportSize = direction == ScrollDirection.Horizontal
? viewPort.rect.width
: viewPort.rect.height;
// ► 设置初始内容大小(根据总数据量)
_contentSize = _totalItems * (_itemSize + _spacing);
content.sizeDelta = direction == ScrollDirection.Horizontal
? new Vector2(_contentSize, 0)
: new Vector2(0, _contentSize);
// ► 事件监听(比Update更高效)
scrollRect.onValueChanged.AddListener(OnScroll);
// ► 初始加载可见项目
StartCoroutine(InitialLoad());
}
// ▣ 协程:分帧加载避免卡顿
IEnumerator InitialLoad()
{
// 计算需要加载的数量:可视数量 + 缓冲区
int requiredItems = Mathf.CeilToInt(_viewportSize / (_itemSize + _spacing)) + bufferZone * 2;
for (int i = 0; i < requiredItems; i++)
{
GetNewItem(i);
yield return new WaitForSeconds(loadingDelay);
}
}
// ▣ 对象池获取项目
RectTransform GetNewItem(int dataIndex)
{
RectTransform item;
if (_pool.Count > 0)
{
item = _pool.Dequeue();
item.gameObject.SetActive(true);
}
else
{
// ► 随机选择不同预制体实现多样化(根据需求修改)
var prefab = itemPrefabs[dataIndex % itemPrefabs.Length];
item = Instantiate(prefab, content);
}
// ► 实际项目位置计算(关键!)
float position = dataIndex * (_itemSize + _spacing);
item.anchoredPosition = direction == ScrollDirection.Horizontal
? new Vector2(position, 0)
: new Vector2(0, -position);
_activeItems.Add(item);
return item;
}
// ▣ 滚动事件回调
void OnScroll(Vector2 normalizedPos)
{
// 计算滚动偏移量
Vector2 currentOffset = direction == ScrollDirection.Horizontal
? new Vector2(content.anchoredPosition.x, 0)
: new Vector2(0, content.anchoredPosition.y);
Vector2 delta = currentOffset - _lastScrollPos;
_lastScrollPos = currentOffset;
UpdateItems(delta);
CheckBoundary();
}
// ▣ 边界检测与内容跳转
void CheckBoundary()
{
Vector2 anchoredPos = content.anchoredPosition;
bool needReposition = false;
// 向右/向下滚动超出
if (direction == ScrollDirection.Horizontal && anchoredPos.x < -_contentSize)
{
anchoredPos.x += _contentSize;
needReposition = true;
}
else if (direction == ScrollDirection.Vertical && anchoredPos.y > _contentSize)
{
anchoredPos.y -= _contentSize;
needReposition = true;
}
// 向左/向上滚动超出
if (direction == ScrollDirection.Horizontal && anchoredPos.x > 0)
{
anchoredPos.x -= _contentSize;
needReposition = true;
}
else if (direction == ScrollDirection.Vertical && anchoredPos.y < 0)
{
anchoredPos.y += _contentSize;
needReposition = true;
}
if (needReposition)
{
// ► Canvas.ForceUpdateCanvases() 强制立即更新布局计算
Canvas.ForceUpdateCanvases();
content.anchoredPosition = anchoredPos;
}
}
// ▣ 动态更新可见项目
void UpdateItems(Vector2 delta)
{
// 获取当前第一个活动的项目索引
int firstIndex = Mathf.FloorToInt(
(direction == ScrollDirection.Horizontal
? -content.anchoredPosition.x
: content.anchoredPosition.y) / (_itemSize + _spacing)
);
// ► 偏移项目
foreach (var item in _activeItems)
{
// 当项目完全移出视口
if (IsOutOfView(item))
{
_pool.Enqueue(item);
item.gameObject.SetActive(false);
}
}
// 清理已回收的项目
_activeItems.RemoveAll(item => !item.gameObject.activeSelf);
// 加载新项目
int requiredItems = Mathf.CeilToInt(_viewportSize / (_itemSize + _spacing)) + bufferZone;
for (int i = firstIndex - bufferZone; i <= firstIndex + requiredItems; i++)
{
if (i < 0 || i >= _totalItems) continue;
if (!HasItem(i)) GetNewItem(i);
}
}
// ▣ 辅助方法:判断项目是否在视口外
bool IsOutOfView(RectTransform item)
{
// ► TransformPoint:将本地坐标转换为世界坐标
Vector3[] itemCorners = new Vector3[4];
item.GetWorldCorners(itemCorners);
Vector3[] viewCorners = new Vector3[4];
viewPort.GetWorldCorners(viewCorners);
// 根据方向判断
if (direction == ScrollDirection.Horizontal)
{
return itemCorners[2].x < viewCorners[0].x ||
itemCorners[0].x > viewCorners[2].x;
}
else
{
return itemCorners[0].y > viewCorners[2].y ||
itemCorners[2].y < viewCorners[0].y;
}
}
// ▣ 检查是否已有该数据项
bool HasItem(int index)
{
foreach (var item in _activeItems)
{
int itemIndex = (int)(item.anchoredPosition.x / (_itemSize + _spacing));
if (itemIndex == index) return true;
}
return false;
}
// ▣ 安全回收
void OnDestroy()
{
foreach (var item in _activeItems)
Destroy(item.gameObject);
foreach (var item in _pool)
Destroy(item.gameObject);
}
}