Unity所有UGUI组件的基础——Layout Element

问题背景

在我制作人物背包的过程中,需要当鼠标放在物体上时显示一些字符串表示物品的信息,所以我希望所显示的文本框大小以及背景图片大小都能随我填入的文字多少而变化,并且我需要有一个最大宽度,即超出宽度后应当换行竖向增长而不是无限制的横向变长,并且文本应当在背景的中央。

解决方案

层级关系:

我们先分析一下问题,父物体Inspect Window中包含image背景图,它要能够随着子物体Inspect Text大小变化而变化,Inspect Text大小需要根据组件Text中的文字数目变化,并且Inspect Text应当位于Inspect Window下正确的位置(居中)

Inspect Text用到的组件

Unity所有UGUI组件的基础——Layout Element_第1张图片
首先我们肯定要从最根源的位置入手也就是字符串的长度,它引起了之后一系列的变化。
Text这一组件并没有什么特殊之处可以针对字符串长度提供外部方法或者什么选项,但是它是一个UI组件,换句话说它是一个Layout Element,这就是这个问题最关键的地方,Unity的整个ugui系统包含了很多组件,他们互相之间是有共同之处的,拥有Layout Element属性是其中之一。
实际上所有的ui组件大致分为两类,一类就是像Text,Layout Element这样的只包含Layout Element信息但自己并不会主动调整大小的组件;另一类像layout group, scroll rect,content size fitter等控件他们除了本身含有Layout Element信息外还会根据自己或子物体的Layout Element去调整大小
在inspector窗口最下方你可以查看当前gameobject或者说rect transform的layout property
它是直接决定我们最终效果的数据,你会发现它实际上就是一个Layout Element,只不过它其中的数据来自于这个物体上挂载的各个组件,实际上这是由各组件的优先级Layout Priority决定的
Unity所有UGUI组件的基础——Layout Element_第2张图片
在了解这些后问题的解决就明了起来了,如果我们希望“自适应变化”,我们就需要一个“ui控件”,如果我们想要按照Text变化,就要保证Text的Layout Element的优先级最高。比较遗憾的一点是unity内置的ui组件的优先级都已经是确定的,我们能做的就是引入一个Layout Element组件并给他赋予更高的优先级来覆盖一些已有的组件。
关于Layout Element组件的参数含义网上很多就不赘述了,Content Fitter也是对应的。
传送门
这里还有一个小知识点,就是控件对ui组件大小的控制是以pivot为参考的,关于pivot就又是另一大块知识了,但我们只需要知道pivot与物体的中心之类的东西没有关系,它就是为了ui元素的放缩旋转等操作而设立的,它的坐标是以物体本身为单位的,(0,0)代表左下角,(1,1)代表右上角,而content fitter参考pivot为0表示向上扩展,0.5表示中间(不扩展),1表示向下扩展,横向同理为左右。所以我们当然需要把pivot设置为(1,1)才能让content fitter正确工作。
现在如果我们不添加自定义的Layout Element则结果会是这样的
Unity所有UGUI组件的基础——Layout Element_第3张图片

