1. 项目概述:为什么我们需要一个可折叠的多级菜单?
在Unity的UGUI开发中,菜单系统是几乎所有交互密集型应用(无论是游戏还是工具软件)的基石。一个结构清晰、交互流畅的菜单,能极大提升用户体验和操作效率。然而,当菜单层级变深、内容变多时,开发者常常会面临几个头疼的问题:界面布局混乱、滚动卡顿、动态内容高度计算不准,以及代码维护困难。
传统的做法可能是为每个菜单项预制一个固定的RectTransform,手动计算位置,或者使用多个嵌套的ScrollRect。前者在面对动态增减的菜单项时几乎是一场灾难,后者则会导致滚动逻辑冲突和性能开销。而Unity自带的ScrollRect和ContentSizeFitter组件,如果运用得当,可以优雅地解决这些问题。它们一个负责处理滚动视图,一个负责动态调整内容容器的大小,是天生的搭档。
这个项目,就是要彻底搞懂如何将这两个核心组件,结合Unity的预制体(Prefab)系统、布局组(Layout Group)以及事件系统,串联起来,打造一个 高性能、易维护、可无限扩展 的可折叠多级菜单。它不仅仅是“能用”,更要“好用”和“耐用”。无论你是想为你的游戏制作一个复杂的技能树、一个庞大的背包系统,还是一个企业级工具软件的设置面板,这套方案都能提供坚实的底层支持。
2. 核心组件深度解析:ScrollRect与ContentSizeFitter的协同原理
在动手之前,我们必须吃透这两个组件的“脾气秉性”,知道它们各自擅长什么,以及如何让它们默契配合。
2.1 ScrollRect:不只是“滚动条”
很多开发者对ScrollRect的理解停留在“一个带滚动条的框”,这大大低估了它的能力。ScrollRect的核心是一个“视口”(Viewport)和一个“内容区域”(Content)。视口决定了你能看到多大范围,而内容区域则是实际承载所有子物体的容器。
关键属性与工作流:
- Viewport: 通常就是ScrollRect自身或一个指定的子物体。它通过
Mask或RectMask2D组件来裁剪超出范围的内容。 强烈建议使用RectMask2D,它在性能上优于传统的Mask,尤其对于包含大量UI元素的滚动列表。 - Content: 这是所有菜单项的父级Transform。它的锚点(Anchors)和轴心(Pivot)设置至关重要,直接决定了滚动方向和内容对齐方式。
- Movement Type: 滚动类型。对于菜单,
Clamped(弹性限制)或Elastic(带弹性效果的)通常比Unrestricted(无限制)更符合直觉,能防止内容被完全拖出视野。 - Inertia: 惯性。开启后,滚动会有滑行效果,手感更自然,但对于需要精准操作的菜单,可以考虑关闭。
一个常被忽略的细节: ScrollRect的滚动能力依赖于Content的尺寸 大于 Viewport的尺寸。如果Content的高度(对于垂直滚动)和Viewport一样或更小,滚动条就不会出现。这就是为什么我们需要ContentSizeFitter来动态计算Content的真实尺寸。
2.2 ContentSizeFitter:动态尺寸的“计算器”
ContentSizeFitter是自动布局(Auto Layout)系统的一部分。它根据其子物体的布局,自动调整RectTransform的宽高。
核心模式:
- Horizontal/Vertical Fit:
-
Unconstrained: 不约束,手动设置。 -
MinSize: 尺寸调整为能容纳所有子物体的最小尺寸。 -
PreferredSize: 尺寸调整为子物体“偏好尺寸”(Preferred Size)的总和。这是 实现折叠/展开动画的关键 。
-
为什么是 PreferredSize ? 因为UI元素(如 LayoutElement )可以设置 Preferred Height 。当我们折叠一个菜单项时,可以将其 Preferred Height 设置为0;展开时,再设置回其原始高度或计算出的所需高度。ContentSizeFitter检测到子物体偏好尺寸的变化,就会自动重新计算并调整Content的整体尺寸,从而触发ScrollRect的重新布局和滚动范围的更新。
协同工作流:
- 用户点击折叠按钮。
- 脚本将该菜单项下所有子内容(次级菜单)的
Preferred Height设置为0(或通过SetActive(false)并配合LayoutElement)。 - ContentSizeFitter组件检测到子物体尺寸变化。
- ContentSizeFitter重新计算Content容器的
Preferred Height,并应用新的高度值。 - ScrollRect察觉到Content的尺寸发生了变化,立即更新滚动区域和滚动条的位置。
- 整个过程可以配合
Canvas.ForceUpdateCanvases()(谨慎使用)或LayoutRebuilder.ForceRebuildLayoutImmediate来立即强制刷新布局,实现平滑的动画效果。
注意: 过度频繁地强制重建布局(
ForceRebuildLayoutImmediate)是性能杀手,尤其是在一帧内多次操作。最佳实践是在一帧内完成所有尺寸变更后,调用一次重建,或者使用LayoutGroup


221

被折叠的 条评论
为什么被折叠?



