Unity笔记:ScrollRect代码阅读

大体流程

Unity Docs - UGUI | Class ScrollRect

总的说

自身不负责Rebuild,设置脏之后交由LayoutRebuilder注册到CanvasUpdateRegistry里待rebuild的集合在固定时机统一Rebuild。自身只在Prelayout和Postlayout做一下数据准备和数据更新

自身的ICanvasElement.Rebuild是在CanvasUpdateRegistry.PerformUpdate对集合内的每个合法的(是的会先检验)ICanvasElement依次调用的

  1. 根据各种信息计算view和content的边界
  2. 根据输入(拖动等)更新边界(修改AnchorPosition)

content的移动是这样的流程:

  1. 通过记录当前光标位置和触发滚动的起始位置的差值计算出Content锚点的offset
  2. position += offset
  3. 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 组件生命周期管理的基础功能,比如 OnEnableOnDisableOnDestroy
  • IInitializePotentialDragHandler:用于处理潜在的拖拽操作。在用户开始拖拽一个 UI 元素之前被调用。你可以在 OnInitializePotentialDrag 方法中设置拖拽的初始状态
  • IEventSystemHandler:标记接口,不直接定义任何方法。它用于表示某个组件可以处理事件系统的事件。所有 UI 事件接口(如 IDragHandlerIBeginDragHandler 等)都继承自这个接口,确保它们可以被事件系统识别和调用
  • IBeginDragHandler:处理拖拽开始的事件。当用户开始拖拽 UI 元素时,OnBeginDrag 方法会被调用,在这个方法中处理拖拽开始时的逻辑
  • IEndDragHandler:处理拖拽结束的事件。当用户结束拖拽 UI 元素时,OnEndDrag 方法会被调用,在这个方法中处理拖拽结束时的逻辑
  • IDragHandler:处理拖拽进行中的事件。当用户在拖拽 UI 元素时,OnDrag 方法会被调用,在这个方法中处理拖拽的过程中发生的事情,比如更新拖拽位置
  • IScrollHandler:处理滚动事件。当用户在 UI 元素上滚动(如鼠标滚轮)时,OnScroll 方法会被调用,在这个方法中处理滚动的逻辑
  • ICanvasElement:标记接口,用于表示 UI 元素是 Canvas 组件的一部分。实现了这个接口的组件可以在 Canvas 上进行布局计算和更新
  • ILayoutElement:定义了参与布局计算的元素应具有的基本属性,UI布局系统使用这些信息计算布局
  • ILayoutGroup:用于管理一组 UI 元素的布局。它通常作为容器(如 HorizontalLayoutGroupVerticalLayoutGroup)的基类,负责对子元素进行自动排列和调整。
  • 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一样大。

此外:

  1. 当Content大小大于等于view时是不会触发AdjustBounds
  2. zero变量(或者后面版本的delta)实际上保存的是水平方向和垂直方向上contentBounds超出viewBound的长度

注:float.Epsilon是一个比0大但非常接近0的常量,代表 float 类型中最小的正数,也可以用作表示浮点数运算中可能存在的微小误差的常量。


但是我有一个大大滴疑问:第二次调用AdjustBounds我觉得不会被执行。因为第一次AdjustBounds已经把二者Size搞一样了,第二次跟本进不去if。我不太清楚怎么回事。

LateUpdate

  1. 通过调用EnsureLayoutHasRebuilt确保已经重建
  2. 调用UpdateBounds
  3. 如果超出可滚动范围

布局更新周期

Canvas.willRenderCanvases在 Canvas 即将开始渲染之前被调用。布局重建函数是+=到这上面的,届时会分别遍历m_LayoutRebuildQueue跟m_GraphicRebuildQueue并对合法的ICanvasElement实例执行ICanvasElement.Rebuild

渲染机制

CnBlogs - 浅谈UGUI的渲染机制

两种重建与三种脏标记

  1. 图像重建和布局重建
  2. 布局脏、顶点脏、材质脏

Github Pages - UGUI Rebuild
知乎 - 【Unity源码学习】网格重建

  1. RectTransform的属性发生变化导致SetLayoutDirty
  2. 顶点变化(如图像fill参数变化时)会导致SetVerticesDirty进而图像重建
  3. Graphic派生出的带有color属性的,这个属性改了也会顶点脏
  4. 材质变化也是图像重建
  5. 重建是先布局后图像

Question:这俩重建哪个开销大?

A:图像重建大

通过阅读理解到的优化建议

方向:

  1. 避免重建(某些元素本不需要)
  2. 避免OverDraw
  3. 减少CPU和GPU的IO(比如通过图集)

Tips:

  1. 禁用 Canvas 组件不会通过 Canvas 层次结构触发昂贵的 OnDisable/OnEnable 回调
  2. 自动布局代价很昂贵
  3. 重建画布产生的主要是CPU成本
  1. 使用全屏 UI 时,应隐藏其他所有内容,或者在全屏 UI 期间降低 Application.targetFrameRate
  2. 避免元素堆叠(针对OverDraw)
  3. 多个Mask之间可以进行合批
  4. Mask内外不能进行合批.\
  5. RectMask2D本身不产生drawcall

参考

博客园 - Unity编辑器扩展基础总结 | 第2章 标准编辑器扩展

简书 - Unity优化 | 如何优化UGUI的ScrollRect

51CTO - 【Unity UGUI】ScrollRect 动态缩放格子大小,自动定位到中间的格子

ScrollRect 探究

知乎 - UGUI源码解析(十)ScrollRect

相关推荐
黑叶白树18 分钟前
简单的签到程序 python笔记
笔记·python
幸运超级加倍~1 小时前
软件设计师-上午题-15 计算机网络(5分)
笔记·计算机网络
小码编匠2 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
芊寻(嵌入式)2 小时前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
准橙考典3 小时前
怎么能更好的通过驾考呢?
人工智能·笔记·自动驾驶·汽车·学习方法
密码小丑4 小时前
11月4日(内网横向移动(一))
笔记
鸭鸭梨吖5 小时前
产品经理笔记
笔记·产品经理
Envyᥫᩣ5 小时前
C#语言:从入门到精通
开发语言·c#
齐 飞5 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb