Unity UGUI 无限循环列表组件
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, // 0Color.green, // 1Color.blue, // 2Color.yellow, // 3Color.cyan, // 4Color.magenta, // 5new Color(1f, 0.5f, 0f), // 橙色 6new Color(0.5f, 0f, 1f), // 紫色 7new Color(0f, 1f, 0.5f), // 青绿色 8new 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; // 被点击的项目组件
}