文章目录
-
- [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(水平排列) ContentSizeFitter的Horizontal 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. 使用方法
-
Hierarchy 右键 → UI → Scroll View 自动创建完整层级。
-
删除不需要的 Scrollbar(如只需垂直滚动,删除 Horizontal Scrollbar)。
-
配置 Content:
- 添加
VerticalLayoutGroup(Spacing = 10) - 添加
ContentSizeFitter(Vertical Fit = Preferred Size) - 设置 Pivot = (0.5, 1)(顶部对齐,最关键的一步)
- 添加
-
创建列表项预制体(Item Prefab),设好固定高度。
-
将预制体实例化到 Content 下,或在 Inspector 中手动添加子对象。
-
挂载
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 样式:
- Handle 的 Image 替换为自定义滑块图片(圆角矩形/胶囊形)
- Background 的 Image 替换为轨道图片(或设为透明)
- 调整 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只影响鼠标滚轮,不影响触摸。触摸灵敏度由拖拽距离直接决定,无需额外配置。