Unity/Auto Layout -- 理解Layout Elements(布局元素)

前言

在UGUI1中,Canvas下的每个GameObject都会自动添加 Rect Transform 组件来控制自身的位置和大小。通常情况下,基于Rect Transform的布局系统已经足够灵活,可以方便地满足大部分UI布局的需要。利用UGUI中的 anchor(锚点)和 pivot(轴点)特性,可以让UI GameObject2实现大部分依赖父容器的定位功能,例如绑定在父容器的左上角或正中央,长度和宽度撑满父类等功能。

然而,另外的一些需要父容器依赖子类的UI功能,仅仅使用Rect Transform则很难做到。例如,让Text文本框的大小根据字数动态变化;让列表的长度根据列表项的数量动态变化等等。它们可能满足以下条件中的一条和多条:

  1. 容器的具体大小在设计阶段无法提前预知
  2. 父容器的大小可能会在运行时动态变化
  3. 父容器的布局很大程度上依赖于子类

这种情况下,使用 Auto Layout (自动布局)往往可以达到预期的效果。

概念

Auto Layout

自动布局系统为具有嵌套结构的UI布局提供了可行方案(例如水平或垂直列表或表格),并且允许布局元素根据其包含的子类内容进行大小调节(例如可以根据文本调节自身大小的Text)。

自动布局系统是基于最基础的 Rect Transform 来构筑的,它可以使用在任何一个包含 Rect Transform 的元素上。

自动布局系统主要基于 layout elementslayout controllers 两种概念 。本文着重介绍 layout elements

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示意图:

Layout Properties

实际情况和图片不符?
首先底部的黑底部分是可以通过其左上角的DropDown进行切换的,其次仅含有RectTransformUI GameObject是不会显示该面板的。
事实上,所有含有 Rect TransformUI GameObject都可以充当Layout Element

Layout Properties(布局属性)

就像上一节中图片中显示的一样,一个广义上的Layout Element包含以下六条属性:

  1. Minmum width
  2. Minmum height
  3. Preferred width
  4. Preferred height
  5. Flexible width
  6. Flexible height

它们的默认值都是0,一些特定的UI组件(Components)在附加到具体的UI GameObject上时可以改变其布局属性。典型的例子就是Text以及Image组件,它们会改变 Preferred widthPreferred height来匹配图片或文本内容。

以Text为例来测试,默认情况下Text会根据文本的内容和字体大小都因素来修改Preferred widthPreferred height。当没有任何文本时,Preferred width为 0,Preferred height为16(与Font Size等属性有关,表示一行的高度);在Text一栏随意添加几行内容,会发现Preferred widthPreferred height会根据文本的行宽和行高来动态变化。

Text修改布局属性示意图

除此之外,如果对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

Layout Property 如何发挥光和热

不同于基础的 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组件,不出意外的话,你可以看到如图所示的效果。

Step1

如果你还记得上一节学过的知识,可能会产生一些疑问:单独查看Child的Layout Properties时,会发现 Minmum width/heightPreferred width/height 都是 0,如果Layout Controller 真的生效了,为什么两张图片的大小不是0? 原因就在于虽然Horizontal Layout Group属于Layout Controller,但它还在背地里偷偷地修改了Layout Element的计算规则,当然下一节我们就会屏蔽这种效果。

Minmum width/height

为了观察Minmum width/height 对图片大小的影响,我们需要对Horizontal Layout Group进行一些小幅的修改。如下图所示:
LayoutGroupSetting

这时再观察显示效果,你会发现两张Child图片都“消失”不见了。这时因为子类的Image设置的Layout Element属性起了作用。由于两张图片的Minmum和Preferred大小都为0,所以在Layout Controller的驱动下,它们的大小都变成了0。不仅如此,如果你是一个善于观察的人,还可以发现 Rect Transform中调节 Width和Height的属性都变成了灰色。这也从侧面表明Layout Element中设置的属性成功生效,并覆盖了Rect Transform中的属性。

Child属性

这也解决了上一节末尾提出的问题。你也可以为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_Size

不知道你有没有发现规律?实际上,Min Width/height 是最低标准,在动态布局中,每个布局元素至少要保持其自身Min Width/Height 的大小,这一大小不会因为父类容器的大小而产生任何改变,完全没有妥协的余地。因此,这一条规则也最容易理解。

Preferred Width/Height

现在进入实验的第二阶段,即Preferred Width/Height 的作用原理。先将两个Child子类的 Min Width/Height 恢复成 10*10,然后将Preferred Width/Height设置成 20*20。

还记得Parent的大小么?没错,40*20。我们这次通过修改Parent的Width来观察两个Child的变化情况。
Preferred_Size

Okay, 现在来总结一下规律:

  1. 无论父容器如何变化,子类始终满足 Min Size (如 15 * 20所示)
  2. 如果子类在满足Min Size的情况下,父容器还有多余的空间可供使用,那么所有拥有Preferred Size的子类会平分多余的空间。(如30*20)。
  3. 当父类空间分配给单个子类的空间超过Preferred Size后,该子类的大小不会继续增长(仅限未设置flexible size的情况)。
Flexible Width/Height

现在进入第三个阶段,让我们看看Flexible Width/Height是如何生效的。如果你实际操作了上面的两个过程,可能会发现MinPreferred都是以常规单位计算的,然而Flexible则是以相对单位计算的。如果任何广义上的Layout Element拥有大于0的Flexible Size,就意味着所有的剩余空间都会被其填满。当然,有很大的几率同时存在多个子容器拥有大于0的Flexible Size,这种情况下它们不会像Preferred Size一样简单的平分,而是根据Flexible Size的数值比例平分。

当然,仅当满足Min Size以及Preferred Size的计算后,才会考虑Flexible Size。为了方便测试,我们可以先取消Layout ElementMin SizePreferred Size的设置(这样两个Child都只会使用Image组件设置的默认值0,对Flexible Size计算不会产生任何影响)。下面是三种不同的Flexible Size比例下两种子容器的大小情况。

Flexible Width

还记得那个”Horizontal Layout Group“么?在实验的第一步我们修改了其中的两个选项——即Child Controls SizeChild Force Expand。前一项的作用就是屏蔽掉这个Layout Controller对布局属性的影响,完全使用子容器的布局属性进行设置;后一项则是将所有子容器的Flexible Size都设置为1,这样就可以让所有子容器以填充的方式平分父容器。

结语

本文以我个人的视角大致讲解了一下Layout Element的原理和如何查看和设置布局元素对应的布局属性。在使用时一定要牢记Layout Element对应的布局属性并不会自动生效,必须以Layout Controller作为驱动。欢迎各位程序猿朋友给我留言,有什么疑问我会尽快答复。

扩展阅读

Unity Manual - Auto Layout
Unity Manual - Layout Element


  1. Unity引擎自带的UI系统,与之类似的有Unity4时代常用的第三方UI系统NGUI。
  2. 本文中的 “UI GameObject” 指的都是位于Canvas下,附加有 Rect Transform,在UI中发挥光和热的GameObject。
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页