UGUI内核大探究(七)Graphic中我们讲到Graphic组件SetLayoutDirty里会通知LayoutRebuilder布局需要重建,那么布局是具体是怎样重建的呢?我们知道UGUI有三种布局组,HorizontalLayoutGroup(水平布局组)、VerticalLayoutGroup(垂直布局组)和GridLayoutGroup(格子布局组),为对象添加某一种布局组,就可以让其子对象按照对应布局方法排列。那么这些LayoutGroup与Graphic又有什么关系呢?本文就带着这些问题就探究一下Layout系列组件的原理。

按照惯例,附上UGUI源码下载地址。

在研究Layout系列组件之前,我们首先要看一下LayoutRebuilder这个类。Graphic是通过MarkLayoutForRebuild这个静态方法标记自己需要重建的。

        public static void MarkLayoutForRebuild(RectTransform rect){if (rect == null)return;RectTransform layoutRoot = rect;while (true){var parent = layoutRoot.parent as RectTransform;if (!ValidLayoutGroup(parent))break;layoutRoot = parent;}// We know the layout root is valid if it's not the same as the rect,// since we checked that above. But if they're the same we still need to check.if (layoutRoot == rect && !ValidController(layoutRoot))return;MarkLayoutRootForRebuild(layoutRoot);}

这个方法里,会找到父对象中最上层的 ILayoutGroup类型的组件(ValidLayoutGroup)layoutRoot,如果没有找到,便判断一下自己是否是ILayoutController类型(ValidController),然后为符合条件的layoutRoot创建Rebuilder。

        private static void MarkLayoutRootForRebuild(RectTransform controller){if (controller == null)return;var rebuilder = s_Rebuilders.Get();rebuilder.Initialize(controller);if (!CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild(rebuilder))s_Rebuilders.Release(rebuilder);}

s_Rebuilders是一个LayoutRebuilder类型的ObjectPool,通过Get方法创建(或从Pool里取出)一个实例rebuilder(再通过Release方法回收这一段内存)。我们看到这段代码调用了TryRegisterCanvasElementForLayoutRebuild,将rebuilder注册到CanvasUpdateRegistry。

LayoutRebuilder继承了ICanvasElement接口。UGUI内核大探究(六)CanvasUpdateRegistry中介绍过当Canvas绘制前会调用CanvasUpdateRegistry,而后者会遍历所有注册到它的ICanvasElement类型的组件,调用他们的Rebuild方法。

        public void Rebuild(CanvasUpdate executing){switch (executing){case CanvasUpdate.Layout:// It's unfortunate that we'll perform the same GetComponents querys for the tree 2 times,// but each tree have to be fully iterated before going to the next action,// so reusing the results would entail storing results in a Dictionary or similar,// which is probably a bigger overhead than performing GetComponents multiple times.PerformLayoutCalculation(m_ToRebuild, e => (e as ILayoutElement).CalculateLayoutInputHorizontal());PerformLayoutControl(m_ToRebuild, e => (e as ILayoutController).SetLayoutHorizontal());PerformLayoutCalculation(m_ToRebuild, e => (e as ILayoutElement).CalculateLayoutInputVertical());PerformLayoutControl(m_ToRebuild, e => (e as ILayoutController).SetLayoutVertical());break;}}

PerformLayoutCalculation从m_ToRebuild(也就是保存在rebuilder里的layoutRoot)获取ILayoutElement类型的组件,根据第二个参数(Lamda表达式)调用他们的对应的方法(CalculateLayoutInputHorizontal或CalculateLayoutInputVertical)。再调用方法之前,以m_ToRebuild的子对象为参数递归调用PerformLayoutCalculation。(由此我们可知,Graphic实际上是为Image和Text调用MarkLayoutForRebuild方法)

PerformLayoutControl从m_ToRebuild获取ILayoutController(ILayoutGroup继承自这个接口)组件,根据第二参数调用他们对应的方法(SetLayoutHorizontal或SetLayoutVertical),然后以m_ToRebuild的子对象为参数递归调用PerformLayoutControl。

在PerformLayoutControl里我们看到ILayoutSelfController(ILayoutController的派生接口)类型的组件会优先于其他组件调用方法。那么ILayoutSelfController是什么样的组件呢?

上图中,我们为一个Text添加了ContentSizeFitter组件,并设置Horizontal Fit和Vertical Fit都为Preferred Size,就可以看到这个对象的尺寸变成了跟Text内容一样的尺寸。

ContentSizeFitter就是继承了ILayoutSelfController接口的组件,同样继承了后者的还有AspectRatioFitter。二者是调节自身尺寸的组件,所以是ILayoutSelfController。

在ContentSizeFitter里SetLayoutHorizontal和SetLayoutVertical方法会调用HandleSelfFittingAlongAxis方法:

        private void HandleSelfFittingAlongAxis(int axis){FitMode fitting = (axis == 0 ? horizontalFit : verticalFit);if (fitting == FitMode.Unconstrained)return;m_Tracker.Add(this, rectTransform, (axis == 0 ? DrivenTransformProperties.SizeDeltaX : DrivenTransformProperties.SizeDeltaY));// Set size to min or preferred sizeif (fitting == FitMode.MinSize)rectTransform.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetMinSize(m_Rect, axis));elserectTransform.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetPreferredSize(m_Rect, axis));}

根据适应方式为rectTransform设置尺寸(最小尺寸或首选尺寸)。

AspectRatioFitter是方面比例适应器,它的SetLayoutHorizontal和SetLayoutVertical方法虽然是空方法,但是aspectMode、aspectRatioaspectRatio改变以及OnEnable(调用时机参见Untiy3D组件小贴士(一)OnEnabled与OnDisabled)时会调用SetDirty,继而调用UpdateRect:

        private void UpdateRect(){if (!IsActive())return;m_Tracker.Clear();switch (m_AspectMode){
#if UNITY_EDITORcase AspectMode.None:{if (!Application.isPlaying)m_AspectRatio = Mathf.Clamp(rectTransform.rect.width / rectTransform.rect.height, 0.001f, 1000f);break;}
#endifcase AspectMode.HeightControlsWidth:{m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaX);rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, rectTransform.rect.height * m_AspectRatio);break;}case AspectMode.WidthControlsHeight:{m_Tracker.Add(this, rectTransform, DrivenTransformProperties.SizeDeltaY);rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, rectTransform.rect.width / m_AspectRatio);break;}case AspectMode.FitInParent:case AspectMode.EnvelopeParent:{m_Tracker.Add(this, rectTransform,DrivenTransformProperties.Anchors |DrivenTransformProperties.AnchoredPosition |DrivenTransformProperties.SizeDeltaX |DrivenTransformProperties.SizeDeltaY);rectTransform.anchorMin = Vector2.zero;rectTransform.anchorMax = Vector2.one;rectTransform.anchoredPosition = Vector2.zero;Vector2 sizeDelta = Vector2.zero;Vector2 parentSize = GetParentSize();if ((parentSize.y * aspectRatio < parentSize.x) ^ (m_AspectMode == AspectMode.FitInParent)){sizeDelta.y = GetSizeDeltaToProduceSize(parentSize.x / aspectRatio, 1);}else{sizeDelta.x = GetSizeDeltaToProduceSize(parentSize.y * aspectRatio, 0);}rectTransform.sizeDelta = sizeDelta;break;}}}

虽然有个不明对象m_Tracker,但是并不妨碍我们读代码。

HeightControlsWidth(高度控制宽度)和WidthControlsHeight(宽度控制高度)就不多讲了。FitInParent是当aspectRatio小于父对象的宽高比例时,以父对象的高为基准按照aspectRatio设置宽,大于时以父对象的宽为基准按照aspectRatio设置高。而FitInParent则相反。

接着我们看ILayoutController的另一个派生接口ILayoutGroup。LayoutGroup继承了ILayoutGroup。

LayoutGroup是一个抽象类,HorizontalOrVerticalLayoutGroup和GridLayoutGroup继承自它。而HorizontalOrVerticalLayoutGroup同样是个抽象类,HorizontalLayoutGroup和VerticalLayoutGroup继承自它。

LayoutGroup不仅继承了ILayoutGroup,还继承了ILayoutElement,这表明它也可以作为布局元素添加到别的布局组或适应器里。

    public interface ILayoutElement{// After this method is invoked, layout horizontal input properties should return up-to-date values.// Children will already have up-to-date layout horizontal inputs when this methods is called.void CalculateLayoutInputHorizontal();// After this method is invoked, layout vertical input properties should return up-to-date values.// Children will already have up-to-date layout vertical inputs when this methods is called.void CalculateLayoutInputVertical();// Layout horizontal inputsfloat minWidth { get; }float preferredWidth { get; }float flexibleWidth { get; }// Layout vertical inputsfloat minHeight { get; }float preferredHeight { get; }float flexibleHeight { get; }int layoutPriority { get; }}

先讲继承自ILayoutGroup的方法。LayoutGroup的SetLayoutHorizontal和SetLayoutVertical是抽象方法,具体是在GridLayoutGroup、HorizontalLayoutGroup和VerticalLayoutGroup里实现的。

GridLayoutGroup的SetLayoutHorizontal和SetLayoutVertical调用了SetCellsAlongAxis方法,这个方法里根据CellSize和Spacing等参数为它包含的子对象设置尺寸和位置。

而HorizontalLayoutGroup和VerticalLayoutGroup组件的SetLayoutHorizontal和SetLayoutVertical调用了基类HorizontalOrVerticalLayoutGroup的SetChildrenAlongAxis,它会按照自己的尺寸水平或竖直分割成N份(N为子对象的数量),并为子对象设置位置和尺寸。

SetCellsAlongAxis和SetChildrenAlongAxis使用到的rectChildren(矩形子对象)是通过CalculateLayoutInputHorizontal获得的。这个方法是继承自ILayoutElement的方法,前面提到过。

这个方法将子对象中不忽略Layout的RectTransform保存在rectChildren里。

而LayoutGroup的子类各自重写了CalculateLayoutInputHorizontal以及CalculateLayoutInputVertical,除了上面的操作之外,还计算了minWidth、preferredWidth、flexibleWidth、minHeight、preferredHeight和flexibleHeight属性,而这些属性也是ILayoutElement接口的属性,而这些属性在HorizontalOrVerticalLayoutGroup计算子对象尺寸时用到。

UGUI内核大探究(十)Layout与Fitter相关推荐

  1. UGUI内核大探究(十六)InputField

    InputField是UGUI的重要组件,可以提供文本输入功能,是与用户交互的一个重要手段.我们可以在编辑器里,为OnValueChanged和OnEndEdit两个事件添加监听,这样就可以获得用户输 ...

  2. UGUI内核大探究(十八)Raycaster

    射线其实是属于事件系统,它在EventSystem/Raycasters目录下,有BaseRaycaster.PhysicsRaycaster和Physics2DRaycaster三个类,命名空间也是 ...

  3. UGUI内核大探究(十二)Slider

    Slider是UGUI的一个组件,使用它可以实现滑动条,算是一个比较常用的组件,它与ScrollBar(参考UGUI内核大探究(十一)ScrollRect与ScrollBar)有些类似,但又不太相同. ...

  4. UGUI内核大探究(十一)ScrollRect与ScrollBar

    当我们在Unity Editor里创建一个Scroll View的时候含有ScrollRect的对象,它下面还有三个子对象,两个含有ScrollBar组件的子对象是作为滚动条,一个Viewport用于 ...

  5. UGUI内核大探究(八)MaskableGraphic

    MaskableGraphic是UGUI的核心组件,它继承自Graphic.MaskableGraphic是一个抽象类,它的派生类有RawImage.Image.Text.顾名思义,MaskableG ...

  6. UGUI内核大探究(九)Image与RawImage

    Image组件是UGUI里最常用的组件(可能没有之一),我们知道其实还有一个RawImage组件.那么二者的区别是什么呢?之前的文章UGUI内核大探究(八)MaskableGraphic中我们提到过, ...

  7. UGUI内核大探究(十三)Dropdown

    Dropdown(下拉框)可谓是UGUI的集大成者,在Unity Editor里新建一个Dropdown,会随之附赠Text(Label对象).Image(Arrow对象).ScrollRect(Te ...

  8. UGUI内核大探究(二)执行事件

    UGUI内核大探究(一)EventSystem我们探究了事件系统,其中我们讲到EventSystem可以通过ExecuteEvents这个类来执行事件,那么事件是如何执行的呢?这里涉及到了两个文件Ev ...

  9. UGUI内核大探究(一)EventSystem

    2019独角兽企业重金招聘Python工程师标准>>> UGUI是Unity3D官方推出的UI系统,为了更好的使用UGUI,我们就需要去了解它. UGUI代码开源,我们可以从bitb ...

  10. C++之指针探究(十四):回调函数

    相关博文:C++之指针探究(十三):函数指针数组 相关博文:C++之指针探究(十二):指针.下标.数组及其作函数参数探究 相关博文:C++之指针探究(十一):函数名的本质和函数指针 相关博文:C++之 ...

最新文章

  1. 左耳朵耗子:程序员如何把控自己的职业?
  2. function “printf“ declared implicitly
  3. OSGi:进入微服务架构的门户
  4. 虽然自己不是唱歌的材料
  5. C++ 文字常量与常变量
  6. jvm 堆外内存_jvm┃java内存区域,跳槽大厂必会知识点
  7. 钉钉web版防撤回、屏蔽已读
  8. [C#]C#补习——控制流语句相关
  9. java之接口适配器
  10. 使用JAVA命令查看JVM参数
  11. 深度学习基础知识——信息论(自信息、信息熵与马尔科夫链)
  12. 击穿面试官的套路:经典面试问题剖析
  13. react中使用Route报错` A <Route> is only ever to be used as the child of <Routes> element, never rendered`
  14. Jim Joseph加入Burson Cohn Wolfe,担任新设立的全球职位
  15. Java 2.4(将磅转换为千克)编写程序,将磅数转换为千克数。程序提示输入用户磅数,然后转换为千克并显示结果。一磅等于0.454千克。下面是一个运行示例:
  16. linux-ubuntu常用命令(深圳文鹏)
  17. 日期 时间格式转换 时间过滤器 moment
  18. MYSQL数据库DML常见的命令
  19. 一半径为R的球沉入水中,球面顶部正好与水面相切,球的密度为1,求将球从水中取出所做的功
  20. 中文自然语言处理--基于 textCNN 的电影推荐系统

热门文章

  1. 多摩川读写EEPROM以及并口实现
  2. 硬盘测试软件测试有问题,用crystaldiskinfo工具检测出来硬盘不良怎么办
  3. 虚拟机服务器坏处,服务器虚拟化技术的优缺点
  4. php opendir(),PHP opendir()用法及代码示例
  5. 串口-TCP调试助手
  6. 基于模块化多电平换流器(MMC)的柔性直流输电系统simulink仿真模型开发
  7. 仿淘宝中心轮播图 JS[代码+详细讲解+效果图]
  8. 【数学建模】数据包络分析法
  9. Python实现汉译英
  10. Redis集群原理和总结