uguilayout

author author     2023-03-23     274

关键词:

参考技术A 一、 UGUI布局系统由 布局元素 (Layout Elements)和 布局控制器 (Layout Controller)组成。

1、布局控制器的基础接口为ILayoutController。

(1)ILayoutController有两个方法,布局系统调用这两个方法的顺序是固定的,首先调用SetLayoutHorizontal,再调用SetLayoutVertical:

A、void SetLayoutHorizontal() :处理水平方向的布局;

B、void SetLayoutVertical() :处理垂直方向的布局。

(2)继承ILayoutController的两个接口:

A、ILayoutSelfController :只控制自己RectTransform的改变;继承ILayoutSelfController的两个类如下:

[1] (内容尺寸适配器) Content Size Fitter  <—— ILayoutSelfController:根据孩子中布局元素的尺寸及自身包含内容的多少控制自身的尺寸;

[2] (纵横比适配器) AspectRatioFitter  <—— ILayoutSelfController:根据自身设置的模式(AspectMode)和比率(Aspect Ratio)以及宽或高调整另一边的大小,不受子元素的影响。

B、ILayoutGroup :控制所有孩子节点的布局变化。这个接口就是我们比较熟悉的接口,平时项目中使用的很多布局组件都继承该接口:

[1] ScrollRect   <—— ILayoutGroup;

[2] GridLayoutGroup   <—— LayoutGroup  <—— ILayoutGroup;

[3] HorizontalLayoutGroup\VerticalLayoutGroup->HorizontalOrVerticalLayoutGroup  <—— LayoutGroup <—— ILayoutGroup

2、所有包含RectTransform组件的GameObject都视为布局元素。

(1)布局元素拥有一些定义自身尺寸的属性:

A、Minimum width:最小宽度;

B、Minimum height:最小高度;

C、Preferred width:如果有充足的空间可以分配的最合适宽度;

D、Preferred height:如果有充足的空间可以分配的最合适高度;

E、Flexible width:灵活宽度,一般是相对于父元素的比例;

F、Flexible height:灵活高度,一般是相对于父元素的比例。

默认情况下,Minimum width、Minimum height、Preferred width、Preferred height均为0,Flexible width、Flexible height都是disabled的。如果想调整这些属性就需要添加继承ILayoutElement接口的组件重写这些值。像我们常用的Text、Image组件都继承了ILayoutElement;同时一些布局控制器自身也继承了ILayoutElement,如:LayoutGroup(包括所有继承LayoutGroup的子类)、ScrollRect;UISystem也创建了一个单独组件LayoutElement继承ILayoutElement,任何一个想重写上述属性的GameObject都可以通过添加LayoutElement组件实现。LayoutElement同时继承了ILayoutIgnorer接口,可以通过设置ignoreLayout决定该GameObject是否被Layout System忽略。

(2)ILayoutElement定义了布局元素用到的属性minWidth、preferredWidth、flexibleWidth、minHeight、preferredHeight、flexibleHeight,

同时定义了两个方法:

void CalculateLayoutInputHorizontal():计算布局元素的minWidth,preferredWidth和flexibleWidth值

void CalculateLayoutInputVertical():计算布局元素的minHeight,preferredHeight和flexibleHeight值

二、布局重建过程

1、首先引起布局重建的元素调用LayoutRebuilder.MarkLayoutForRebuild() ,然后逐层向上递归查找直到找到最上层的布局根节点,这个根节点必须包含继承ILayoutGroup接口、Behaviour类并且isActiveAndEnabled属性为true的组件。然后以这个根节点初始化一个布局重建器LayoutRebuilder,然后将该LayoutRebuilder添加到CanvasUpdateRegistry中m_LayoutRebuildQueue队列中等待处理。调用过程如下:

static void LayoutRebuilder.MarkLayoutForRebuild(RectTransform rect) 

->static void LayoutRebuilder.MarkLayoutRootForRebuild(RectTransform controller) 

->static bool CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild(ICanvasElement element) 

-> CanvasUpdateRegistry.InternalRegisterCanvasElementForLayoutRebuild(ICanvasElement element) 

-> 加入m_LayoutRebuildQueue队列中。

