uGUI知识点剖析之AutoLayout
http://www.2fz1.com/post/unity-ugui-autolayout/
uGUI知识点剖析之AutoLayout
前文详细介绍过RectTransform,RectTransform作为单个UI元素布局是十分灵活的,但是当一组UI元素需要规律的排布时,就需要“AutoLayout”这种组级别的布局方式。AutoLayout能决定子元素的排布方式,同时子元素也可以影响父元素的排布。相对于RectTransform,AutoLayout的功能更容易被理解和上手。
---第一到第三部份比较基础,熟悉的同学可以直接跳过---
一、概要
自动布局系统主要提供两种功能“layout controllers(布局控制器)”和“layout elements(布局元素)”。一般情况下布局控制器是父元素如何控制子元素的布局,而布局元素是子素控制自己本身的布局大小并可以影响父元素的布局方式。

二、布局控制器
(一)、Layout Group(布局组)
布局组提供Horizontal Layout Group(水平布局组)和Vertical Layout Group(垂直布局组),以及Grid Layout Group(网格布局组)三种功能。
这三个布局组参数基本一致,详细说明可以参考官网文档:官网文档。但也有一些差别。
1、水平和垂直布局组特有的属性:Child Force Expand

勾选Child Force Expand,如果父元素有额外可用空间时,会促使子元素强制扩充。一般配合布局元素组件的minimum,preferred和flexible尺寸使用。
2、网格布局组
- Start Corner和Child Alignment的区别:Start Corner是决定子元素排放顺序的开始位置,Child Alignment是决定所有子元素作为一个整体在父元素中的排放位置。
- 网格布局组,子布局元素设定的尺寸信息无效。在网格布局组下,其子布局元素设置的minimum,preferred和flexible尺寸无法生效,只会生效网格设定的尺寸。在网格布局组下,子布局元素只能通过Ignore Layout跳出布局控制器,其它属性设置无效。

(二)、控制自身的布局控制器
Content Size Fitter(内容尺寸适配器)和Aspect Ratio Fitter(宽高比适配器)是控制自身尺寸的布局控制器。
Content Size Fitter(内容尺寸适配器)

通过子元素设定的布局元素minimum,preferred尺寸或内容本身的显示尺寸来调整本身的尺寸信息。最常用于文本内容的父元素,父元素通过文本内容的长度动态设置自身的尺寸。
Aspect Ratio Fitter(宽高比适配器)

通过调整自身的宽或高来对应调整另一边的尺寸,也可以选择填满父元素。和网格布局组一样,子布局元素设定的尺寸信息无效。
三、布局元素
布局元素是一个含有RectTransform组件的GameObject,作为布局元素并不能直接更改RectTransform中的尺寸信息,只能通过挂载“layout elements component”这个组件来设置布局信息,以供布局控制器计算。

(图:layout elements component)
- Minimun width(最小宽度)
- Minimum height(最小高度)
- Preferred width(期望宽度,相当于最大宽度)
- Preferred height(期望高度,相当于最大高度)
- Flexible width(灵活宽度,一般是相对父元素的比例)
- Flexible height(灵活高度,一般是相对父元素的比例)
四、布局接口(自定义布局功能)
可参考官方uGUI开源代码:https://bitbucket.org/Unity-Technologies/ui

