Unity UGUI 无限循环列表组件

发布于:2025-07-17 ⋅ 阅读:(18) ⋅ 点赞:(0)

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());

扩展建议

自定义列表项

  1. 继承 LoopItem
  2. 重写 SetData 方法
  3. 添加自定义UI元素和逻辑

添加更多动画效果

  1. 修改 moveCurve 参数
  2. 在动画协程中添加缩放、旋转等效果
  3. 支持不同方向的滚动(垂直滚动)

数据绑定

  1. 实现数据源接口
  2. 支持动态添加/删除数据
  3. 添加数据变化通知

注意事项

  1. Mask组件:确保Container有Mask组件用于遮挡
  2. 预制体设置:Item预制体必须有Text组件
  3. 性能考虑:数据量很大时考虑异步加载
  4. 内存管理:及时清理不需要的数据引用
  5. 动画冲突:避免在动画进行时调用跳转方法

常见问题

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; // 被点击的项目组件
}

网站公告

今日签到

点亮在社区的每一天
去签到