其中有一个比较特殊的就是ScrollRect,在ScrollRect的OnEnable()方法和SetDirtyCaching()方法中会先调用CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this),再调用LayoutRebuilder.MarkLayoutForRebuild(rectTransform)。与LayoutRebuilder比较,ScrollRect本身也继承了ICanvasElement接口。在Rebuild()函数中,ScrollRect针对CanvasUpdate.Prelayout(布局执行前)和CanvasUpdate.PostLayout(布局执行后)两个阶段实现了自己的执行逻辑。而LayoutRebuilder的Rebuild()函数只针对CanvasUpdate.Layout阶段实现对应的执行逻辑

2、CanvasUpdateRegistry构造函数中会将PeformUpdate方法加入Canvas.willRenderCanvases事件中,然后在渲染所有的Canvas之前,抛出willRenderCanvases事件从而调用PeformUpdate(),在PeformUpdate函数中执行步骤如下:

(1)首先会删除m_LayoutRebuildQueue中所有无效的元素;

(2)然后根据节点的深度(父节点个数越大越靠前)对m_LayoutRebuildQueue排序;

(3)然后分别以CanvasUpdate.PreLayout(布局前),CanvasUpdate.Layout(布局),CanvasUpdate.PostLayout(布局后)的参数顺序调用每一个ICanvasElement元素的Rebuild方法;

(4)然后调用所有ICanvasElement元素的LayoutComplete()方法。针对该方法只有LayoutRebuilder类中实现:将该LayoutRebuilder从LayoutRebuilder的静态对象池s_Rebuilders中删除;

(5)然后清空m_LayoutRebuildQueue队列;

(6)布局结束后调用ClipperRegistry.instance.Cull()遍历所有裁剪组件(继承IClipper接口的组件,如RectMask2D)的裁剪方法PerformClipping()。 

3、下面看一下ICanvasElement布局重建过程的实现,重建的实现就在LayoutRebuilder的Rebuild方法中,源码如下:

如上所示,整个布局分四个步骤:

(1)对该节点下所有有效(继承Behaviour且isActiveAndEnabled为true)的布局元素(即包含继承ILayoutElement组件的节点)执行CalculateLayoutInputHorizontal()计算水平方向布局尺寸。对孩子的遍历顺序由下往上,因为父节点尺寸计算依赖于子节点的尺寸;虽然针对所有布局元素,但是真正实现CalculateLayoutInputHorizontal()方法的只有LayoutGroup相关的组件。

LayoutGroup实现了一个虚方法CalculateLayoutInputHorizontal(),主要就是收集其子节点下所有没有被标记 ignoreLayout 的物体存到m_RectChildren列表中。LayoutGroup的子类重写该函数时都会先调用这个虚函数,然后再执行自己特有的逻辑。

A、 水平布局组件 HorizontalLayoutGroup <—— HorizontalOrVerticalLayoutGroup <—— LayoutGroup

重写CalculateLayoutInputHorizontal()。如上,首先调用LayoutGroup中CalculateLayoutInputHorizontal()方法,然后调用HorizontalOrVerticalLayoutGroup中的CalcAlongAxis(int axis, bool isVertical)方法,CalcAlongAxis()是一个公用方法,可以根据传入的参数确定计算哪个方向的尺寸,其中axis表示方向(0:horizontal 1:vertical),isVertical判断是否一个Vertical Group。CalcAlongAxis()执行过程如下:

① 首先遍历所有孩子节点调用GetChildSizes()方法获取对应方向上的尺寸(min、preferred、flexible);

② 然后根据所有子节点的尺寸计算当前节点对应方向上的尺寸totalMin、totalPreferred、totalFlexible;

③ 调用SetLayoutInputForAxis()方法将第二步计算的值设置对应的向量m_TotalMinSize、m_TotalPreferredSize、m_TotalFlexibleSize;

B、垂直布局组件VerticalLayoutGroup 重写方法如下,实现参考HorizontalOrVerticalLayoutGroup

(2)对该节点下所有布局控制器(即包含继承ILayoutController组件的节点)执行SetLayoutHorizontal()设置水平布局。对孩子的遍历顺序自上而下,因为孩子节点实际的布局依赖于父节点的布局;

