Unity中的UGUI源码解析之图形对象(Graphic)(2)-ICanvasElement
Unity中的UGUI源码解析之图形对象(Graphic)(2)-ICanvasElement
在上一篇文章中, 我们对整个Graphic部分做了概述, 这篇文章我们介绍ICanvasElement和CanvasUpdateRegistry.
ICanvasElement是一个接口(Interface). 抽象了能够在画布上显示的元素行为. 文件所在为: UnityEngine.UI/UI/Core/CanvasUpateRegistry.cs
.
相关的辅助类有: enum CanvasUpdate
和class CanvasUpdateRegistry
.
在Unity中, 有以下元素实现了该接口:
即:
- Toggle
- LayoutRebuilder
- ScrollRect
- Graphic
- InputField
- Scrollbar
- MaskableGraphic
- Image
- Slider
- RawImage
- Text
我们会在后面的文章慢慢介绍这些元素, 在本文中, 主要关注接口本身和其辅助类.
enum CanvasUpdate
CanvasUpdate是一个枚举, 本身很简单, 用于表明元素的刷新时机, 主要在重建(rebuild)和刷新(update)中使用.
/// <summary>
/// 在Canvas更新时调用的更新时机
/// </summary>
public enum CanvasUpdate
{/// <summary>/// 在布局之前调用/// </summary>Prelayout = 0,/// <summary>/// 在布局时调用/// </summary>Layout = 1,/// <summary>/// 在布局之后调用/// </summary>PostLayout = 2,/// <summary>/// 在渲染之前调用/// </summary>PreRender = 3,/// <summary>/// 稍后再渲染之前调用/// </summary>LatePreRender = 4,/// <summary>/// 最大值, 用于数组遍历上界/// </summary>MaxUpdateValue = 5
}
interface ICanvasElement
ICanvasElement抽象了在画布上显示的元素的基本行为, 用于元素在重建(rebuild)和刷新(update)的各个时机定义和触发特定的行为.
/// <summary>
/// 可以在画布上的元素行为接口
/// </summary>
public interface ICanvasElement
{/// <summary>/// 重新构建给定阶段的元素/// 可以将元素的更新延迟到画布刷新之前, 降低更新的消耗/// </summary>/// <param name="executing"></param>void Rebuild(CanvasUpdate executing);/// <summary>/// 相关联的transform/// </summary>Transform transform { get; }/// <summary>/// 此元素完成布局时的回调/// </summary>void LayoutComplete();/// <summary>/// 此元素完成Graphic重新构建时的回调/// </summary>void GraphicUpdateComplete();/// <summary>/// due to unity overriding null check/// we need this as something may not be null/// but may be destroyed/// 返回元素是否被销毁/// </summary>/// <returns></returns>bool IsDestroyed();
}
class CanvasUpdateRegistry
CanvasUpdateRegistry类是一个单例类, 主要负责驱动UGUI元素的刷新和重建, 是底层的Native代码(Canvas)和UGUI元素之间沟通的桥梁.
CanvasUpdateRegistry十分简单, 主要是维护了两个队列, 一个存放需要在布局更新时(比如位置发生变化时)刷新和重建的UGUI元素, 一个存放需要在图形更新时(也就是渲染内容发生变化时, 比如材质和顶点变化), 然后通过注册一个在画布渲染之前发送的事件, 照着布局更新到图形更新的顺序依次对两个队列进行刷新和重建.
刷新队列是各个UGUI元素自己在合适的时机, 调用CanvasUpdateRegistry单例进行注册和加入的, 在每次刷新之后会清空队列.
主要流程
主要的流程为:
在CanvasUpdateRegistry构造函数中注册画布渲染前事件:
Canvas.willRenderCanvases += PerformUpdate;
各个UGUI元素在合适的时机注册布局更新队列(
RegisterCanvasElementForLayoutRebuild
)或者图形更新队列(RegisterCanvasElementForGraphicRebuild
)触发画布渲染前事件:
PerformUpdate
UI系统分析启动:
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
清理无效的队列元素(比如那些不再存活的元素):
CleanInvalidItems
标记布局更新中:
m_PerformingLayoutUpdate = true;
布局队列元素排序:
m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
对布局队列中的有效元素(可能在某个时机中变得无效)分别进行各个时机的重建:
Prelayout(0), Layout(1), PostLayout(2);
int i = 0; i <= (int)CanvasUpdate.PostLayout; i++
ObjectValidForUpdate(rebuild)
rebuild.Rebuild((CanvasUpdate)i)
对布局队列中的元素分别进行布局完成的回调:
m_LayoutRebuildQueue[i].LayoutComplete()
清空布局队列:
m_LayoutRebuildQueue.Clear();
清理布局更新中标记:
m_PerformingLayoutUpdate = false;
进行剔除(cull):
ClipperRegistry.instance.Cull();
标记图形更新中:
m_PerformingGraphicUpdate = true;
对图形队列中的有效元素(可能在某个时机中变得无效)分别进行各个时机的重建:
PreRender(3), LatePreRender(4)
var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++
ObjectValidForUpdate(element)
element.Rebuild((CanvasUpdate)i)
对图形队列中的元素分别进行图形更新完成的回调:
m_GraphicRebuildQueue[i].GraphicUpdateComplete()
清空图形队列:
m_GraphicRebuildQueue.Clear();
清理图形更新中标记:
m_PerformingGraphicUpdate= false;
UI系统分析结束:
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
基本属性和字段
public class CanvasUpdateRegistry
{/// <summary>/// 单例/// </summary>private static CanvasUpdateRegistry s_Instance;/// <summary>/// 是否在更新布局(位置)/// </summary>private bool m_PerformingLayoutUpdate;/// <summary>/// 是否在更新图形(渲染)/// </summary>private bool m_PerformingGraphicUpdate;/// <summary>/// 需要在布局更新时重建的元素(位置)/// </summary>private readonly IndexedSet<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>();/// <summary>/// 需要在图形更新时重建的元素(渲染)/// </summary>private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();/// <summary>/// 单例/// </summary>public static CanvasUpdateRegistry instance{get{if (s_Instance == null)s_Instance = new CanvasUpdateRegistry();return s_Instance;}}
}
注册元素和回调
// 构造函数中注册即将开始渲染画布时的回调
protected CanvasUpdateRegistry()
{Canvas.willRenderCanvases += PerformUpdate;
}/// <summary>
/// 注册图形元素重建
/// </summary>
/// <param name="element"></param>
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}/// <summary>
/// 注册布局元素重建
/// </summary>
/// <param name="element"></param>
public static void RegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}public static void UnRegisterCanvasElementForRebuild(ICanvasElement element)
{instance.InternalUnRegisterCanvasElementForLayoutRebuild(element);instance.InternalUnRegisterCanvasElementForGraphicRebuild(element);
}
工具方法
/// <summary>
/// 是否正在进行布局更新
/// </summary>
/// <returns></returns>
public static bool IsRebuildingLayout()
{return instance.m_PerformingLayoutUpdate;
}/// <summary>
/// 是否正在进行图形更新
/// </summary>
/// <returns></returns>
public static bool IsRebuildingGraphics()
{return instance.m_PerformingGraphicUpdate;
}/// <summary>
/// 清理无效的元素
/// </summary>
private void CleanInvalidItems()
{// So MB's override the == operator for null equality, which checks// if they are destroyed. This is fine if you are looking at a concrete// mb, but in this case we are looking at a list of ICanvasElement// this won't forward the == operator to the MB, but just check if the// interface is null. IsDestroyed will return if the backend is destroyed.for (int i = m_LayoutRebuildQueue.Count - 1; i >= 0; --i){var item = m_LayoutRebuildQueue[i];if (item == null){m_LayoutRebuildQueue.RemoveAt(i);continue;}if (item.IsDestroyed()){m_LayoutRebuildQueue.RemoveAt(i);item.LayoutComplete();}}for (int i = m_GraphicRebuildQueue.Count - 1; i >= 0; --i){var item = m_GraphicRebuildQueue[i];if (item == null){m_GraphicRebuildQueue.RemoveAt(i);continue;}if (item.IsDestroyed()){m_GraphicRebuildQueue.RemoveAt(i);item.GraphicUpdateComplete();}}
}/// <summary>
/// 判断元素是否有效
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
private bool ObjectValidForUpdate(ICanvasElement element)
{var valid = element != null;var isUnityObject = element is Object;if (isUnityObject)valid = (element as Object) != null; //Here we make use of the overloaded UnityEngine.Object == null, that checks if the native object is alive.return valid;
}/// <summary>
/// 布局元素排序委托, 做成静态成员可以减少函数到委托的转换
/// </summary>
private static readonly Comparison<ICanvasElement> s_SortLayoutFunction = SortLayoutList;/// <summary>
/// 排序节点
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
private static int SortLayoutList(ICanvasElement x, ICanvasElement y)
{Transform t1 = x.transform;Transform t2 = y.transform;return ParentCount(t1) - ParentCount(t2);
}
执行更新主流程
/// <summary>
/// 执行更新主流程
/// </summary>
private void PerformUpdate()
{// UI系统分析启动UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);// 清理无效元素CleanInvalidItems();// 标记布局更新中m_PerformingLayoutUpdate = true;// 排序布局元素(根据父节点数量从小到大的排序)m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);// 在各个时机分别触发各个布局元素的重建(Prelayout, Layout, PostLayout)for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++){for (int j = 0; j < m_LayoutRebuildQueue.Count; j++){var rebuild = instance.m_LayoutRebuildQueue[j];try{// 只对有效的元素进行触发(可能在某个时机中会改变有效性)if (ObjectValidForUpdate(rebuild))rebuild.Rebuild((CanvasUpdate)i);}catch (Exception e){Debug.LogException(e, rebuild.transform);}}}// 触发所有布局元素布局完成的回调for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)m_LayoutRebuildQueue[i].LayoutComplete();// 清理布局元素队列instance.m_LayoutRebuildQueue.Clear();// 清理布局元素更新中的标记m_PerformingLayoutUpdate = false;// 布局元素完成更新后, 进行剔除操作(后面的文章单独介绍)ClipperRegistry.instance.Cull();// 标记图形元素更新中m_PerformingGraphicUpdate = true;// 在各个时机分别触发各个图形元素的重建(PreRender, LatePreRender)for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++){for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++){try{var element = instance.m_GraphicRebuildQueue[k];// 只对有效的元素进行触发(可能在某个时机中会改变有效性)if (ObjectValidForUpdate(element))element.Rebuild((CanvasUpdate)i);}catch (Exception e){Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);}}}// 触发所有图形元素更新完成的回调for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)m_GraphicRebuildQueue[i].GraphicUpdateComplete();// 清空图形队列instance.m_GraphicRebuildQueue.Clear();// 清理图形更新中标记m_PerformingGraphicUpdate = false;// UI系统分析结束UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
}
举例子: Graphic的实现
/// <summary>
/// 重建接口的实现
/// </summary>
/// <param name="update"></param>
public virtual void Rebuild(CanvasUpdate update)
{if (canvasRenderer.cull) return;// 在渲染之前更新所需内容(顶点或者材质)switch (update){case CanvasUpdate.PreRender:if (m_VertsDirty){UpdateGeometry();m_VertsDirty = false;}if (m_MaterialDirty){UpdateMaterial();m_MaterialDirty = false;}break;}
}
总结
今天介绍了ICanvasElement和其相关的工具类, ICanvasElement抽象了可以在画布上显示的元素的行为, 主要是重建和一些回调, 而CanvasUpdateRegistry则是使用ICanvasElement的主体, 根据注册画布渲染前事件来触发那些需要进行重建的元素(比如元素自己知道某些情况下需要进行重建, 就要把自己注册到CanvasUpdateRegistry)的接口.
本质上CanvasUpdateRegistry的存在是为了统一UGUI元素的刷新, 一方面避免刷新时机的不统一, 一方面也能降低刷新频率. Unity在很多地方都使用了类似的机制, 值得我们借鉴.
好了, 今天的内容就是这些, 希望对大家有所帮助.
Unity中的UGUI源码解析之图形对象(Graphic)(2)-ICanvasElement相关推荐
- Unity中的UGUI源码解析之事件系统(2)-EventSystem组件
Unity中的UGUI源码解析之事件系统(2)-EventSystem组件 今天介绍我们的第一个主角: EventSystem. EventSystem在整个事件系统中处于中心, 相当于事件系统的管理 ...
- Unity中的UGUI源码解析之事件系统(8)-输入模块(中)
Unity中的UGUI源码解析之事件系统(8)-输入模块(中) 接上一篇文章, 继续介绍输入模块. Unity中主要处理的是指针事件, 也就是在2d平面上跟踪指针设备输入坐标的的事件, 这一类事件有鼠 ...
- Unity中的UGUI源码解析之事件系统(9)-输入模块(下)
Unity中的UGUI源码解析之事件系统(9)-输入模块(下) 接上一篇文章, 继续介绍输入模块. StandaloneInputModule类是上一篇文章介绍的抽象类PointerInputModu ...
- Unity中的UGUI源码解析之事件系统(6)-RayCaster(下)
Unity中的UGUI源码解析之事件系统(6)-RayCaster(下) 接上一篇文章, 继续介绍投射器. GraphicRaycaster GraphicRaycaster继承于BaseRaycas ...
- Unity中的UGUI源码解析之事件系统(3)-EventData
Unity中的UGUI源码解析之事件系统(3)-EventData 为了在事件系统中传递数据, Unity提供了EventData相关的类来封装这一类数据. 了解这些结构有助于我们对后面模块的学习. ...
- elementui组件_elementui 中 loading 组件源码解析(续)
上一篇我们说了elementui如何将loading组件添加到 Vue 实例上,具体内容见上期 elementui 中 loading 组件源码解析. 这一篇我们开始讲讲自定义指令 自定义指令 关于自 ...
- elementui table某一列是否显示_elementui 中 loading 组件源码解析(续)
上一篇我们说了elementui如何将loading组件添加到 Vue 实例上,具体内容见上期 elementui 中 loading 组件源码解析. 这一篇我们开始讲讲自定义指令 自定义指令 关于自 ...
- UGUI源码解析——ContentSizeFitter
一:前言 ContentSizeFitter继承自ILayoutSelfController,是调整对象自适应的组件,ContentSizeFitter不改变子物体的大小和位置,而是根据子物体(ILa ...
- UGUI源码解析——LayoutElement
一:前言 继承了ILayoutElement和ILayoutIgnorer接口,作为布局元素组件 挂载了Layout Element组件的对象,布局并不会生效,它是受到实现了布局组的控制(Horizo ...
最新文章
- tcp时间戳 引起的网站不能访问
- 【错误记录】GitHub 提交代码失败、获取代码失败、连接超时、权限错误、ping 请求连接超时 ( 查找域名对应 IP | 设置 host 文件 )
- c语言中一百以内相乘的积,一百以内的加减乘除法游戏....
- dokuwiki 的管理和使用
- ITK:创建具有相关类ID的样本列表
- plsql developer 64位版本
- 新媒体视频导演 - 美学基础 todo
- wamp5.5.12安装re dis扩展
- Linux whoami命令、Linux su命令、Linux w命令
- Ubuntu 中的编程语言(中)
- 虚拟机备份克隆导致SQL SERVER 出现IO错误案例
- Tensorflow学习笔记 (用 tf.data 加载图片)
- 单片机74LS164C语言例子,74ls164单片机编程汇总(跑马灯/驱动数码管)
- 计算机PS考试都考哪些,计算机专业ps考试题(考查课)(10页)-原创力文档
- thinkphp使用163/126邮箱发送
- 计算机图形表示的原理
- Android中应用分包的方法(Apk Splits)
- SIPP测试使用指导
- Objective-C 理解之方括号[ ]的使用
- 前端框架vue04~~vue.cli脚手架的基本使用