如果给Text中随意填写一些文字,则只有Preferred Width会发生改变
Unity所有UGUI组件的基础——Layout Element_第4张图片
于是我们以及知道Text这一组件是如何设置他的Layout Element的了,在文本中没有换行的情况下,它根据字体大小设置高度,随着文字增多增加宽度,并且都是preferred值,并没有启用其他的属性
这显然不能满足我们自动换行并维持最大宽度的要求,于是我们需要添加一个更高优先级的自定组件来覆盖。
Unity所有UGUI组件的基础——Layout Element_第5张图片
覆盖后的数据
Unity所有UGUI组件的基础——Layout Element_第6张图片
这个Preferred Width就是我们所期望的最大宽度(实际上是固定宽度,如果真的要完成Min Width到Preferred Width的自适应则需要代码来动态设置新添加的Layout Element(很简单,一个min函数就可以完成,但因为实际效果差别不大所以就采用这一固定宽度了),然后我们保留Text的Preferred Height因为它可以感知文本长度变化
于是现在我们就有了一个固定宽度,随文本长度向下扩展的文本ui物体

Inspect Window用到的组件

Unity所有UGUI组件的基础——Layout Element_第7张图片

我们现在要来处理父物体了,实际上要让父物体随子物体大小改变是很困难的,没有直接的组件可以实现这一点,我们需要多个组件配合实现或者使用代码辅助实现。
首先我们需要一个像Text感知文本长度一样感知子物体大小变化的组件,Vertical Layout Group可以胜任,并且还可以子物体置中Unity所有UGUI组件的基础——Layout Element_第8张图片
注意我们并不想用它控制子物体大小,所以不勾选ControlChildSize和Expand
添加后的效果
Unity所有UGUI组件的基础——Layout Element_第9张图片
由于Padding,它会根据子物体改变自身的Layout Element,我们正是利用这一点,然后再添加ContentFitter,显然Image的优先级更高(也有可能是VLG没有设置Preferred值,但经我测试发现并不是,可以确定是image优先级更高),我们无法改变这一点,但Image并没有设置Min选项,我们可以将ContentFitter设置为取Min,即可利用VerticalLayoutGroup的布局数据
请添加图片描述
我们应该正确理解ContentFitter中各个选项的正确含义,我在最初以为Min是边界值而Preferred是变化值,但实际上无论哪个值都只是一个确定的数而已,没有哪一个值本身就是变化(自适应)的,只是由其他Layout Element组件来改变他们,就像几个通道一样供你获取数据,对于Layout Element而言Min意味着最小值不可妥协,而对于ContentFitter而言Min就只是一个通道,如果你想访问的Layout Element在这个通道拥有最高优先级你就可以通过它去访问对应的Layout Element。
除此以外用代码也可以解决这个问题,你可以使用代码访问子物体大小设置Layout Element或者直接设置RectTransform(效果是一样的,因为手动添加的Layout Element拥有最高优先级)

/*rectTransform.sizeDelta = new Vector2(
    Mathf.Max(inspectText.GetComponent().sizeDelta.x * 1.2f, minWindow.x),
    Mathf.Max(inspectText.GetComponent().sizeDelta.y * 1.2f, minWindow.y)
);*/

这样的话可以实现按比例倍数调整,而不是像Padding一样的固定值。

小细节

ok,我们似乎已经把一切事情处理好了,当我们通过代码设置text值后一切就似乎应当正确工作,但实际运行起来并不如意,背景图片没有正确按照大小变化
请添加图片描述
经过观察,直到再一次把鼠标放上时才会正常,这是因为我们忽略了一个关于代码执行顺序(生命周期)的问题。
我们知道当激活一个物体时会先激活父物体再激活子物体,所以如果在修改text后直接激活父物体Window,window的尺寸就会被设置为text修改前对应的size(因为子物体尚未被激活,Size也没有随着文本多少而变化),并且我们可以看出父物体Vertical Layout Group的Layout Element并不是在每帧都在调整改变(因为在子物体size随着text改变后,父物体Vertical Layout Group也没有重新修改他的Layout Element(至少没有修改它的min值),直到被关闭然后重新激活才修改)。
言外之意是我们不能通过修改text然后激活父物体来达到效果,那么我们先激活子物体再激活父物体可以吗,答案是不行,因为当你setactive子物体后子物体上的脚本不会立即执行(不会立即resize,应该是到下一帧才会执行),然后你再setactive父物体,按照prefab中物体执行顺序为由根节点向下,最后仍然会先执行父物体的resize。

Tips:非prefab的执行顺序与被创建出的时间有关(也就是Hirarchy中越底下的越先执行)

所以唯一的办法是我们在修改text前先将父物体的Vertical Layout Group unenable,待修改完后再enable,并且保持父子物体都处于active状态,这样Vertical Layout Group就会重新设置自己的Min值并且是在子物体resize之后(下一帧)。
此外我还加了一个小协程来保证当一切被正确设置以后再将窗口变为可见(这里使用canvas group来隐藏也是因为上面的原因,因为你必须保证子物体为激活状态而且不可见),这里需要两个EndOfFrame也证实了上述观点,从Text长度被改变的开始,第一帧子物体被resize因为text被改变,并且父物体Vertical Layout Group被enable,第二帧父物体被resize,然后在第二帧结束时将Window正确的展现出来。

private IEnumerator ShowInspect()
{
    yield return new WaitForEndOfFrame();
    yield return new WaitForEndOfFrame();
    GetComponentInParent<CanvasGroup>().alpha = 1;
}

你可能感兴趣的:(unity,unity,c#,游戏开发)