A、HorizontalLayoutGroup和VerticalLayoutGroup中的SetLayoutHorizontal()会直接调用HorizontalOrVerticalLayoutGroup.SetChildrenAlongAxis()方法根据布局方向和布局组件类型设置其下子布局元素的位置和尺寸。

B、GridLayoutGroup也重写了SetLayoutHorizontal()方法,直接调用SetCellsAlongAxis(0),针对水平布局实现如图2-4所示,只设置每个子元素的尺寸,而不会改变子元素的位置。

(3)对该节点下所有布局元素(即包含继承ILayoutElement组件的节点)执行CalculateLayoutInputHorizontal()计算垂直方向布局尺寸;对孩子的遍历顺序由下往上;

(4)对该节点下所有布局控制器(即包含继承ILayoutController组件的节点)执行SetLayoutVertical()设置垂直布局。对孩子的遍历顺序自上而下;

从上面的执行顺序还可以看出,Layout System先搞定水平方向的布局然后才去设置垂直方向的布局。

三、布局重建触发因素

1、 以下函数,都会直接或间接的调用LayoutBuilder.MarkLayoutForRebuild()触发布局重建:

(1)OnEnable() :所有继承ILayoutController或ILayoutElement组件(AspectRatioFitter除外)的OnEnable()方法都会都会间接调用LayoutBuilder.MarkLayoutForRebuild();

A、Graphic :OnEnable() -> SetAllDirty() -> ( m_SkipLayoutUpdate变量为false时才会调用 )LayoutBuilder.MarkLayoutForRebuild();

B、LayoutGroup\ContentSizeFitter\LayoutElement\ScrollRect :OnEnable() -> SetDirty() -> LayoutBuilder.MarkLayoutForRebuild()。

(2)OnDisable() :所有继承ILayoutController或ILayoutElement组件的OnDisable()方法都会调用LayoutBuilder.MarkLayoutForRebuild()

(3)OnRectTransformDimensionsChange(): RectTransform尺寸变化时调用。继承自UIBehaviour的ContentSizeFitter、LayoutGroup、ScrollRect、Graphic都重写了该方法,具体调用如下:

A、ContentSizeFitter :OnRectTransformDimensionsChange() ->SetDirty() -> (IsActive()为true才会调用)LayoutRebuilder.MarkLayoutForRebuild();

B、LayoutGroup :OnRectTransformDimensionsChange() ->(isRootLayoutGroup为true时才会调用)SetDirty() ->(IsActive()为true时才会调用)LayoutRebuilder.MarkLayoutForRebuild();

C、ScrollRect :OnRectTransformDimensionsChange() -> SetDirty() ->( IsActive()为true时才会调用 )LayoutRebuilder.MarkLayoutForRebuild();

D、Graphic :OnRectTransformDimensionsChange() ->( gameObject.activeInHierarchy为true时才会调用 )SetLayoutDirty() ->(IsActive()为true时才会调用)LayoutRebuilder.MarkLayoutForRebuild();

(4)OnDidApplyAnimationProperties(): 动画导致属性( 重写OnDidApplyAnimationProperties()方法的类自身所特有的属性 )改变时调用。继承UIBehaviour的Graphic、LayoutGroup、LayoutElement都重写了该方法,具体调用如下:

A、Graphic :OnDidApplyAnimationProperties() -> SetAllDirty() -> ( m_SkipLayoutUpdate变量为false时才会调用 )LayoutBuilder.MarkLayoutForRebuild()。Graphic本身是个抽象类,不能当作组件直接添加,而继承自Graphic的Image\Text( ——> MaskableGraphic  ——> Graphic)组件的一些特有属性,如图3-1红框内标记,当动画改变Text的这些属性时会调用OnDidApplyAnimationProperties()函数。如图3-2红框内标记,当动画改变Image的这些属性时会调用OnDidApplyAnimationProperties()函数。

B、LayoutGroup :OnDidApplyAnimationProperties() -> SetDirty() -> ( IsActive()为true时才会调用 )LayoutBuilder.MarkLayoutForRebuild()

如图3-3、3-4、3-5分别是VerticalLayoutGroup、HorizontalLayoutGroup,当动画改变VerticalLayoutGroup的这些属性时会调用OnDidApplyAnimationProperties()函数。

C、LayoutElement :OnDidApplyAnimationProperties() -> SetDirty() -> ( IsActive()为true时才会调用 )LayoutBuilder.MarkLayoutForRebuild()

