【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 只影响鼠标滚轮,不影响触摸。触摸灵敏度由拖拽距离直接决定,无需额外配置。
相关推荐
mxwin18 小时前
unity shader中 ddx ddy是什么
unity·游戏引擎·shader
郝学胜-神的一滴20 小时前
[简化版 GAMES 101] 计算机图形学 08:三角形光栅化上
c++·unity·游戏引擎·godot·图形渲染·opengl·unreal
nnsix21 小时前
Unity ILRuntime 笔记
unity·游戏引擎
nnsix1 天前
Unity API 兼容的 .NET Standard 2.1 和 .NET Framework 区别
unity·游戏引擎·.net
mxwin1 天前
Unity Shader 制作半透明物体 使用多Pass提前写入深度的方式 避免穿模
unity·游戏引擎
nnsix1 天前
Unity HybridCLR 笔记
笔记·unity·游戏引擎
nnsix1 天前
Unity Addressables 笔记
unity·游戏引擎
RReality1 天前
【Unity Shader URP】视差贴图 实战教程
ui·平面·unity·游戏引擎·图形渲染·贴图
小清兔2 天前
Addressable的设置打包流程
笔记·游戏·unity·c#
3D霸霸2 天前
Sourcetree 拉取新工程
数据仓库·unity