Unity UGUI 之 ScrollBar与ScrollView

本文仅作学习笔记与交流,不作任何商业用途

本文包括但不限于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);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哈基咩咩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值