如图3-6所示,当动画改变LayoutElement的这些属性时会调用OnDidApplyAnimationProperties()函数。

(5)OnBeforeTransformParentChanged() :父节点transform属性变化前调用。继承自UIBehaviour的Graphic和LayoutElement类都重写了该方法:

A、Graphic :OnBeforeTransformParentChanged() -> SetAllDirty() -> LayoutBuilder.MarkLayoutForRebuild()

B、LayoutElement :OnBeforeTransformParentChanged() -> SetDirty() -> LayoutBuilder.MarkLayoutForRebuild()

2、 某些组件特有属性变化或函数调用:

(1)RectTransform 中导致布局变化的属性:Width、Height、Scale、以及锚点变化;

(2)Text 中导致布局变化的属性:text、alignment、fontSize、horizontalOverflow、verticalOverflow、lineSpacing、fontStyle、resizeTextForBestFit、resizeTextMinSize、supportRichText、resizeTextMaxSize、font;方法:FontTextureChanged()。

(3)Image 中导致布局变化的属性:sprite、overrideSprite;方法:SetNativeSize()、OnCanvasHierarchyChanged()。其中OnCanvasHierarchyChanged()只有在canvas.referencePixelsPerUnit变化且Image类型为Sliced或Tiled时才会调用SetLayoutDirty()

(4)LayoutGroup 中导致布局变化的属性:padding、childAlignment;方法:OnTransformChildrenChanged()

(5)GridLayoutGroup 中导致布局变化的属性:startCorner、startAxis、cellSize、spacing、constraint、constraintCount

(6)HorizontalOrVerticalLayoutGroup 中导致布局变化的属性:spacing、childForceExpandWidth、childForceExpandHeight、childControlWidth、childControlHeight、childScaleWidth、childScaleHeight

(7)ContentSizeFitter 中导致布局变化的属性:horizontalFit、verticalFit

(8)LayoutElement 中导致布局变化的属性:ignoreLayout、minWidth、minHeight、preferredWidth、preferredHeight、flexibleWidth、flexibleHeight、layoutPriority

(9)ScrollRect 中导致布局变化的属性:verticalScrollbar、horizontalScrollbar、horizontalScrollbarVisibility、verticalScrollbarVisibility、viewport

四、布局重建分析工具

1、第三节我们总结了一些可能导致布局重建的原因,游戏中尽量避免执行这些操作以减少布局重建,同时尽可能少的使用Layout相关组件以减少CPU时间的占用。通过第二节对重建过程的分析,我们知道每个引起布局重建的元素的元素都会先加入CanvasUpdateRegistry中m_LayoutRebuildQueue队列中,所以我们可以通过反射的方式来获取游戏中引起布局重建的元素(引用 https://www.xuanyusong.com/archives/4573),脚本如下:

public class CheckRebuildElement:MonoBehaviour



       IList m_LayoutRebuildQueue;

        privatevoid Awake()

       

           System.Type type = typeof(CanvasUpdateRegistry);

           FieldInfo field = type.GetField("m_LayoutRebuildQueue",BindingFlags.NonPublic | BindingFlags.Instance);

           m_LayoutRebuildQueue = (IList)field.GetValue(CanvasUpdateRegistry.instance);

       

        privatevoid Update()

       

            for(int j = 0; j < m_LayoutRebuildQueue.Count; j++)

           

                varrebuild = m_LayoutRebuildQueue[j];

                if(ObjectValidForUpdate(rebuild))

               

                    if(rebuild.transform)

                   

                       Debug.LogFormat("0 引起布局重建", rebuild.transform.name);

                       Graphic g = rebuild.transform.GetComponent();

                       if ((g != null) && (g.canvas != null))

                       

                           Debug.LogFormat(" 网格:0",rebuild.transform.GetComponent().canvas.name);

                       

                                                         

               

           

       

        privatebool ObjectValidForUpdate(ICanvasElement element)

       

            var valid = element != null;

            var isUnityObject = element is UnityEngine.Object;

            if(isUnityObject)

               valid = (element as UnityEngine.Object) != null; //Here we make use ofthe overloaded UnityEngine.Object == null, that checks if the native object isalive.

            return valid;