(一)、布局主要接口
- ILayoutController:布局控制器
- ILayoutElement:布局元素(布局控制器本身也是布局元素,LayoutGroup也实现它)
- ILayoutSelfController:实现此接口,表明组件需要驱动自身的RectTransform。目前有:ContentSizeFitter、AspectRatioFitter。
(二)、LayoutGroup实现
- 实现CalculateLayoutInputHorizontal方法,将没有忽略布局的子布局元素添加到列表。
- GetStartOffset方法,根据childAlignment计算子元素开始排布的起始位置。
- SetChildAlongAxis方法,按指定起始位置和尺寸添加子元素,未开源。
(三)、HorizontalOrVerticalLayoutGroup实现
HorizontalOrVerticalLayoutGroup是布局组控制器的具体实现类,包含两个方法CalcAlongAxis和SetChildrenAlongAxis。
- CalcAlongAxis:计算所有子布局元素的totalMin、totalPreferred、totalFlexible
- SetChildrenAlongAxis:通过CalcAlongAxis计算的值,进行子元素起始位置和子元素尺寸的最终计算,并调用LayoutGroup.SetChildAlongAxis添加子元素。
此段代码比较关键,故直接贴出来:
HorizontalOrVerticalLayoutGroup.cs
protected void SetChildrenAlongAxis(int axis, bool isVertical)
{
float size = rectTransform.rect.size[axis];
bool alongOtherAxis = (isVertical ^ (axis == 1));
if (alongOtherAxis)
{
float innerSize = size - (axis == 0 ? padding.horizontal : padding.vertical);
for (int i = 0; i < rectChildren.Count; i++)
{
RectTransform child = rectChildren[i];
float min = LayoutUtility.GetMinSize(child, axis);
float preferred = LayoutUtility.GetPreferredSize(child, axis);
float flexible = LayoutUtility.GetFlexibleSize(child, axis);
if ((axis == 0 ? childForceExpandWidth : childForceExpandHeight))
flexible = Mathf.Max(flexible, 1);
float requiredSpace = Mathf.Clamp(innerSize, min, flexible > 0 ? size : preferred);
float startOffset = GetStartOffset(axis, requiredSpace);
SetChildAlongAxis(child, axis, startOffset, requiredSpace);
}
}
else
{
float pos = (axis == 0 ? padding.left : padding.top);
if (GetTotalFlexibleSize(axis) == 0 && GetTotalPreferredSize(axis) < size)
pos = GetStartOffset(axis, GetTotalPreferredSize(axis) - (axis == 0 ? padding.horizontal : padding.vertical));
float minMaxLerp = 0;
if (GetTotalMinSize(axis) != GetTotalPreferredSize(axis))
minMaxLerp = Mathf.Clamp01((size - GetTotalMinSize(axis)) / (GetTotalPreferredSize(axis) - GetTotalMinSize(axis)));
float itemFlexibleMultiplier = 0;
if (size > GetTotalPreferredSize(axis))
{
if (GetTotalFlexibleSize(axis) > 0)
itemFlexibleMultiplier = (size - GetTotalPreferredSize(axis)) / GetTotalFlexibleSize(axis);
}
for (int i = 0; i < rectChildren.Count; i++)
{
RectTransform child = rectChildren[i];
float min = LayoutUtility.GetMinSize(child, axis);
float preferred = LayoutUtility.GetPreferredSize(child, axis);
float flexible = LayoutUtility.GetFlexibleSize(child, axis);
if ((axis == 0 ? childForceExpandWidth : childForceExpandHeight))
flexible = Mathf.Max(flexible, 1);
float childSize = Mathf.Lerp(min, preferred, minMaxLerp);
childSize += flexible * itemFlexibleMultiplier;
SetChildAlongAxis(child, axis, pos, childSize);
pos += childSize + spacing;
}
}
(四)、渲染
根据上面的类和接口示意图,当UI元素RectTransform发生变化时,并不会立即触发UI重建。为了提高性能,节省开销,uGUI也是在帧的末尾(渲染发生之前)才会进行重新进入UI重建。主要是通过Canvas.willRenderCanvases事件进行触发。
在uGUI布局系统中,主要是通过LayoutRebuilder类和CanvasUpdateRegistry类实现的。
- 继承UIBehaviour的类,在UI重建事件触发时,调用
SetDirty方法,再调用LayoutRebuilder.MarkLayoutForRebuild。 - CanvasUpdateRegistry类主要负责队例管理,排序,并注册
Canvas.willRenderCanvases事件,以接受事件触发队列中的重建方法。 - LayoutRebuilder类是真正实现UI重建的类,通过
Rebuild方法,会先调用CalcAlongAxis计算所有子布局元素的totalMin、totalPreferred、totalFlexible。再调用SetChildrenAlongAxis给子元素设置最终的起始位置和子元素尺寸。
五、弹性布局(也有叫:灵活布局)
弹性布局是在有限的空间内按需分配置空间,一个子元素分配多了,剩下的子元素就少了,子元素之间按一定比例分配。在uGUI中表现为,一个父元素,使用了水平布局组或垂直布局组(网格布局组无效)。子元素设置Flexibl width或Flexibl height。
一个疑惑的实例
按以前类似的弹性布局UI(比如:CSS3的box-flex属性)实例,可以有如下推断。以弹性宽度为例:所有子元素的FlexiblWidth标记为totalFlexibleWidth,单个子元素的实际宽度=父元素宽 (FlexiblWidth / totalFlexibleWidth)。*(注意:此段待商议)
我们来一段实例:
- 一个Panel(垂直布局组)下面有两Button(ButtonA和ButtonB)
- Panel高度为100
- ButtonA的FlexiblHeight为:2
- ButtonB的FlexiblHeight为:3
按以上推断结果:
- ButtonA.height = 100 * (2 / (2+3)) = 40
- ButtonB.height = 100 * (3 / (2+3)) = 60
伤神的,实际的结果并非如此,实际结果为:
- ButtonA.height = 42
- ButtonB.height = 58
这是为什么呢,uGUI的官方文档又不说清楚,真是无比坑。好在现在uGUI的源码了,回到上文的HorizontalOrVerticalLayoutGroup.SetChildrenAlongAxis方法。
//axis是一个标志位,0表示宽度,1表示高度;size是实际的尺寸
float minMaxLerp = Mathf.Clamp01((size - GetTotalMinSize(axis)) / (GetTotalPreferredSize(axis) - GetTotalMinSize(axis)));
float itemFlexibleMultiplier = (size - GetTotalPreferredSize(axis)) / GetTotalFlexibleSize(axis);
float childSize = Mathf.Lerp(min, preferred, minMaxLerp);
childSize += flexible * itemFlexibleMultiplier;
根据以上代码,关键信息终于暴露了,不考虑flexible时,childSize是min和preferred的一个插值,而插值系数minMaxLerp的值,见以上公式。也就是如果min和preferred都不为0时,childSize是取两者之间的一个插值,可以理解为百分比。
重要:只有当size>totalPreferredSize时,itemFlexibleMultiplier才不会为0,设置了flexible值才会有效。也就是说弹性布局只有在所有子元素PreferredSize之和小于父元素实际尺寸,才会有效。
弹性尺寸计算公式
flexible / totalFlexibleSize * (size - totalPreferredSize)
解读为,弹性比例乘以未分配的空间尺寸。
最终的childSize是min和preferred之间的一个插值,再叠加弹性尺寸所得。
再回到上文这个疑惑的例子,根据上面的公式,弹性尺寸和Preferred的设置是有关的,那么Button是不是有默认的PreferredSize呢?Button上默认会挂载一个Image,通过脚本获取,Button.Image默认的Preferred为10。
根据以上弹性尺寸的全新理解,我们再来推断以上例子的结果。
- size=100
- ButtonA.Image.minHeight=0
- ButtonA.Image.preferredHeight=10
- ButtonAHeight=10f(min和preferred之间的插值)
- ButtonAHeight+=2/(2+3) * (100-(10+10))(弹性尺寸)
- ButtonAHeight=42(与实际结果一致)
“案子破了”,如果uGUI文档能说详细一点,就不用大费周章才能了解一个简单的属性值。
一个通俗的例子
说一个隔壁老王家分房的案例,以加深对弹性布局的理解。
老王家三个孩子,分别叫:王一、王二、王三。老王有一套面积200平米的房子,需要给他们分房。
1、按比例分配
老王偏心,特别喜欢老幺王三,故给王三分配了比例为2。王一,王二分别分配比例为1。大家都知道弹性布局是受min和preferred影响,也就是老王的儿子们有最小分配和期望分配。为了完全按比例分配,老王对儿子们的期望都不管不顾,将min和preferred设为了0.

2、部份按比例
王三和老王,他必须要80平米,剩下的120平,王一和王二一人一半,这种情况下怎么分呢?
理论上只要给王三分80,给王一和王二flexible设为1,即可。但实际结果又不并不是所期望的样子,为什么呢?其实这种情况下需要老王把childForceExpandWidth设为false,因为在uGUI处理中,如果childForceExpandWidth设为了true,即使王三没有设置flexible,也会被强制设为1。
if ((axis == 0 ? childForceExpandWidth : childForceExpandHeight))
flexible = Mathf.Max(flexible, 1);
uGUI知识点剖析之AutoLayout的更多相关文章
- uGUI知识点剖析之RectTransform
http://www.2fz1.com/post/unity-ugui-recttransform/#jtss-tsina uGUI知识点剖析之RectTransform 一.基本要点 RectTra ...
- 《众妙之门——精通CSS3》一书知识点剖析
不得不佩服京东的速度,昨天刚下单的两本书今天上午就到了.其中一本是全彩页的<众妙之门 - 精通CSS3>,细看了前几十页,书上的叙述方式给我的印象其实不如“彩页”来的讨喜——接连说上几个例 ...
- Android知识点剖析系列:深入了解layout_weight属性
摘录自:http://www.cnblogs.com/net168/p/4227144.html 前言 Android中layout_weight这个属性对于经常捣鼓UI的我们来说,肯定不会陌生.但是 ...
- Unity UGUI知识点
1.Canvas 属性:Screen Space Overlay -画布随屏幕大小改变而改变,面板不会被其他控件挡住 Screen Space camera 面板能被其他控件挡住 world spac ...
- Android的log日志知识点剖析
log类的继承结构 Log public final class Log extends Object java.lang.Object ↳ android.util.Log log日志的常用方法 分 ...
- 黑马程序员:轻松精通Java学习路线连载1-基础篇!
编程语言Java,已经21岁了.从1995年诞生以来,就一直活跃于企业中,名企应用天猫,百度,知乎......都是Java语言编写,就连现在使用广泛的XMind也是Java编写的.Java应用的广泛已 ...
- 好用的SQLAlchemy
准备 安装SQLAlchemy框架 测试代码 知识点剖析 引入库支持 基类和引擎 实体类 声明类 数据库自动完成 CRUD 总结 这里简单的记录一下本人第一次使用SQLAlchemy这个ORM框架的过 ...
- 零基础的人怎么学习Java
编程语言Java,已经21岁了.从1995年诞生以来,就一直活跃于企业中,名企应用天猫,百度,知乎......都是Java语言编写,就连现在使用广泛的XMind也是Java编写的.Java应用的广泛已 ...
- 关于android的一些博文收集
Java网络编程总结 http://www.cnblogs.com/oubo/archive/2012/01/16/2394641.html Android应用系列:双击返回键退出程序 http:// ...
随机推荐
- 微信小程序------联动选择器
picker 从底部弹起的滚动选择器,现支持五种选择器,通过mode来区分,分别是普通选择器,多列选择器,时间选择器,日期选择器,省市区选择器,默认是普通选择器. 先来看看效果图: 1:普通选择器 m ...
- String C++完整实现。
String C++实现 改进: /* 版权信息:狼 文件名称:String.h 文件标识: 摘 要:对于上版本简易的String进行优化跟进. 改进 1.(将小块内存问题与大块分别对待)小内存块每个 ...
- 1-25-循环控制符break、continue和函数详解
大纲: 1-for循环补充 1-1-for循环实战---类C格式应用 2-break.continue循环控制符 2-1实战:帮助理解break.continue作用 3-函数详解 3-1.脚本文件中 ...
- neutron源码分析(一)OpenStack环境搭建
一.OpenStack安装 安装一个初始化的Mitaka版本的OpenStack环境用于分析,neutron源码 序号 角色 IP地址 版本 1 controller 172.16.15.161 mi ...
- 007PHP基础知识——类型转换 外部变量
<?php /**类型转换 */ /*1.自由转换*/ /*2.强制转换:不改变原变量,生成新的变量*/ //转换为字符串: /*$a=100; $b=(string)$a; var_dump( ...
- (MSSQL)sp_refreshview刷新视图失败及更新Table字段失败的问题解决
在近期工作中遇到一个任务,需要批量更改散布在很多Table中的某字段,同时刷新相关视图,但是在执行脚本时,发现了如下问题 更新字段问题 消息 ,级别 ,状态 ,第 行 对象'View_Simple' ...
- jQuery实现点击式选项卡
参考:jQuery权威指南jQuery初步jQuery选择器jQuery操作domjQuery操作dom事件jQuery插件jQuery操作AjaxjQuery动画与特效jQuery实现导航栏jQue ...
- react 入门的好东西 可以做出一个完整的网站
链接 (包含了antd 组件的使用) 安装依赖报错问题 可能需要按顺序安装, 不能cnpm npm 混合安装, 参考这个package.js ...
- iOS通讯录相关知识-浅析
本文来自于:贞娃儿的博客 http://blog.sina.com.cn/zhenwawaer 在开发一些应用中,我们如果需要iPhone设备中的通讯录信息.或者,需要开发通讯录相关的一些功能.那 ...
- Spring之基本关键策略
目的 为了简化Java开发. 策略 基于POJO(普通Java类)的轻量级和最小侵入性编程: 通过依赖注入(DI)和面向接口实现松耦合: 基于切面和惯例进行声明式编程: 通过切面和模板减少样板式代码. ...