Unity UGUI 无限循环列表组件
概述
这是一个高性能的Unity UGUI无限循环列表组件,支持任意数量的数据项,只使用少量UI对象实现无限滚动效果。
特性
- ✅ 无限循环滚动:支持数据的无限循环显示
- ✅ 高性能优化:只创建5个UI对象处理任意数量数据
- ✅ 智能动画系统:中间元素平滑移动,首尾交换瞬间切换
- ✅ 多种跳转方式:支持直接跳转、逐步跳转、平滑跳转
- ✅ 拖拽交互:支持手势拖拽切换
- ✅ 自适应时间:根据跳转距离自动调整动画时长
- ✅ Mask遮挡:只显示指定数量的项目
核心原理
UI对象管理
- 创建5个UI对象,只显示中间3个
- 通过首尾交换实现无限循环效果
- 使用Mask组件遮挡多余的UI元素
动画策略
- 中间可见元素:3个可见UI元素有平滑移动动画
- 首尾交换元素:瞬间出现在新位置,无移动动画
- 智能跳转:根据距离选择最佳动画方式
文件结构
InfiniteScroll/
├── InfiniteLoopList.cs # 主控制器
└── LoopItem.cs # 列表项组件
安装和设置
1. 创建UI结构
Canvas
└── InfiniteLoopList (空GameObject)
└── Container (添加Image和Mask组件)
└── [动态生成的Item们]
2. Container设置
- 添加
Image
组件(可设为透明) - 添加
Mask
组件 - 设置RectTransform大小来控制可见区域
3. 创建Item预制体
- 创建UI元素作为Item基础
- 添加
Image
(背景)、Text
(文本)、Button
(可选) - 添加
LoopItem
脚本
4. 配置主脚本
- 将Container拖到
container
字段 - 将Item预制体拖到
itemPrefab
字段 - 设置相关参数
API文档
InfiniteLoopList 主要方法
基本移动
// 移动到下一个数据项
public void MoveToNext()
// 移动到上一个数据项
public void MoveToPrevious()
跳转功能
// 直接跳转(无动画)
public void JumpToIndex(int index)
// 跳转并选择是否使用动画
public void JumpToIndex(int index, bool withAnimation)
// 平滑跳转(自动计算时长)
public void SmoothJumpToIndex(int targetIndex, float duration = -1f)
获取状态
// 获取当前中心项的数据索引
public int GetCurrentCenterIndex()
// 获取当前中心项的数据
public string GetCurrentCenterData()
LoopItem 主要方法
// 设置数据
public void SetData(string data, int index)
// 设置可见性
public void SetVisible(bool visible)
// 获取数据
public string GetData()
public int GetDataIndex()
配置参数
InfiniteLoopList 参数
参数 | 类型 | 默认值 | 说明 |
---|---|---|---|
itemPrefab | GameObject | null | 列表项预制体 |
container | Transform | null | 容器对象 |
visibleCount | int | 3 | 可见项目数量 |
totalUICount | int | 5 | UI对象池大小 |
itemWidth | float | 200f | 每项宽度 |
spacing | float | 20f | 项目间距 |
moveSpeed | float | 500f | 移动速度 |
moveCurve | AnimationCurve | EaseInOut | 动画曲线 |
使用示例
基本使用
// 获取组件
InfiniteLoopList list = GetComponent<InfiniteLoopList>();
// 移动操作
list.MoveToNext(); // 下一个
list.MoveToPrevious(); // 上一个
// 跳转操作
list.JumpToIndex(5); // 直接跳转到索引5
list.JumpToIndex(10, true); // 带动画跳转到索引10
list.SmoothJumpToIndex(15, 2f); // 2秒平滑跳转到索引15
自定义数据
// 在InfiniteLoopList.cs的InitializeData方法中修改
void InitializeData()
{
dataList.Clear();
// 添加自定义数据
for (int i = 0; i < yourDataCount; i++)
{
dataList.Add(yourData[i]);
}
}
跳转策略
智能跳转算法
// 距离判断
if (距离 ≤ 5步)
{
使用逐步移动动画; // 每步完整动画
}
else
{
使用平滑跳转动画; // 整体缓动动画
}
时间计算
- 逐步跳转:每步固定时间 + 0.05秒间隔
- 平滑跳转:
distance * 0.08f
,限制在0.3-1.5秒之间 - 最短路径:自动计算环形结构中的最短距离
性能优化
对象池管理
- 只创建5个UI对象处理任意数量数据
- 通过数据索引映射实现无限数据支持
- UI对象重用,避免频繁创建销毁
动画优化
- 首尾交换无动画,减少不必要的视觉干扰
- 中间元素动画流畅,提供良好的用户体验
- 智能时间计算,避免动画时间过长
内存优化
- 数据与UI分离,支持大量数据
- 按需更新UI内容
- 最小化GC分配
测试功能
OnGUI测试面板
组件提供了完整的测试界面,包括:
- 基本移动测试:上一个/下一个按钮
- 近距离跳转:测试逐步移动动画
- 远距离跳转:测试平滑移动动画
- 直接跳转:测试无动画跳转
- 自动测试:自动测试各种距离的跳转效果
测试方法
// 自动测试不同距离的跳转
StartCoroutine(TestVariousDistances());
扩展建议
自定义列表项
- 继承
LoopItem
类 - 重写
SetData
方法 - 添加自定义UI元素和逻辑
添加更多动画效果
- 修改
moveCurve
参数 - 在动画协程中添加缩放、旋转等效果
- 支持不同方向的滚动(垂直滚动)
数据绑定
- 实现数据源接口
- 支持动态添加/删除数据
- 添加数据变化通知
注意事项
- Mask组件:确保Container有Mask组件用于遮挡
- 预制体设置:Item预制体必须有Text组件
- 性能考虑:数据量很大时考虑异步加载
- 内存管理:及时清理不需要的数据引用
- 动画冲突:避免在动画进行时调用跳转方法
常见问题
Q: 为什么显示位置不正确?
A: 检查Container的RectTransform设置,确保锚点和位置正确。
Q: 动画不流畅怎么办?
A: 调整 moveSpeed
参数或修改 moveCurve
动画曲线。
Q: 如何支持垂直滚动?
A: 修改位置计算逻辑,将X坐标改为Y坐标。
Q: 数据更新后如何刷新?
A: 调用 UpdateAllItems()
方法刷新显示。
这个组件为Unity UGUI提供了高性能的无限循环列表解决方案,适用于各种需要循环显示大量数据的场景。
完整代码
InfiniteLoopList.cs
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections.Generic;
using System;
public class InfiniteLoopList : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Header("配置")]
public GameObject itemPrefab; // 列表项预制体
public Transform container; // 容器(有Mask组件)
public int visibleCount = 3; // 可见数量
public int totalUICount = 5; // 总UI数量
public float itemWidth = 200f; // 每项宽度
public float spacing = 20f; // 间距
[Header("动画设置")]
public float moveSpeed = 500f; // 移动速度
public AnimationCurve moveCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
// 数据
private List<string> dataList = new List<string>();
private List<LoopItem> uiItems = new List<LoopItem>();
// 位置管理
private List<float> positions = new List<float>();
private int centerIndex = 0; // 中心显示的数据索引
// 拖拽相关
private Vector2 dragStartPos;
private float dragStartTime;
private bool isDragging = false;
private bool isAnimating = false;
// 计算相关
private float itemDistance; // 项目间距离
private RectTransform containerRect;
// OnGUI相关
private bool showDetailPanel = false;
void Start()
{
InitializeData();
SetupList();
}
/// <summary>
/// 初始化数据
/// </summary>
void InitializeData()
{
// 创建测试数据
for (int i = 0; i < 20; i++)
{
dataList.Add($"Item {i}");
}
}
/// <summary>
/// 设置列表
/// </summary>
void SetupList()
{
containerRect = container.GetComponent<RectTransform>();
itemDistance = itemWidth + spacing;
// 计算各个位置
CalculatePositions();
// 创建UI项
CreateUIItems();
// 初始化显示
UpdateAllItems();
}
/// <summary>
/// 计算所有位置
/// </summary>
void CalculatePositions()
{
positions.Clear();
// 计算中心位置
float centerPos = 0f;
// 计算所有位置(以中心为基准)
for (int i = 0; i < totalUICount; i++)
{
int offset = i - totalUICount / 2;
float pos = centerPos + offset * itemDistance;
positions.Add(pos);
}
}
/// <summary>
/// 创建UI项
/// </summary>
void CreateUIItems()
{
for (int i = 0; i < totalUICount; i++)
{
GameObject itemObj = Instantiate(itemPrefab, container);
LoopItem item = itemObj.GetComponent<LoopItem>();
if (item == null)
{
item = itemObj.AddComponent<LoopItem>();
}
// 设置初始位置
RectTransform itemRect = itemObj.GetComponent<RectTransform>();
itemRect.anchoredPosition = new Vector2(positions[i], 0);
itemRect.sizeDelta = new Vector2(itemWidth, itemRect.sizeDelta.y);
uiItems.Add(item);
}
}
/// <summary>
/// 更新所有项目
/// </summary>
void UpdateAllItems()
{
int centerUIIndex = totalUICount / 2;
for (int i = 0; i < uiItems.Count; i++)
{
// 计算这个UI项应该显示的数据索引
int dataOffset = i - centerUIIndex;
int dataIndex = (centerIndex + dataOffset) % dataList.Count;
if (dataIndex < 0) dataIndex += dataList.Count;
// 更新数据
uiItems[i].SetData(dataList[dataIndex], dataIndex);
// 检查是否在可见范围内
bool isVisible = Mathf.Abs(i - centerUIIndex) <= visibleCount / 2;
uiItems[i].SetVisible(isVisible);
}
}
/// <summary>
/// 移动到下一个(向右移动,显示下一个数据)
/// </summary>
public void MoveToNext()
{
if (isAnimating) return;
centerIndex = (centerIndex + 1) % dataList.Count;
StartCoroutine(AnimateMoveToNext());
}
/// <summary>
/// 移动到上一个(向左移动,显示上一个数据)
/// </summary>
public void MoveToPrevious()
{
if (isAnimating) return;
centerIndex = (centerIndex - 1 + dataList.Count) % dataList.Count;
StartCoroutine(AnimateMoveToPrevious());
}
/// <summary>
/// 向右移动动画(显示下一个数据)
/// UI向左移动,最左边UI瞬间跳到最右边
/// </summary>
System.Collections.IEnumerator AnimateMoveToNext()
{
isAnimating = true;
// 最左边的UI项(将要移动到最右边)
LoopItem leftmostItem = uiItems[0];
RectTransform leftmostRect = leftmostItem.GetComponent<RectTransform>();
// 瞬间将最左边的UI移动到最右边的隐藏位置
float hiddenRightPos = positions[positions.Count - 1] + itemDistance;
leftmostRect.anchoredPosition = new Vector2(hiddenRightPos, 0);
// 重新排列UI项列表(最左边移到最右边)
uiItems.RemoveAt(0);
uiItems.Add(leftmostItem);
// 准备动画数据
List<Vector2> startPositions = new List<Vector2>();
List<Vector2> targetPositions = new List<Vector2>();
for (int i = 0; i < uiItems.Count; i++)
{
RectTransform itemRect = uiItems[i].GetComponent<RectTransform>();
startPositions.Add(itemRect.anchoredPosition);
targetPositions.Add(new Vector2(positions[i], 0));
}
// 执行动画
float duration = itemDistance / moveSpeed;
float elapsed = 0f;
while (elapsed < duration)
{
float t = elapsed / duration;
float curveT = moveCurve.Evaluate(t);
for (int i = 0; i < uiItems.Count; i++)
{
RectTransform itemRect = uiItems[i].GetComponent<RectTransform>();
Vector2 pos = Vector2.Lerp(startPositions[i], targetPositions[i], curveT);
itemRect.anchoredPosition = pos;
}
elapsed += Time.deltaTime;
yield return null;
}
// 确保最终位置正确
for (int i = 0; i < uiItems.Count; i++)
{
RectTransform itemRect = uiItems[i].GetComponent<RectTransform>();
itemRect.anchoredPosition = targetPositions[i];
}
// 更新数据显示
UpdateAllItems();
isAnimating = false;
}
/// <summary>
/// 向左移动动画(显示上一个数据)
/// UI向右移动,最右边UI瞬间跳到最左边
/// </summary>
System.Collections.IEnumerator AnimateMoveToPrevious()
{
isAnimating = true;
// 最右边的UI项(将要移动到最左边)
LoopItem rightmostItem = uiItems[uiItems.Count - 1];
RectTransform rightmostRect = rightmostItem.GetComponent<RectTransform>();
// 瞬间将最右边的UI移动到最左边的隐藏位置
float hiddenLeftPos = positions[0] - itemDistance;
rightmostRect.anchoredPosition = new Vector2(hiddenLeftPos, 0);
// 重新排列UI项列表(最右边移到最左边)
uiItems.RemoveAt(uiItems.Count - 1);
uiItems.Insert(0, rightmostItem);
// 准备动画数据
List<Vector2> startPositions = new List<Vector2>();
List<Vector2> targetPositions = new List<Vector2>();
for (int i = 0; i < uiItems.Count; i++)
{
RectTransform itemRect = uiItems[i].GetComponent<RectTransform>();
startPositions.Add(itemRect.anchoredPosition);
targetPositions.Add(new Vector2(positions[i], 0));
}
// 执行动画
float duration = itemDistance / moveSpeed;
float elapsed = 0f;
while (elapsed < duration)
{
float t = elapsed / duration;
float curveT = moveCurve.Evaluate(t);
for (int i = 0; i < uiItems.Count; i++)
{
RectTransform itemRect = uiItems[i].GetComponent<RectTransform>();
Vector2 pos = Vector2.Lerp(startPositions[i], targetPositions[i], curveT);
itemRect.anchoredPosition = pos;
}
elapsed += Time.deltaTime;
yield return null;
}
// 确保最终位置正确
for (int i = 0; i < uiItems.Count; i++)
{
RectTransform itemRect = uiItems[i].GetComponent<RectTransform>();
itemRect.anchoredPosition = targetPositions[i];
}
// 更新数据显示
UpdateAllItems();
isAnimating = false;
}
/// <summary>
/// 跳转到指定索引(无动画)
/// </summary>
public void JumpToIndex(int index)
{
JumpToIndex(index, false);
}
/// <summary>
/// 跳转到指定索引
/// </summary>
/// <param name="index">目标索引</param>
/// <param name="withAnimation">是否使用动画</param>
public void JumpToIndex(int index, bool withAnimation)
{
if (index < 0 || index >= dataList.Count || isAnimating) return;
if (!withAnimation)
{
// 直接跳转,无动画
centerIndex = index;
UpdateAllItems();
}
else
{
// 带动画的跳转
StartCoroutine(AnimatedJumpToIndex(index));
}
}
/// <summary>
/// 带动画的跳转到指定索引
/// </summary>
System.Collections.IEnumerator AnimatedJumpToIndex(int targetIndex)
{
if (isAnimating || targetIndex == centerIndex) yield break;
isAnimating = true;
int startIndex = centerIndex;
int distance = CalculateShortestDistance(startIndex, targetIndex);
// 根据距离决定跳转方式
int maxStepsForAnimation = 5; // 超过5步就用平滑跳转
if (Mathf.Abs(distance) <= maxStepsForAnimation)
{
// 距离较短,使用逐步移动
yield return StartCoroutine(StepByStepJump(distance));
}
else
{
// 距离较长,使用平滑跳转
float duration = Mathf.Clamp(Mathf.Abs(distance) * 0.1f, 0.5f, 2f); // 限制在0.5-2秒之间
yield return StartCoroutine(SmoothJumpCoroutine(targetIndex, duration));
}
isAnimating = false;
}
/// <summary>
/// 计算最短距离(考虑环形结构)
/// </summary>
int CalculateShortestDistance(int from, int to)
{
int forward = (to - from + dataList.Count) % dataList.Count;
int backward = (from - to + dataList.Count) % dataList.Count;
if (forward <= backward)
{
return forward;
}
else
{
return -backward;
}
}
/// <summary>
/// 逐步跳转(用于短距离)
/// </summary>
System.Collections.IEnumerator StepByStepJump(int distance)
{
bool moveForward = distance > 0;
int steps = Mathf.Abs(distance);
for (int i = 0; i < steps; i++)
{
if (moveForward)
{
centerIndex = (centerIndex + 1) % dataList.Count;
yield return StartCoroutine(AnimateMoveToNext());
}
else
{
centerIndex = (centerIndex - 1 + dataList.Count) % dataList.Count;
yield return StartCoroutine(AnimateMoveToPrevious());
}
// 步骤间的短暂延迟
if (i < steps - 1) // 最后一步不需要延迟
{
yield return new WaitForSeconds(0.05f);
}
}
}
/// <summary>
/// 平滑跳转到指定索引(优化版)
/// </summary>
/// <param name="targetIndex">目标索引</param>
/// <param name="duration">动画持续时间</param>
public void SmoothJumpToIndex(int targetIndex, float duration = -1f)
{
if (targetIndex < 0 || targetIndex >= dataList.Count || isAnimating) return;
// 如果没有指定时间,根据距离自动计算
if (duration < 0)
{
int distance = Mathf.Abs(CalculateShortestDistance(centerIndex, targetIndex));
duration = Mathf.Clamp(distance * 0.08f, 0.3f, 1.5f); // 自动计算合适的时间
}
StartCoroutine(SmoothJumpCoroutine(targetIndex, duration));
}
/// <summary>
/// 优化的平滑跳转协程
/// </summary>
System.Collections.IEnumerator SmoothJumpCoroutine(int targetIndex, float duration)
{
if (isAnimating || targetIndex == centerIndex) yield break;
isAnimating = true;
int startIndex = centerIndex;
int totalDistance = CalculateShortestDistance(startIndex, targetIndex);
bool moveForward = totalDistance > 0;
int steps = Mathf.Abs(totalDistance);
float elapsed = 0f;
int lastProcessedStep = 0;
while (elapsed < duration && lastProcessedStep < steps)
{
float t = elapsed / duration;
float smoothT = moveCurve.Evaluate(t);
// 计算当前应该完成的步数
int currentStep = Mathf.RoundToInt(smoothT * steps);
// 如果需要移动到下一步
if (currentStep > lastProcessedStep)
{
int stepsToMove = currentStep - lastProcessedStep;
for (int i = 0; i < stepsToMove; i++)
{
if (moveForward)
{
centerIndex = (centerIndex + 1) % dataList.Count;
}
else
{
centerIndex = (centerIndex - 1 + dataList.Count) % dataList.Count;
}
}
UpdateAllItems();
lastProcessedStep = currentStep;
}
elapsed += Time.deltaTime;
yield return null;
}
// 确保最终到达目标位置
centerIndex = targetIndex;
UpdateAllItems();
isAnimating = false;
}
#region 拖拽处理
public void OnBeginDrag(PointerEventData eventData)
{
if (isAnimating) return;
isDragging = true;
dragStartPos = eventData.position;
dragStartTime = Time.time;
}
public void OnDrag(PointerEventData eventData)
{
if (!isDragging || isAnimating) return;
Vector2 dragDelta = eventData.position - dragStartPos;
float dragDistance = dragDelta.x;
// 移动所有项目
for (int i = 0; i < uiItems.Count; i++)
{
RectTransform itemRect = uiItems[i].GetComponent<RectTransform>();
Vector2 newPos = new Vector2(positions[i] + dragDistance, 0);
itemRect.anchoredPosition = newPos;
}
}
public void OnEndDrag(PointerEventData eventData)
{
if (!isDragging || isAnimating) return;
isDragging = false;
Vector2 dragDelta = eventData.position - dragStartPos;
float dragDistance = dragDelta.x;
float dragTime = Time.time - dragStartTime;
float dragVelocity = dragDistance / dragTime;
// 判断滑动方向和距离
bool shouldMove = Mathf.Abs(dragDistance) > itemDistance * 0.3f ||
Mathf.Abs(dragVelocity) > 500f;
if (shouldMove)
{
if (dragDistance > 0)
{
// 向右拖拽,显示上一个数据
MoveToPrevious();
}
else
{
// 向左拖拽,显示下一个数据
MoveToNext();
}
}
else
{
// 回弹到原位置
StartCoroutine(AnimateToOriginalPositions());
}
}
/// <summary>
/// 回弹到原位置
/// </summary>
System.Collections.IEnumerator AnimateToOriginalPositions()
{
isAnimating = true;
List<Vector2> startPositions = new List<Vector2>();
List<Vector2> targetPositions = new List<Vector2>();
for (int i = 0; i < uiItems.Count; i++)
{
RectTransform itemRect = uiItems[i].GetComponent<RectTransform>();
startPositions.Add(itemRect.anchoredPosition);
targetPositions.Add(new Vector2(positions[i], 0));
}
float duration = 0.3f;
float elapsed = 0f;
while (elapsed < duration)
{
float t = elapsed / duration;
float curveT = moveCurve.Evaluate(t);
for (int i = 0; i < uiItems.Count; i++)
{
RectTransform itemRect = uiItems[i].GetComponent<RectTransform>();
Vector2 pos = Vector2.Lerp(startPositions[i], targetPositions[i], curveT);
itemRect.anchoredPosition = pos;
}
elapsed += Time.deltaTime;
yield return null;
}
// 确保最终位置正确
for (int i = 0; i < uiItems.Count; i++)
{
RectTransform itemRect = uiItems[i].GetComponent<RectTransform>();
itemRect.anchoredPosition = targetPositions[i];
}
isAnimating = false;
}
#endregion
/// <summary>
/// 获取当前中心项的数据索引
/// </summary>
public int GetCurrentCenterIndex()
{
return centerIndex;
}
/// <summary>
/// 获取当前中心项的数据
/// </summary>
public string GetCurrentCenterData()
{
return dataList[centerIndex];
}
/// <summary>
/// OnGUI测试界面
/// </summary>
void OnGUI()
{
if (!Application.isPlaying) return;
// 主测试面板
GUILayout.BeginArea(new Rect(10, 10, 300, 400));
GUILayout.BeginVertical("box");
// 标题和状态
GUILayout.Label("无限循环列表测试", GUI.skin.box);
GUILayout.Label($"当前: 索引{centerIndex} | {dataList[centerIndex]}");
if (isAnimating) GUILayout.Label("⏳ 动画中...", GUI.skin.box);
GUILayout.Space(5);
// 基本移动
GUILayout.BeginHorizontal();
if (GUILayout.Button("← 上一个", GUILayout.Height(30)))
MoveToPrevious();
if (GUILayout.Button("下一个 →", GUILayout.Height(30)))
MoveToNext();
GUILayout.EndHorizontal();
GUILayout.Space(10);
// 跳转测试
GUILayout.Label("跳转测试:");
// 近距离跳转(逐步动画)
GUILayout.Label("近距离跳转(逐步):");
GUILayout.BeginHorizontal();
if (GUILayout.Button("±1")) JumpToIndex((centerIndex + 1) % dataList.Count, true);
if (GUILayout.Button("±2")) JumpToIndex((centerIndex + 2) % dataList.Count, true);
if (GUILayout.Button("±3")) JumpToIndex((centerIndex + 3) % dataList.Count, true);
GUILayout.EndHorizontal();
// 远距离跳转(平滑动画)
GUILayout.Label("远距离跳转(平滑):");
GUILayout.BeginHorizontal();
if (GUILayout.Button("±8")) JumpToIndex((centerIndex + 8) % dataList.Count, true);
if (GUILayout.Button("±10")) JumpToIndex((centerIndex + 10) % dataList.Count, true);
if (GUILayout.Button("对面")) JumpToIndex((centerIndex + 10) % dataList.Count, true);
GUILayout.EndHorizontal();
// 直接跳转(无动画)
GUILayout.Label("直接跳转(无动画):");
GUILayout.BeginHorizontal();
if (GUILayout.Button("0")) JumpToIndex(0, false);
if (GUILayout.Button("5")) JumpToIndex(5, false);
if (GUILayout.Button("10")) JumpToIndex(10, false);
if (GUILayout.Button("15")) JumpToIndex(15, false);
if (GUILayout.Button("19")) JumpToIndex(19, false);
GUILayout.EndHorizontal();
GUILayout.Space(10);
// 测试说明
GUILayout.Label("说明:", GUI.skin.box);
GUILayout.Label("• ≤5步: 逐步移动动画");
GUILayout.Label("• >5步: 平滑跳转动画");
GUILayout.Label("• 时间根据距离自动调整");
GUILayout.Label("• 最长不超过2秒");
GUILayout.Space(5);
// 自动测试
if (GUILayout.Button("自动测试各种距离", GUILayout.Height(25)))
{
StartCoroutine(TestVariousDistances());
}
GUILayout.EndVertical();
GUILayout.EndArea();
// 详细信息面板(可选显示)
if (showDetailPanel)
{
ShowDetailPanel();
}
// 切换详细面板的按钮
if (GUI.Button(new Rect(320, 10, 80, 25), showDetailPanel ? "隐藏详情" : "显示详情"))
{
showDetailPanel = !showDetailPanel;
}
}
/// <summary>
/// 显示详细信息面板
/// </summary>
void ShowDetailPanel()
{
GUILayout.BeginArea(new Rect(410, 10, 300, 400));
GUILayout.BeginVertical("box");
GUILayout.Label("详细状态信息", GUI.skin.box);
// UI状态详情
GUILayout.Label("UI对象状态:");
for (int i = 0; i < uiItems.Count && i < 5; i++)
{
if (uiItems[i] != null)
{
int dataIndex = uiItems[i].GetDataIndex();
RectTransform rect = uiItems[i].GetComponent<RectTransform>();
float xPos = rect.anchoredPosition.x;
string visibility = "隐藏";
if (i == 1) visibility = "左";
else if (i == 2) visibility = "中";
else if (i == 3) visibility = "右";
GUILayout.Label($"UI[{i}]: 数据{dataIndex} | X:{xPos:F0} | {visibility}");
}
}
GUILayout.Space(10);
// 操作说明
GUILayout.Label("操作说明:", GUI.skin.box);
GUILayout.Label("• 拖拽: 左拖显示下一个,右拖显示上一个");
GUILayout.Label("• 动画: 中间UI有动画,首尾交换无动画");
GUILayout.Label("• 跳转: 可选择有无动画效果");
GUILayout.EndVertical();
GUILayout.EndArea();
}
/// <summary>
/// 测试各种距离的跳转
/// </summary>
System.Collections.IEnumerator TestVariousDistances()
{
// 测试短距离(逐步)
JumpToIndex(0, false);
yield return new WaitForSeconds(0.5f);
JumpToIndex(2, true); // 2步
yield return new WaitForSeconds(2f);
JumpToIndex(7, true); // 5步
yield return new WaitForSeconds(3f);
// 测试长距离(平滑)
JumpToIndex(17, true); // 10步,会用平滑跳转
yield return new WaitForSeconds(2f);
JumpToIndex(3, true); // 跨越首尾,会选择最短路径
yield return new WaitForSeconds(2f);
// 回到起点
JumpToIndex(0, true);
}
}
LoopItem.cs
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 无限循环列表的单个项目组件
/// </summary>
public class LoopItem : MonoBehaviour
{
[Header("UI组件")]
public Text itemText; // 显示文本的组件
public Image backgroundImage; // 背景图像组件
public Button itemButton; // 按钮组件(可选)
// 数据相关
private int dataIndex; // 当前显示的数据索引
private string itemData; // 当前显示的数据内容
private CanvasGroup canvasGroup; // 用于控制透明度
void Awake()
{
InitializeComponents();
SetupButton();
}
/// <summary>
/// 初始化组件引用
/// </summary>
void InitializeComponents()
{
// 自动获取组件(如果没有手动指定)
if (itemText == null)
itemText = GetComponentInChildren<Text>();
if (backgroundImage == null)
backgroundImage = GetComponent<Image>();
if (itemButton == null)
itemButton = GetComponent<Button>();
// 获取或添加CanvasGroup用于控制透明度
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
}
/// <summary>
/// 设置按钮事件
/// </summary>
void SetupButton()
{
if (itemButton != null)
{
itemButton.onClick.RemoveAllListeners();
itemButton.onClick.AddListener(OnItemClick);
}
}
/// <summary>
/// 设置数据内容
/// </summary>
/// <param name="data">要显示的数据</param>
/// <param name="index">数据索引</param>
public void SetData(string data, int index)
{
itemData = data;
dataIndex = index;
// 更新文本显示
if (itemText != null)
{
itemText.text = data;
}
// 设置样式
SetItemStyle(index);
// 更新按钮交互状态
UpdateInteractable();
}
/// <summary>
/// 根据索引设置项目样式
/// </summary>
/// <param name="index">数据索引</param>
void SetItemStyle(int index)
{
if (backgroundImage != null)
{
// 根据索引设置不同颜色,便于区分
Color baseColor = GetColorByIndex(index);
backgroundImage.color = new Color(baseColor.r, baseColor.g, baseColor.b, 0.8f);
}
// 可以根据需要添加更多样式设置
SetTextStyle(index);
}
/// <summary>
/// 根据索引获取颜色
/// </summary>
/// <param name="index">数据索引</param>
/// <returns>对应的颜色</returns>
Color GetColorByIndex(int index)
{
Color[] colors = {
Color.red, // 0
Color.green, // 1
Color.blue, // 2
Color.yellow, // 3
Color.cyan, // 4
Color.magenta, // 5
new Color(1f, 0.5f, 0f), // 橙色 6
new Color(0.5f, 0f, 1f), // 紫色 7
new Color(0f, 1f, 0.5f), // 青绿色 8
new Color(1f, 0f, 0.5f) // 粉红色 9
};
return colors[index % colors.Length];
}
/// <summary>
/// 设置文本样式
/// </summary>
/// <param name="index">数据索引</param>
void SetTextStyle(int index)
{
if (itemText != null)
{
// 可以根据索引设置不同的文本样式
itemText.color = Color.white;
itemText.fontSize = 16;
// 示例:每5个一组,设置不同的字体大小
if (index % 5 == 0)
{
itemText.fontSize = 18;
itemText.fontStyle = FontStyle.Bold;
}
else
{
itemText.fontSize = 16;
itemText.fontStyle = FontStyle.Normal;
}
}
}
/// <summary>
/// 设置可见性(用于显示/隐藏非中心项目)
/// </summary>
/// <param name="visible">是否可见</param>
public void SetVisible(bool visible)
{
if (canvasGroup != null)
{
// 可见项目完全不透明,隐藏项目半透明
canvasGroup.alpha = visible ? 1f : 0.3f;
// 可选:完全禁用隐藏项目的交互
canvasGroup.interactable = visible;
canvasGroup.blocksRaycasts = visible;
}
else
{
// 备用方案:直接控制GameObject激活状态
gameObject.SetActive(visible);
}
}
/// <summary>
/// 设置高亮状态(用于标识中心项目)
/// </summary>
/// <param name="highlighted">是否高亮</param>
public void SetHighlighted(bool highlighted)
{
if (backgroundImage != null)
{
if (highlighted)
{
// 高亮时边框或阴影效果
backgroundImage.color = Color.white;
// 可以添加缩放效果
transform.localScale = Vector3.one * 1.1f;
}
else
{
// 恢复正常颜色
Color normalColor = GetColorByIndex(dataIndex);
backgroundImage.color = new Color(normalColor.r, normalColor.g, normalColor.b, 0.8f);
// 恢复正常大小
transform.localScale = Vector3.one;
}
}
}
/// <summary>
/// 更新交互状态
/// </summary>
void UpdateInteractable()
{
if (itemButton != null)
{
// 确保按钮在有数据时可交互
itemButton.interactable = !string.IsNullOrEmpty(itemData);
}
}
/// <summary>
/// 项目点击事件处理
/// </summary>
void OnItemClick()
{
Debug.Log($"点击了项目: {itemData} (索引: {dataIndex})");
// 尝试获取父级的无限循环列表组件
InfiniteLoopList parentList = GetComponentInParent<InfiniteLoopList>();
if (parentList != null)
{
// 如果点击的不是中心项,跳转到该项
if (dataIndex != parentList.GetCurrentCenterIndex())
{
parentList.JumpToIndex(dataIndex, true); // 带动画跳转
}
else
{
// 如果点击的是中心项,可以执行其他逻辑
Debug.Log($"中心项被点击: {itemData}");
OnCenterItemClick();
}
}
// 发送自定义事件(可选)
SendItemClickEvent();
}
/// <summary>
/// 中心项目被点击时的处理
/// </summary>
void OnCenterItemClick()
{
// 可以在这里添加中心项目特有的点击逻辑
// 例如:播放特殊动画、打开详情界面等
// 示例:简单的缩放动画
StartCoroutine(PlayClickAnimation());
}
/// <summary>
/// 播放点击动画
/// </summary>
System.Collections.IEnumerator PlayClickAnimation()
{
Vector3 originalScale = transform.localScale;
Vector3 targetScale = originalScale * 1.2f;
// 放大
float duration = 0.1f;
float elapsed = 0f;
while (elapsed < duration)
{
float t = elapsed / duration;
transform.localScale = Vector3.Lerp(originalScale, targetScale, t);
elapsed += Time.deltaTime;
yield return null;
}
// 缩小回原始大小
elapsed = 0f;
while (elapsed < duration)
{
float t = elapsed / duration;
transform.localScale = Vector3.Lerp(targetScale, originalScale, t);
elapsed += Time.deltaTime;
yield return null;
}
transform.localScale = originalScale;
}
/// <summary>
/// 发送项目点击事件
/// </summary>
void SendItemClickEvent()
{
// 可以使用事件系统或其他方式通知其他组件
// 例如:EventManager.TriggerEvent("ItemClicked", dataIndex);
// 或者使用Unity的事件系统
var eventData = new ItemClickEventData
{
itemData = this.itemData,
dataIndex = this.dataIndex,
clickedItem = this
};
// 向上传递事件
SendMessageUpwards("OnLoopItemClicked", eventData, SendMessageOptions.DontRequireReceiver);
}
/// <summary>
/// 获取当前显示的数据
/// </summary>
/// <returns>当前数据内容</returns>
public string GetData()
{
return itemData;
}
/// <summary>
/// 获取当前数据索引
/// </summary>
/// <returns>当前数据索引</returns>
public int GetDataIndex()
{
return dataIndex;
}
/// <summary>
/// 检查是否有有效数据
/// </summary>
/// <returns>是否有有效数据</returns>
public bool HasValidData()
{
return !string.IsNullOrEmpty(itemData);
}
/// <summary>
/// 重置项目状态
/// </summary>
public void ResetItem()
{
itemData = string.Empty;
dataIndex = -1;
if (itemText != null)
{
itemText.text = string.Empty;
}
if (backgroundImage != null)
{
backgroundImage.color = Color.white;
}
transform.localScale = Vector3.one;
SetVisible(false);
}
/// <summary>
/// 应用自定义数据(支持泛型扩展)
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="data">数据对象</param>
/// <param name="index">索引</param>
public void SetCustomData<T>(T data, int index)
{
dataIndex = index;
// 将泛型数据转换为字符串显示
if (data != null)
{
itemData = data.ToString();
}
else
{
itemData = "Null";
}
if (itemText != null)
{
itemText.text = itemData;
}
SetItemStyle(index);
UpdateInteractable();
}
#region Unity生命周期
void OnDestroy()
{
// 清理事件监听
if (itemButton != null)
{
itemButton.onClick.RemoveAllListeners();
}
}
void OnValidate()
{
// 在编辑器中验证组件设置
if (Application.isPlaying) return;
if (itemText == null)
itemText = GetComponentInChildren<Text>();
if (backgroundImage == null)
backgroundImage = GetComponent<Image>();
if (itemButton == null)
itemButton = GetComponent<Button>();
}
#endregion
}
/// <summary>
/// 项目点击事件数据
/// </summary>
[System.Serializable]
public class ItemClickEventData
{
public string itemData; // 项目数据
public int dataIndex; // 数据索引
public LoopItem clickedItem; // 被点击的项目组件
}