在UGUI1中,Canvas下的每个GameObject都会自动添加 Rect Transform
组件来控制自身的位置和大小。通常情况下,基于Rect Transform
的布局系统已经足够灵活,可以方便地满足大部分UI布局的需要。利用UGUI中的 anchor
(锚点)和 pivot
(轴点)特性,可以让UI GameObject
2实现大部分依赖父容器的定位功能,例如绑定在父容器的左上角或正中央,长度和宽度撑满父类等功能。
然而,另外的一些需要父容器依赖子类的UI功能,仅仅使用Rect Transform
则很难做到。例如,让Text文本框的大小根据字数动态变化;让列表的长度根据列表项的数量动态变化等等。它们可能满足以下条件中的一条和多条:
这种情况下,使用 Auto Layout (自动布局)往往可以达到预期的效果。
自动布局系统为具有嵌套结构的UI布局提供了可行方案(例如水平或垂直列表或表格),并且允许布局元素根据其包含的子类内容进行大小调节(例如可以根据文本调节自身大小的Text)。
自动布局系统是基于最基础的 Rect Transform
来构筑的,它可以使用在任何一个包含 Rect Transform
的元素上。
自动布局系统主要基于 layout elements
和 layout controllers
两种概念 。本文着重介绍 layout elements
。
在Unity中, Layout Element
具有广义和狭义的区别。狭义上说,Layout Element
是一种可选的UI组件;广义上说, Layout Element
指一切附加有 Rect Transform
以及 其他任意数量的UI组件的GameObject。 Auto Layout 中提到的 Layout Elements
显然指的是其广义概念。
正因为广义上说所有UI GameObject
都或多或少地扮演了 Layout Element
的角色,所以每个UI GameObject
都可以在其对应的Inspector面板中找到自身的Layout Properties参数。下面是一个新建的Text组件的Layout Properties示意图:
实际情况和图片不符?
首先底部的黑底部分是可以通过其左上角的DropDown进行切换的,其次仅含有RectTransform
的UI GameObject
是不会显示该面板的。
事实上,所有含有Rect Transform
的UI GameObject
都可以充当Layout Element
。
就像上一节中图片中显示的一样,一个广义上的Layout Element
包含以下六条属性:
它们的默认值都是0,一些特定的UI组件(Components)在附加到具体的UI GameObject
上时可以改变其布局属性。典型的例子就是Text以及Image组件,它们会改变 Preferred width
和 Preferred height
来匹配图片或文本内容。
以Text为例来测试,默认情况下Text会根据文本的内容和字体大小都因素来修改Preferred width
和 Preferred height
。当没有任何文本时,Preferred width
为 0,Preferred height
为16(与Font Size等属性有关,表示一行的高度);在Text一栏随意添加几行内容,会发现Preferred width
和 Preferred height
会根据文本的行宽和行高来动态变化。
除此之外,如果对Text自带的布局属性不满意,我们也可以通过手动添加一个 Layout Element
组件(即狭义含义)来修改其中的任何一项属性,该组件设置的属性优先于Text,Image等内置组件,并且可以同时存在多个,它们的优先级可以通过 “Layout Priority”来调节。
虽然上一节我们知道了Layout Element
包含了六种属性,并且如何查看它们。但是我们还没有涉及到一个重要的问题——它们是如何在UI布局系统中发挥作用的?
表面上看,虽然Text在文本改变时也会动态修改UI GameObject
自身的布局属性,但是它们并没有对该元素在UI中的位置和大小等信息进行任何影响。Text组件依然会在Rect Transform
中指定好的区域中渲染绘制,而这些属性完全是在游戏运行前手动指定的。所谓的Layout Element
完全没有贡献一丝一毫的力量。
事实上, Layout Element
具有一个UI GameObject
动态布局时所需要的特定信息,但是它本身并不会负责设置这些信息。还记得Auto Layout
的两大基本概念么?另一个Layout Controller
就是负责读取 Layout Element
包含的信息,然后控制一个UI元素实际的大小的。如果没有手动添加Layout controller
派系的组件,UI GameObject
上的Layout Element
完全就是摆设,它就是你空调上那些永远不会用到的按钮,因为你的空调根本久没有安装这些功能。
所以为了演示Layout Element
的真实价值,本文还是不得不使用Layout Controllers
派系的组件来配合演出,但主角依然是我们的 Layout Element
。
不同于基础的 Rect Transform
布局系统,在自动布局系统中,我们需要考虑一个UI元素在变化状态时是如何修改自身的大小的。这也是为什么布局属性需要使用三套方案设置大小,而不是单纯的固定width和 height。下面给出这些属性发挥作用的基本原则:
Minmum width/height
最先被分配,不带任何妥协Preferred width/height
会被分配flexible width/height
会被继续分配没有听懂吧?没关系,听懂了本文剩余的部分就都是废话了,你可以关掉来节省几分钟的时间休息。下面将通过一个逐步实现的实验来讲解这三条原则的含义。
本小节会使用一个Layout Controller
派系的组件来辅助实验的进行,它的名字叫做 Horizontal Layout Group
。你暂时不需要对它有过多的了解,只需要知道它是让Layout Element
生效的魔法口诀就行了。
实际上,
Horizontal Layout Group
属于Layout Group
的一种,用于快速创建一种横向的布局结构,如果把UI GameObject
作为子类放在其下,它们就会像排队一样整齐地展开。
创建下面演示的Hierarchy结构,Canvas是画布对象,可以通过在Hierarchy中 右键-UI-Canvas
来创建。Parent和两个子对象都是Create Empty
创建得到,只包含 Rect Transform
组件。将Parent的Width设置为40, Height设置为20。
随后在 Parent,Child1以及Child2上分别添加一个Image
组件,用于直观地显示三种容器的大小。颜色最好采用不同的方便区分。这里我采用的颜色是Parent(蓝色),Child1(粉色), Child2(绿色)。
-Canvas
-Parent
-Child1
-Child2
最后也是最重要的一步,就是在Parent上添加一个Horizontal Layout Group
组件,不出意外的话,你可以看到如图所示的效果。
如果你还记得上一节学过的知识,可能会产生一些疑问:单独查看Child的Layout Properties时,会发现
Minmum width/height
和Preferred width/height
都是 0,如果Layout Controller
真的生效了,为什么两张图片的大小不是0? 原因就在于虽然Horizontal Layout Group
属于Layout Controller
,但它还在背地里偷偷地修改了Layout Element
的计算规则,当然下一节我们就会屏蔽这种效果。
为了观察Minmum width/height 对图片大小的影响,我们需要对Horizontal Layout Group
进行一些小幅的修改。如下图所示:
这时再观察显示效果,你会发现两张Child图片都“消失”不见了。这时因为子类的Image设置的Layout Element
属性起了作用。由于两张图片的Minmum和Preferred大小都为0,所以在Layout Controller
的驱动下,它们的大小都变成了0。不仅如此,如果你是一个善于观察的人,还可以发现 Rect Transform
中调节 Width和Height的属性都变成了灰色。这也从侧面表明Layout Element
中设置的属性成功生效,并覆盖了Rect Transform
中的属性。
这也解决了上一节末尾提出的问题。你也可以为Image组件添加一张Source Image观察一下变化。
当然,有的情况下我们不想使用Image为我们设置的布局属性。这时我们就可以添加Layout Element
来覆盖这一默认属性。为Child1和Child2分别添加一个Layout Element
组件,并且将它们的Min Width/Height设置为10。由于Layout Element
设置的布局属性优先级高于Image组件,所以两个Child又会以10*10的大小再次出现。随后以20*20,30*30的大小再次测试,可以看到下图所示的效果。
不知道你有没有发现规律?实际上,Min Width/height 是最低标准,在动态布局中,每个布局元素至少要保持其自身Min Width/Height 的大小,这一大小不会因为父类容器的大小而产生任何改变,完全没有妥协的余地。因此,这一条规则也最容易理解。
现在进入实验的第二阶段,即Preferred Width/Height 的作用原理。先将两个Child子类的 Min Width/Height 恢复成 10*10,然后将Preferred Width/Height设置成 20*20。
还记得Parent的大小么?没错,40*20。我们这次通过修改Parent的Width来观察两个Child的变化情况。
Okay, 现在来总结一下规律:
flexible size
的情况)。现在进入第三个阶段,让我们看看Flexible Width/Height
是如何生效的。如果你实际操作了上面的两个过程,可能会发现Min
和Preferred
都是以常规单位计算的,然而Flexible
则是以相对单位计算的。如果任何广义上的Layout Element
拥有大于0的Flexible Size
,就意味着所有的剩余空间都会被其填满。当然,有很大的几率同时存在多个子容器拥有大于0的Flexible Size
,这种情况下它们不会像Preferred Size
一样简单的平分,而是根据Flexible Size
的数值比例平分。
当然,仅当满足Min Size
以及Preferred Size
的计算后,才会考虑Flexible Size
。为了方便测试,我们可以先取消Layout Element
中 Min Size
和Preferred Size
的设置(这样两个Child都只会使用Image组件设置的默认值0,对Flexible Size
计算不会产生任何影响)。下面是三种不同的Flexible Size
比例下两种子容器的大小情况。
还记得那个”Horizontal Layout Group“么?在实验的第一步我们修改了其中的两个选项——即
Child Controls Size
和Child Force Expand
。前一项的作用就是屏蔽掉这个Layout Controller
对布局属性的影响,完全使用子容器的布局属性进行设置;后一项则是将所有子容器的Flexible Size
都设置为1,这样就可以让所有子容器以填充的方式平分父容器。
本文以我个人的视角大致讲解了一下Layout Element
的原理和如何查看和设置布局元素对应的布局属性。在使用时一定要牢记Layout Element
对应的布局属性并不会自动生效,必须以Layout Controller
作为驱动。欢迎各位程序猿朋友给我留言,有什么疑问我会尽快答复。
Unity Manual - Auto Layout
Unity Manual - Layout Element
Rect Transform
,在UI中发挥光和热的GameObject。 ↩