大体流程
Unity Docs - UGUI | Class ScrollRect
总的说
自身不负责Rebuild,设置脏之后交由LayoutRebuilder
注册到CanvasUpdateRegistry里待rebuild的集合在固定时机统一Rebuild。自身只在Prelayout和Postlayout做一下数据准备和数据更新
自身的ICanvasElement.Rebuild
是在CanvasUpdateRegistry.PerformUpdate
对集合内的每个合法的(是的会先检验)ICanvasElement
依次调用的
- 根据各种信息计算view和content的边界
- 根据输入(拖动等)更新边界(修改AnchorPosition)
content的移动是这样的流程:
- 通过记录当前光标位置和触发滚动的起始位置的差值计算出Content锚点的
offset
position += offset
SetContentAnchoredPosition(position)
在SetContentAnchoredPosition(position)
中如果限制在垂直或者水平就忽略某个维度的内容,随后把Content的锚点设为position
并使用UpdateBounds
更新
本质上就是通过输入移动Content,位置不动的mask遮罩确保了只显示某一个区域。
开头
csharp
[AddComponentMenu("UI/Scroll Rect", 37)] // 添加到菜单
[SelectionBase]
[ExecuteAlways] // 编辑模式和Play模式都能执行
[DisallowMultipleComponent] // 不允许多个同类脚本
[RequireComponent(typeof(RectTransform))] // 需要具有RectTransform组件
这个[SelectionBase]
就是挂在子物体上,点击子物体时在Hierarchy选中根物体。
Drive & Implementaton
csharp
UIBehaviour, IInitializePotentialDragHandler, IEventSystemHandler, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler, ICanvasElement, ILayoutElement, ILayoutGroup, ILayoutController
UIBehaviour
:所有组件的基类,它提供了 UI 组件生命周期管理的基础功能,比如OnEnable
、OnDisable
、OnDestroy
等IInitializePotentialDragHandler
:用于处理潜在的拖拽操作。在用户开始拖拽一个 UI 元素之前被调用。你可以在OnInitializePotentialDrag
方法中设置拖拽的初始状态IEventSystemHandler
:标记接口,不直接定义任何方法。它用于表示某个组件可以处理事件系统的事件。所有 UI 事件接口(如IDragHandler
、IBeginDragHandler
等)都继承自这个接口,确保它们可以被事件系统识别和调用IBeginDragHandler
:处理拖拽开始的事件。当用户开始拖拽 UI 元素时,OnBeginDrag
方法会被调用,在这个方法中处理拖拽开始时的逻辑IEndDragHandler
:处理拖拽结束的事件。当用户结束拖拽 UI 元素时,OnEndDrag
方法会被调用,在这个方法中处理拖拽结束时的逻辑IDragHandler
:处理拖拽进行中的事件。当用户在拖拽 UI 元素时,OnDrag
方法会被调用,在这个方法中处理拖拽的过程中发生的事情,比如更新拖拽位置IScrollHandler
:处理滚动事件。当用户在 UI 元素上滚动(如鼠标滚轮)时,OnScroll
方法会被调用,在这个方法中处理滚动的逻辑ICanvasElement
:标记接口,用于表示 UI 元素是 Canvas 组件的一部分。实现了这个接口的组件可以在 Canvas 上进行布局计算和更新ILayoutElement
:定义了参与布局计算的元素应具有的基本属性,UI布局系统使用这些信息计算布局ILayoutGroup
:用于管理一组 UI 元素的布局。它通常作为容器(如HorizontalLayoutGroup
或VerticalLayoutGroup
)的基类,负责对子元素进行自动排列和调整。ILayoutController
:用于控制和管理布局过程。它处理布局计算和更新,确保 UI 元素按照指定的规则进行布局。
参数定义
然后紧接着就是很多参数的定义,以及配套getset的属性。也没啥列举的必要
个别参数在set的时候会标记脏,例如viewport在set时会SetDirtyCaching
ScrollBar的话则是为onValueChanged
事件Addlistener
,最终调用的是SetNormalizedPosition
:一个统一的能分别根据横纵两种模式设置滚动条的函数。当然这个属性在Set的时候会先移除旧ScrollBar的Listener。
OnValueChanged主要内容发生滚动时触发。这会传一个Vector2
表示滚动方向,如果仅允许一种滚动,例如仅允许Y轴向的滚动,则只会使用其中的y值
normalizedPosition 则与当前滚动的位置关联,这是一个介于0到1之间的浮点数。在实现无限列表的时候,这里需要额外处理,其逻辑先挖个坑
SetDirtyCaching的行为:
- Set ScrollBar
- 设置ScrollBar的visibility
SetDirty的行为
- 调整horizontalScrollbarSpacing(以及垂直的也是)
- OnEnable(注意OnDisable并没有)
以horizontalScrollbarSpacing
为例,viewport到底边是有个距离放ScrollBar的,那个距离就是这个Spacing,Vertical版本同理
主要接口实现
Rebuild:
部分函数&功能
标记脏
这个问题涉及重建,例行先看看别人的文章打底:
知乎 - Unity UI重建(Rebuild)源码分析
简书 - UGUI Layout
话说回来,这个组件有两个标记脏的方法:
csharp
protected void SetDirty()
{
if (IsActive())
{
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}
}
MarkLayoutForRebuild
这个方法主要会递归查到一个根物体,然后为之添加LayoutRebuilder
,然后把重建器加到待重建的队列等待重建时统一处理。RegisterCanvasElementForLayoutRebuild
这个只对本级注册重建。
这两个注册最后调的接口是一样的。
csharp
protected void SetDirtyCaching()
{
if (IsActive())
{
CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
m_ViewRect = null;
}
}
据说,布局重建不一定导致图形重建,例如一个按钮平移了一下。
滚动与拖动
Scroll是滚动,Drag是拖动
由滚动条和滚轮输入的算滚动,点击屏幕拉动的算拖动。
OnScroll是主要的处理滚动的函数:
主要注意拿到Vector2 scrollDelta
会给y
乘-1,因为向上滚动是正方向。
滚动的根本逻辑是结合scrollDelta和滚动系数去修改RectTransform的anchorPosition
关于拖动的代码再说一下这个:
csharp
// 在滚动前设置速度为0
// 因为ScrollRect存在一个属性inertia模拟滚动的惯性
// 所以存在没有拖动但是因为模拟了惯性导致内容依旧在移动的情况(此时速度不为0)
public virtual void OnInitializePotentialDrag(PointerEventData eventData)
{
if (eventData.button == PointerEventData.InputButton.Left)
{
m_Velocity = Vector2.zero;
}
}
※边界
Bound相关的还是挺重要的。这基本上就是界面滚动最底层的逻辑了
好吧我吐槽一下之前看1.0.0的代码给我看难受了,代码风格和最新的ugui的差很多。不仅没有注释,当他写出Vector2 zero = Vector2.zero
后还修改了zero的值后使我深深滴难受了一下。
csharp
internal static void AdjustBounds(ref Bounds viewBounds, ref Vector2 contentPivot, ref Vector3 contentSize, ref Vector3 contentPos)
{
Vector3 vector = viewBounds.size - contentSize;
if (vector.x > 0f)
{
contentPos.x -= vector.x * (contentPivot.x - 0.5f);
contentSize.x = viewBounds.size.x;
}
if (vector.y > 0f)
{
contentPos.y -= vector.y * (contentPivot.y - 0.5f);
contentSize.y = viewBounds.size.y;
}
}
首先,Bound相关的操作应该是在view那个坐标系下计算的(一般情况下Content是view的子级)。
- GetBounds返回的是m_Content相对viewRect的位置和大小
- UpdateBounds则分为两段,前一段是走了一遍
AdjustBounds
,这次的结果确保Content至少和View一样大(因为只有view小于content才能滚动)。之后如果是MovementType.Clamped
,则计算contentBound的max和min的xy是否大于view对应的,相当于是计算contentBound是否有超出view的,如果有(即其结果的平方大于常量float.Epsilon
)则调用AdjustBounds
(这是第二次调用) AdjustBounds
注释说是确保Content至少和view一样大。但是我手动计算逻辑认定会移动Content,使其Pivot位于view的中心位置,然后Content大小放到至少和view一样大。
此外:
- 当Content大小大于等于view时是不会触发
AdjustBounds
的 zero
变量(或者后面版本的delta
)实际上保存的是水平方向和垂直方向上contentBounds超出viewBound的长度
注:float.Epsilon
是一个比0大但非常接近0的常量,代表 float 类型中最小的正数,也可以用作表示浮点数运算中可能存在的微小误差的常量。
但是我有一个大大滴疑问:第二次调用AdjustBounds
我觉得不会被执行。因为第一次AdjustBounds
已经把二者Size搞一样了,第二次跟本进不去if。我不太清楚怎么回事。
LateUpdate
- 通过调用
EnsureLayoutHasRebuilt
确保已经重建 - 调用UpdateBounds
- 如果超出可滚动范围
附
布局更新周期
Canvas.willRenderCanvases
在 Canvas 即将开始渲染之前被调用。布局重建函数是+=
到这上面的,届时会分别遍历m_LayoutRebuildQueue跟m_GraphicRebuildQueue并对合法的ICanvasElement
实例执行ICanvasElement.Rebuild
渲染机制
两种重建与三种脏标记
- 图像重建和布局重建
- 布局脏、顶点脏、材质脏
Github Pages - UGUI Rebuild
知乎 - 【Unity源码学习】网格重建
- RectTransform的属性发生变化导致
SetLayoutDirty
- 顶点变化(如图像fill参数变化时)会导致
SetVerticesDirty
进而图像重建 - Graphic派生出的带有
color
属性的,这个属性改了也会顶点脏 - 材质变化也是图像重建
- 重建是先布局后图像
Question:这俩重建哪个开销大?
A:图像重建大
通过阅读理解到的优化建议
方向:
- 避免重建(某些元素本不需要)
- 避免OverDraw
- 减少CPU和GPU的IO(比如通过图集)
Tips:
- 禁用 Canvas 组件不会通过 Canvas 层次结构触发昂贵的 OnDisable/OnEnable 回调
- 自动布局代价很昂贵
- 重建画布产生的主要是CPU成本
- 使用全屏 UI 时,应隐藏其他所有内容,或者在全屏 UI 期间降低
Application.targetFrameRate
- 避免元素堆叠(针对OverDraw)
- 多个Mask之间可以进行合批
- Mask内外不能进行合批.\
- RectMask2D本身不产生drawcall
参考
博客园 - Unity编辑器扩展基础总结 | 第2章 标准编辑器扩展
简书 - Unity优化 | 如何优化UGUI的ScrollRect