【Unity UGUI】ScrollRect 与 Scrollbar 深度用法

文章目录

    • [0. 效果预览](#0. 效果预览)
    • [1. 需求分析](#1. 需求分析)
    • [2. Hierarchy 搭建](#2. Hierarchy 搭建)
    • [3. 核心组件配置](#3. 核心组件配置)
      • [ScrollRect 参数](#ScrollRect 参数)
      • [Content 配置要点](#Content 配置要点)
      • [Scrollbar 参数](#Scrollbar 参数)
    • [4. 完整代码](#4. 完整代码)
      • [4.1 代码控制滚动位置](#4.1 代码控制滚动位置)
      • [4.2 动态添加列表项](#4.2 动态添加列表项)
      • [4.3 嵌套 ScrollRect 冲突处理](#4.3 嵌套 ScrollRect 冲突处理)
    • [5. 使用方法](#5. 使用方法)
    • [6. 参数说明](#6. 参数说明)
      • [ScrollController 脚本参数](#ScrollController 脚本参数)
      • [DynamicListExample 脚本参数](#DynamicListExample 脚本参数)
    • [7. 变体与扩展](#7. 变体与扩展)
      • [7.1 Scrollbar 自动隐藏与自定义样式](#7.1 Scrollbar 自动隐藏与自定义样式)
      • [7.2 弹性回弹自定义](#7.2 弹性回弹自定义)
      • [7.3 滚动事件监听](#7.3 滚动事件监听)
    • [8. 常见问题](#8. 常见问题)
    • [9. 性能 / 适配建议](#9. 性能 / 适配建议)

0. 效果预览

ScrollRect 是 UGUI 中最核心的滚动容器:背包列表、聊天记录、设置面板、排行榜------几乎所有需要滚动的 UI 都靠它。配合 Scrollbar 做滚动条指示,再加上 Layout 组件自动排列子元素,就是一套完整的滚动列表方案。但嵌套滚动冲突、Layout 联动陷阱、弹性回弹自定义这些坑,不提前了解会浪费大量调试时间。


1. 需求分析

核心思路:ScrollRect = 一个可拖拽的 Content 容器 + Viewport 裁剪区域 + 可选的 Scrollbar 联动。Content 的大小决定可滚动范围,Viewport 决定可见区域。

典型使用场景:

  • 背包 / 道具列表(垂直滚动 + GridLayout)
  • 聊天记录(垂直滚动 + 自动滚到底部)
  • 设置面板(垂直滚动 + 多种控件混排)
  • 横向轮播 / 页签切换(水平滚动)
  • 排行榜 / 好友列表

需要实现的功能点:

  • ScrollRect 基础配置(水平/垂直滚动)
  • Viewport + Mask/RectMask2D 裁剪
  • Content 配合 Layout 组件自动排列
  • Scrollbar 联动与自定义样式
  • 弹性回弹(Elastic)与惯性(Inertia)调节
  • 代码控制滚动位置(滚到顶部/底部/指定项)
  • 嵌套 ScrollRect 冲突处理

前置知识:建议先阅读本系列 文章 1(总领篇)文章 2(细节篇),了解 Canvas、RectTransform、Rebuild/Rebatch 机制。


2. Hierarchy 搭建

基础垂直滚动列表

复制代码
Canvas
└── ScrollView
    ├── Viewport(RectMask2D)          ← 裁剪区域
    │   └── Content(VerticalLayoutGroup + ContentSizeFitter)
    │       ├── Item_01
    │       ├── Item_02
    │       ├── Item_03
    │       └── ...
    ├── Scrollbar Vertical(Scrollbar)  ← 垂直滚动条(可选)
    │   └── Sliding Area
    │       └── Handle(Image)          ← 滚动条滑块
    └── Scrollbar Horizontal(Scrollbar)← 水平滚动条(可选)
  • ScrollView :父对象,挂 ScrollRect 组件 + Image(背景,可选)
  • Viewport :挂 RectMask2D(推荐)或 Mask + Image,裁剪超出区域的内容
  • Content :实际承载子元素的容器,挂 VerticalLayoutGroup(垂直排列)+ ContentSizeFitter(自动撑开高度)
  • Scrollbar :滚动条,ScrollRect 的 Vertical Scrollbar 属性指向它

快捷创建 :Hierarchy 右键 → UI → Scroll View 会自动生成完整的层级结构(含 Viewport、Content、两个 Scrollbar),省去手动搭建。

水平滚动列表

和垂直列表结构相同,区别:

  • Content 挂 HorizontalLayoutGroup(水平排列)
  • ContentSizeFitterHorizontal Fit 设为 Preferred Size
  • ScrollRect 勾选 Horizontal,取消 Vertical

3. 核心组件配置

ScrollRect 参数

参数 说明
Content 指向 Content 对象的 RectTransform
Horizontal 是否允许水平滚动
Vertical 是否允许垂直滚动
Movement Type 滚动到边界时的行为:Unrestricted(无限制)/ Elastic(弹性回弹)/ Clamped(硬停)
Elasticity 弹性回弹的强度(仅 Elastic 模式,默认 0.1,越小回弹越慢)
Inertia 是否启用惯性(松手后继续滑动)
Deceleration Rate 惯性减速率(0=立即停止,1=永不停止,默认 0.135)
Scroll Sensitivity 鼠标滚轮灵敏度
Viewport 指向 Viewport 的 RectTransform
Horizontal Scrollbar / Vertical Scrollbar 指向对应的 Scrollbar 组件
Scrollbar Visibility 滚动条显示模式:Permanent(常驻)/ Auto Hide(自动隐藏)/ Auto Hide And Expand Viewport(隐藏时扩展 Viewport)

Content 配置要点

组件 关键参数 说明
VerticalLayoutGroup Spacing = 10, Child Alignment = Upper Center 子元素垂直排列,间距 10
ContentSizeFitter Vertical Fit = Preferred Size Content 高度自动撑开,等于所有子元素高度之和
RectTransform Pivot = (0.5, 1), Anchors = Top-Stretch Pivot 必须在顶部,否则添加子元素时 Content 会向下偏移

关键坑点:Content 的 Pivot 设错是最常见的 ScrollRect 问题。垂直滚动时 Pivot.y 必须为 1(顶部),水平滚动时 Pivot.x 必须为 0(左侧)。否则 ContentSizeFitter 撑开 Content 时,内容会向错误方向偏移。

Scrollbar 参数

参数 说明
Handle Rect 指向滑块(Handle)的 RectTransform
Direction 滚动方向:Bottom To Top / Top To Bottom / Left To Right / Right To Left
Value 当前滚动位置(0~1)
Size 滑块大小(0~1,表示可见区域占总内容的比例,ScrollRect 会自动设置)

4. 完整代码

4.1 代码控制滚动位置

csharp 复制代码
using UnityEngine;
using UnityEngine.UI;
using System.Collections;

/// <summary>
/// ScrollRect 滚动位置控制
/// 挂载到 ScrollView 对象上
/// </summary>
public class ScrollController : MonoBehaviour
{
    [SerializeField] private ScrollRect _scrollRect;

    // ===== 滚动到顶部 =====
    public void ScrollToTop()
    {
        // verticalNormalizedPosition: 1=顶部, 0=底部
        _scrollRect.verticalNormalizedPosition = 1f;
    }

    // ===== 滚动到底部 =====
    public void ScrollToBottom()
    {
        _scrollRect.verticalNormalizedPosition = 0f;
    }

    // ===== 平滑滚动到底部(协程) =====
    public void SmoothScrollToBottom()
    {
        StartCoroutine(SmoothScrollCoroutine(0f, 0.3f));
    }

    // ===== 平滑滚动到顶部(协程) =====
    public void SmoothScrollToTop()
    {
        StartCoroutine(SmoothScrollCoroutine(1f, 0.3f));
    }

    private IEnumerator SmoothScrollCoroutine(float targetPos, float duration)
    {
        float startPos = _scrollRect.verticalNormalizedPosition;
        float elapsed = 0f;

        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            float t = Mathf.SmoothStep(0f, 1f, elapsed / duration);
            _scrollRect.verticalNormalizedPosition = Mathf.Lerp(startPos, targetPos, t);
            yield return null;
        }

        _scrollRect.verticalNormalizedPosition = targetPos;
    }

    // ===== 滚动到指定子元素 =====
    public void ScrollToChild(RectTransform child)
    {
        // 计算子元素在 Content 中的归一化位置
        RectTransform content = _scrollRect.content;
        float contentHeight = content.rect.height;
        float viewportHeight = _scrollRect.viewport.rect.height;
        float scrollRange = contentHeight - viewportHeight;

        if (scrollRange <= 0) return;  // 内容不够长,无需滚动

        // 子元素顶部相对 Content 顶部的距离
        float childTop = Mathf.Abs(child.anchoredPosition.y);
        float normalizedPos = 1f - (childTop / scrollRange);
        normalizedPos = Mathf.Clamp01(normalizedPos);

        _scrollRect.verticalNormalizedPosition = normalizedPos;
    }
}

4.2 动态添加列表项

csharp 复制代码
using UnityEngine;
using UnityEngine.UI;
using TMPro;

/// <summary>
/// 动态添加/删除列表项
/// </summary>
public class DynamicListExample : MonoBehaviour
{
    [SerializeField] private ScrollRect _scrollRect;
    [SerializeField] private GameObject _itemPrefab;  // 列表项预制体
    [SerializeField] private Transform _content;       // Content 容器

    // ===== 添加一条列表项 =====
    public void AddItem(string text)
    {
        GameObject item = Instantiate(_itemPrefab, _content);
        // 设置文字
        TMP_Text label = item.GetComponentInChildren<TMP_Text>();
        if (label != null) label.text = text;

        // 添加后等一帧再滚到底部(等 Layout 重建完成)
        StartCoroutine(ScrollToBottomNextFrame());
    }

    // ===== 清空所有列表项 =====
    public void ClearAll()
    {
        for (int i = _content.childCount - 1; i >= 0; i--)
        {
            Destroy(_content.GetChild(i).gameObject);
        }
    }

    private System.Collections.IEnumerator ScrollToBottomNextFrame()
    {
        // 等一帧,让 ContentSizeFitter 重新计算 Content 高度
        yield return null;
        // 强制立即重建 Layout
        LayoutRebuilder.ForceRebuildLayoutImmediate(_content as RectTransform);
        _scrollRect.verticalNormalizedPosition = 0f;
    }
}

4.3 嵌套 ScrollRect 冲突处理

csharp 复制代码
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

/// <summary>
/// 嵌套 ScrollRect 冲突处理
/// 挂载到内层 ScrollRect 对象上
/// 当内层滚动到边界时,自动把事件传递给外层
/// </summary>
public class NestedScrollRect : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [SerializeField] private ScrollRect _parentScrollRect;  // 外层 ScrollRect
    private ScrollRect _selfScrollRect;
    private bool _isDraggingParent;

    void Awake()
    {
        _selfScrollRect = GetComponent<ScrollRect>();
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        // 判断拖拽方向是否和自身滚动方向一致
        // 如果不一致(如自身水平滚动,用户垂直拖拽),交给父级处理
        _isDraggingParent = ShouldPassToParent(eventData);

        if (_isDraggingParent)
        {
            _parentScrollRect.OnBeginDrag(eventData);
        }
    }

    public void OnDrag(PointerEventData eventData)
    {
        if (_isDraggingParent)
        {
            _parentScrollRect.OnDrag(eventData);
        }
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        if (_isDraggingParent)
        {
            _parentScrollRect.OnEndDrag(eventData);
        }
        _isDraggingParent = false;
    }

    private bool ShouldPassToParent(PointerEventData eventData)
    {
        // 自身是水平滚动,用户垂直拖拽 → 交给父级
        // 自身是垂直滚动,用户水平拖拽 → 交给父级
        bool isHorizontalDrag = Mathf.Abs(eventData.delta.x) > Mathf.Abs(eventData.delta.y);

        if (_selfScrollRect.horizontal && !_selfScrollRect.vertical)
            return !isHorizontalDrag;  // 自身水平,用户垂直 → 传递
        if (_selfScrollRect.vertical && !_selfScrollRect.horizontal)
            return isHorizontalDrag;   // 自身垂直,用户水平 → 传递

        return false;
    }
}

5. 使用方法

  1. Hierarchy 右键 → UI → Scroll View 自动创建完整层级。

  2. 删除不需要的 Scrollbar(如只需垂直滚动,删除 Horizontal Scrollbar)。

  3. 配置 Content:

    • 添加 VerticalLayoutGroup(Spacing = 10)
    • 添加 ContentSizeFitter(Vertical Fit = Preferred Size)
    • 设置 Pivot = (0.5, 1)(顶部对齐,最关键的一步)
  4. 创建列表项预制体(Item Prefab),设好固定高度。

  5. 将预制体实例化到 Content 下,或在 Inspector 中手动添加子对象。

  6. 挂载 ScrollController 脚本,拖入 ScrollRect 引用。


6. 参数说明

ScrollController 脚本参数

参数 类型 默认值 说明
_scrollRect ScrollRect --- ScrollRect 组件引用

DynamicListExample 脚本参数

参数 类型 默认值 说明
_scrollRect ScrollRect --- ScrollRect 组件引用
_itemPrefab GameObject --- 列表项预制体
_content Transform --- Content 容器引用

7. 变体与扩展

7.1 Scrollbar 自动隐藏与自定义样式

复制代码
ScrollRect 的 Scrollbar Visibility 设为 Auto Hide And Expand Viewport:
- 内容不够长时 Scrollbar 自动隐藏,Viewport 自动扩展填满
- 内容超出时 Scrollbar 自动显示

自定义 Scrollbar 样式:

  1. Handle 的 Image 替换为自定义滑块图片(圆角矩形/胶囊形)
  2. Background 的 Image 替换为轨道图片(或设为透明)
  3. 调整 Handle 的 Min Size(Scrollbar 组件没有直接参数,通过 Handle 的 RectTransform 最小宽/高控制)

7.2 弹性回弹自定义

csharp 复制代码
// 运行时调整弹性参数
scrollRect.movementType = ScrollRect.MovementType.Elastic;
scrollRect.elasticity = 0.05f;  // 更慢的回弹(默认 0.1)

// 关闭弹性,硬停在边界
scrollRect.movementType = ScrollRect.MovementType.Clamped;

// 无限制滚动(内容可以拖出边界不回弹)
scrollRect.movementType = ScrollRect.MovementType.Unrestricted;

7.3 滚动事件监听

csharp 复制代码
// 监听滚动位置变化
scrollRect.onValueChanged.AddListener(OnScrollValueChanged);

private void OnScrollValueChanged(Vector2 normalizedPos)
{
    // normalizedPos.y: 1=顶部, 0=底部
    if (normalizedPos.y <= 0.01f)
    {
        Debug.Log("滚到底部了,可以加载更多");
        // LoadMore();
    }
}

8. 常见问题

Q: Content 添加子元素后滚动范围没变?

A: Content 没挂 ContentSizeFitter,或者 Vertical Fit 没设为 Preferred Size。ContentSizeFitter 负责根据子元素自动撑开 Content 的高度,没有它 Content 高度固定不变,ScrollRect 不知道内容变长了。

Q: 添加子元素后 Content 向下偏移,列表从中间开始?

A: Content 的 Pivot.y 不是 1。垂直滚动列表的 Content Pivot 必须设为 (0.5, 1)(顶部),否则 ContentSizeFitter 撑开高度时会以 Pivot 为中心向两边扩展。

Q: ScrollRect + LayoutGroup 子元素位置错乱?

A: Layout 重建是异步的,在同一帧内 Instantiate 子元素后立即读取位置会拿到旧值。解决:yield return null 等一帧,或调用 LayoutRebuilder.ForceRebuildLayoutImmediate(content) 强制立即重建。

Q: 嵌套 ScrollRect 内层无法滚动 / 外层被劫持?

A: 默认情况下内层 ScrollRect 会吃掉所有拖拽事件。用本文的 NestedScrollRect 脚本,根据拖拽方向判断应该由哪层处理。

Q: Scrollbar 滑块太小 / 消失了?

A: 内容非常长时,Scrollbar 的 Handle Size 会按比例缩小到几乎看不见。可以给 Handle 设一个最小高度:在 Handle 的 RectTransform 上设 Min Height(通过 LayoutElement 组件的 Min Height)。


9. 性能 / 适配建议

  • Mask vs RectMask2D :Viewport 推荐用 RectMask2D 而非 Mask。RectMask2D 不需要额外的 Image 组件,不增加 Draw Call,且裁剪在 Shader 层面完成,性能更好。Mask 使用 Stencil Buffer 裁剪,会打断合批。
  • Layout 重建开销 :每次添加/删除子元素都会触发 Layout Rebuild。大量频繁操作时(如聊天消息刷屏),考虑批量添加后统一调用一次 LayoutRebuilder.ForceRebuildLayoutImmediate,而不是每条消息都触发。
  • 大量列表项:ScrollRect 默认会渲染所有子元素(包括不可见的)。100 个以内没问题,1000+ 个会有明显性能问题。大数据量场景必须用对象池 + 虚拟滚动(只渲染可见项),参考本系列后续文章 11(高性能无限滚动列表)。
  • 惯性与帧率Deceleration Rate 在低帧率设备上表现可能不一致。如果需要精确控制滚动行为,关闭 Inertia 自己用协程实现。
  • 移动端触摸 :ScrollRect 默认支持触摸拖拽,但 Scroll Sensitivity 只影响鼠标滚轮,不影响触摸。触摸灵敏度由拖拽距离直接决定,无需额外配置。
相关推荐
人邮异步社区2 小时前
如何自学游戏引擎的开发?
unity·程序员·游戏引擎
郝学胜-神的一滴3 小时前
[简化版 Games 101] 计算机图形学 05:二维变换下
c++·unity·图形渲染·three.js·opengl·unreal
mxwin16 小时前
Unity URP 热更新兼容性:Shader 在 IL2CPP 打包下的注意事项
unity·游戏引擎
mxwin21 小时前
Unity shader中TransformWorldToShadowCoord原理解析
unity·游戏引擎·shader
mxwin21 小时前
Unity Shader 中 ShadowCaster的作用和疑问
unity·游戏引擎
mxwin1 天前
Unity Shader中如何学习阴影技术 产生阴影,接受阴影,联级阴影,软阴影
学习·unity·游戏引擎·shader
♡すぎ♡1 天前
ShaderLab:线条几何体旋转
unity·计算机图形学·着色器·shaderlab
小贺儿开发1 天前
【MediaPipe】Unity3D 指间游鱼互动演示
游戏·unity·人机交互·摄像头·手势识别·互动·康复训练
mxwin1 天前
Unity Shader中CastShadows 和 ReceiveShadows 在代码中的区分
unity·游戏引擎·shader