您的位置:首页 > 移动开发 > Unity3D

Unity:RectTransfrom瞎探索

2015-09-22 00:03 597 查看

RectTransform布局瞎探索

RectTransform布局瞎探索
预备工作
1 anchoredMin和anchoredMax

2 pivot

3 offsetMinoffsetMax和sizeDelta

4 anchoredPosition
Anchor reference point

盲人摸象
1 RectTransform布局计算第一步的猜想
1a 猜想验证

2 RectTransform布局计算第二步的猜想

3 RectTransform布局计算第三步的猜想

4 RectTransform布局计算第四步的猜想

重新梳理一遍
1 anchorsRect

2 anchorsRefPoint

3 absPivot

4 originPivotRect

5 offsetMin和offsetMax

6 finalRect

7 验证代码

1. 预备工作

为了方便观察RectTransform内在变量之间的关系,做一些预备工作,把根据文档最显易见的东西在Edtior中画出来(纯粹是为了验证自己看官方文档所理解的是否与Unity实现的相一致)。

1.1 anchoredMin和anchoredMax

先来看下anchoredMin和anchoredMax,因为在Unity5中摆过几次UI布局的,对它们肯定都不陌生,也好理解。

看一下官方文档对它们的解释:

属性官方描述
anchoredMinThe normalized position in the parent RectTransform that the upper right corner is anchored to.
anchoredMaxThe normalized position in the parent RectTransform that the lower left corner is anchored to.
对官方描述的理解:

* normalized position : 归一化的位置?那说明它是一个范围在0到1之间的位置值。

* in the parent RectTransform : 一个孤立的归一化位置根本无法标出是什么位置,那这一句是不是指明它是相对于Parent的一个归一化位置呢? 又是与parent的什么属性相对呢?

* 另一方面,很明显的,这两个位置,一个指示的是右上(upper right corner)的位置,一个指示的是左下(lower left corner)的位置。既然是左下和右上两个点,那知道这两个就可以再构造出另外两个点:左上和右下。凑成四个点组成一个矩形。

很明显,得到的这四个点是相对于parent的某个属性的。虽然我们并不知道这四个点相对于parent的什么属性,但它们看起来就是相对于parent的矩形框似的。而且刚好RectTransform有提供rect的方法,让我们在不知道它们内部工作原理的情况下直接获取最终的矩形框。那我们就先假设它们就是相对于parent的矩形框好了。

我们可以先来验证一下这个假设。

先摆一个最简单的UI出来看看:



可以看到四个anchorPoint在四个角落处。

为了验证我们的猜想,我们放四个红色小圆圈到Parent下面,位置由我们自己算出来。

代码如下:

void DrawAnchors()
{
if (parentRectTransform == null) {
return;
}
Vector2[] anchored_corner = new Vector2[4];
//** 用anchorMin和anchorMax算出四个anchor点
anchored_corner[0] = rectTransform.anchorMin;
anchored_corner[1] = new Vector2(rectTransform.anchorMax.x,rectTransform.anchorMin.y);
anchored_corner[2] = rectTransform.anchorMax;
anchored_corner[3] = new Vector2(rectTransform.anchorMin.x,rectTransform.anchorMax.y);
//** 使用Unity内置方法rect算出parent的矩形框
var rect_parent = parentRectTransform.rect;
for(int idx = 0;idx < 4;++idx){
//** 因为四个anchor点是相对于parent的归一化坐标
//** 所以将四个anchor点根据parent的size放大值并根据左下角进行偏移
anchored_corner[idx].Scale(rect_parent.size);
anchored_corner[idx] += rect_parent.min;
if(m_anchors[idx]){
//** 因为设置position设置的是世界坐标,而我们算出来的是parent下的局部坐标,因此使用parent的转换矩阵将坐标转换到世界坐标中
m_anchors[idx].position = parentRectTransform.localToWorldMatrix.MultiplyPoint(anchored_corner[idx]);
}
}
}

void OnDrawGizmos ()
{
DrawAnchors ();
}


PS : m_anchors类内成员变量,方便观察计算结果,就是图里四个红色圆圈(A0、A1、A2、A3)

运行结果如图:





四个红点正是我们所计算出来的anchorPoint的位置,与Editor显示的保持了时候同步,Good Job!

1.2 pivot

接下来让我们看看另外一个更加显而易见的RectTransform的内在变量:pivot。

先是照旧看一下它的官方说明:

属性官方文档
pivotThe normalized position in this RectTransform that it rotates around.
* 看说明,这个属性又是一个归一化的位置。看来又要寻找与它归一化相关的属性了。

* that it rotates around。这个pivot是该RectTransform旋转的中心点。这旋转暂时与我们探索的主题无关,先忽略。

那接下来,我们来看看这个pivot是关于哪个属性的归一化位置。

我们在Inspector中调整一下piovt的值试试看

pivot值产生的变化
0.5,0.5
0.75,0.2
0.0,0.0
1.0,1.0
从上面四张图可以看到,中心那个圆圈指示的正是会随pivot变换的点

1. 当pivot为(0,0)时,pivot在矩形框的左下角

2. 当pivot为(1,1)时,pivot在矩形框的右上角

3. 当pivot为(0.5,0.5)时,pivot在矩形框的中心

4. 当pivot为(0.7,0.2)时,pivot在0.7个矩形宽、0.2个矩形高的地方

很好,那很明显,pivot是相对于这个矩形框的归一化位置。

但现在我们还不知道这个矩形框是怎么算出来的,有没有内置的函数可以算出这个矩形框呢?

查阅一下官方文档,发现了以下两个函数

函数官方文档
GetLocalCornersGet the corners of the calculated rectangle in the local space of its Transform.
GetWorldCornersGet the corners of the calculated rectangle in world space.
* 看说明,貌似这两个函数都是给出矩形四个角的位置的,只是一个是局部坐标、一个是世界坐标。

因为我们给某个Transform设置位置时,都是设置的世界坐标系。所以我们就试验下用GetWorldCorners定位出来的四个点是不是我们想要的四个点吧。

照旧的,为了试验,给出代码:

void DrawCorners()
{
Vector3[] corners = new Vector3[4];
rectTransform.GetWorldCorners (corners);
for (int idx = 0; idx < 4; ++idx) {
if(m_corners[idx]){
m_corners[idx].position = corners[idx];
}
}
}

void OnDrawGizmos ()
{
DrawAnchors ();
DrawCorners ();
}


PS : m_corners类内成员变量,方便观察计算结果,就是图里四个蓝色色圆圈(C0、C1、C2、C3)

运行结果如图:







看起来不错,GetWorldCorners得到的正是我们想要的矩形框的四个点了。

有了这四个点,那我们要根据pivot算出最终的pivot的位置就简单了。

既然piovt是归一化的位置,那我们用图形学里面最常用的线性插值对C0(左下,最小)、C2(右上,最大)的x、y分别进行插值就能得到我们想要的pivot位置了。

用代码试验下:

void DrawCornersAndPivot()
{
Vector3[] corners = new Vector3[4];
rectTransform.GetWorldCorners (corners);
for (int idx = 0; idx < 4; ++idx) {
if(m_corners[idx]){
m_corners[idx].position = corners[idx];
}
}
if (m_pivot) {
var pivot_position = Vector2.zero;
pivot_position.x = corners[0].x * (1 - rectTransform.pivot.x) + corners[2].x * rectTransform.pivot.x;
pivot_position.y = corners[0].y * (1 - rectTransform.pivot.y) + corners[2].y * rectTransform.pivot.y;
m_pivot.position = pivot_position;
}
}

void OnDrawGizmos ()
{
DrawAnchors ();
DrawCornersAndPivot ();
}


运行结果,如图:







哎哟,不错啊,看起来我们定的位置与Editor显示的完全同步嘛。

1.3 offsetMin、offsetMax和sizeDelta

这三个属性官方说明如下:

属性官方文档
offsetMinThe offset of the lower left [b]corner[/b] of the rectangle relative to the lower left [b]anchor[/b].
offsetMaxThe offset of the upper right [b]corner[/b] of the rectangle relative to the upper right [b]anchor[/b].
sizeDeltaThe size of this RectTransform relative to the distances between the anchors.
看着挺简易明了的,直译过来差不多就是:

* offsetMin是左下corner左下anchor的偏移

* offsetMax是右上corner右上anchor的偏移

* sizeDelta是这个RectTransform到四个anchors的距离

这么解释不是很直观,也不确定对不对。

鉴于这三个属性都是距离相关的属性,不好在界面上显示出来,所以我们就单纯地输出它们的值,方便观察它们的变换。

输出值很简单,因此,代码:略。

1.4 anchoredPosition

终于来到这最后一个我们想探索的属性了。

照旧,先贴上官方文档说明:

属性官方文档
anchoredPositionThe position of the pivot of this RectTransform relative to the anchor reference point.
The anchor reference point is where the anchors are. If the anchors are not together, the four anchor positions are interpolated according to the pivot placement.
这个看起来就比较晕了。

* 首先这是一个跟pivot有关的位置。

* 其次,这是pivot相对于anchor reference point的位置。这个anchor reference point是个什么东西呢?

Anchor reference point

对官方文档解释的理解:

1. 它是anchors所在的位置

2. 如果四个anchors不在同一位置时,它是四个anchors的插值。

3. 而且这个插值是根据pivot的位置摆放来进行插值的。

这时就比较郁闷了,根据pivot的位置对四个anchors进行插值,从而得出anchored refrenced point的位置。

然后Unity又会根据这个anchored refrenced point 和 anchoredPosition来确定出pivot的位置。

PS: 一个错误的流程图~~~

Created with Raphaël 2.1.0开始获取anchors的位置获取pivot的位置根据pivot对anchors进行插值,得到anchored_referenced_point获取anchoredPosition根据anchoredPosition和anchored_referenced_point计算pivot的位置

这是来构成一个死循环的吗?明显不科学嘛,看来我们需要来摸索摸索这个anchored referenced point到底是个什么东西了。不过这个嘛,就得在稍后再提了。

2. 盲人摸象

2.1 RectTransform布局计算第一步的猜想

我们先猜想下,Unity是怎么确定出一个RectTransform到底应该怎么画的呢?

猜想

* 首先,一个UI的肯定是放在一个Canvas下面的,而Canvas的大小计算大部分时候是跟Camera的视口大小相关的(当然当Canvas是一个World Space的Canvas就不一样了,不过它也是人工指定了、在一开始就确定了的)。因此Canvas的大小可以说一开始是自有另一套机制确定好的了。

* 而我们的UI总是Canvas的子孩子,因此我们的UI总是可以知道parent的大小的。

* 而UI计算布局大小时,最直接相关的就是UI的anchors属性。因此我们假定下Unity计算UI大小时,第一步就是根据anchors和parent(当UI为canvas的第一层的孩子时,这个parent就是canvas)计算出第一个大小。

Created with Raphaël 2.1.0开始根据Canvas的配置计算出Canvas的大小根据孩子RectTransform的anchors和canvas的大小算出RectTransform的初始大小

2.1.a 猜想验证

布置UI Hierachy结构如下:

Canvas

| —— RectTransform

所以我们先来看一下我们的Canvas的大小。



然后我们把子孩子结点的各个属性值置为默认值,看它的初始状态是怎么样的。

InspctorScene View


可以看到,当我们把anchors调整到四个角[anchorMin(0,0),anchorMax(0,1)]时,孩子RectTransform撑满了整个Canvas;

而当我们调整孩子RectTransform的参数时:

InspctorScene View




在Debug模式下,我们确保只改动四个anchors的值,结果得出来了孩子RectTransform的大小被缩小了,以anchors相对于父亲的大小来确定了自己的大小和四个角的位置。

而当四个anchors挤在一点时,初始的size为0.

2.2 RectTransform布局计算第二步的猜想

在我们得到RectTransform的初始大小后,试着调整下sizeDelta,会出现如下图情况:

InspctorScene View






会发现sizeDelta在我们的初始大小上又往外扩大了一些。

同时这个sizeDelta和pivot同时会影响我们的offsetMin和offsetMax。

看样子,offsetMin、offsetMax和sizeDelta以及piovt的关系应该是如下的:

offsetMin = sizeDelta * pivot * -1;

offsetMax = sizeDelta * (1 - pivot);

是不是这样的呢?让我们写写代码验证验证下:

void LogoutDistanceProp()
{
if (m_text) {
var str_log_out = "";
str_log_out += string.Format("rect : {0}",rectTransform.rect.ToString());
str_log_out += string.Format("\nsizeDelta : {0}",rectTransform.sizeDelta.ToString());
str_log_out += string.Format("\noffsetMin : {0} , \nsizeDelta * pivot * -1 : {1}",
rectTransform.offsetMin.ToString(),
(Vector2.Scale(rectTransform.sizeDelta,rectTransform.pivot) * -1).ToString()
);
str_log_out += string.Format("\noffsetMax : {0} , \nsizeDela * (1 - pivot) : {1}",
rectTransform.offsetMax.ToString(),
Vector2.Scale(rectTransform.sizeDelta,Vector2.one - rectTransform.pivot).ToString()
);
m_text.text = str_log_out;
}
}

void OnDrawGizmos ()
{
DrawAnchors ();
DrawCornersAndPivot ();
LogoutDistanceProp ();
}


运行结果如图:



可以看到这时候我们算出来的值总是与Unity自带值是相一致的。

2.3 RectTransform布局计算第三步的猜想

不过这时候,如果我们改变了anchoredPosition时,就会发现我们的结果不太对了。

InspctorScene View


通过观察会发现,改了anchoredPosition后

我们算的offsetMin会与内置的offsetMin相差1个anchoredPosition,

我们算的offsetMax会与内置的offsetMax也会相差1个anchoredPosition,

因此,事实上offsetMin、offsetMax计算应该还与anchoredPosition相关联,



offsetMin = sizeDelta * pivot * -1 + anchoredPosition;

offsetMax = sizeDelta * (1 - pivot) + anchoredPosition;

新的代码如下:

void LogoutDistanceProp()
{
if (m_text) {
var str_log_out = "";
str_log_out += string.Format("rect : {0}",rectTransform.rect.ToString());
str_log_out += string.Format("\nsizeDelta : {0}",rectTransform.sizeDelta.ToString());
str_log_out += string.Format("\noffsetMin : {0} , \nsizeDelta * pivot * -1 + anchoredPosition : {1}",
rectTransform.offsetMin.ToString(),
(Vector2.Scale(rectTransform.sizeDelta,rectTransform.pivot) * -1
+ rectTransform.anchoredPosition).ToString()
);
str_log_out += string.Format("\noffsetMax : {0} , \nsizeDela * (1 - pivot)  + anchoredPosition : {1}",
rectTransform.offsetMax.ToString(),
(Vector2.Scale(rectTransform.sizeDelta,Vector2.one - rectTransform.pivot)
+ rectTransform.anchoredPosition).ToString()
);
m_text.text = str_log_out;
}
}


更新代码后,算出来就正确了:

InspctorScene View


2.4 RectTransform布局计算第四步的猜想

但是仔细观察我们会发现,当我们调整了anchorPosition的值后,pivot不在位于四个anchors的相应的pivot的点了,它产生了偏移了,跟着四个corners一起偏移了anchorPosition的位置。

也就是anchorPosition其实是改变了(而不是四)个点的位置,

它同时改变了以pivot为中心、大小已确定好的这个矩形。

此时回想一下,之前提到过的anchorPosition的解释:

anchorPosition是pivot相对于anchorReferencePoint的(偏移)位置,

那我们现在对anchorReferencePoint到底是个什么东西是不是有了些眉目了呢?

看起来anchorReferencePoint就是未加anchorPosition偏移的pivot。

而未加anchorPosition偏移的四个corner又是与四个anchors相重叠的,

而一但pivot加了anchorPosition偏移,四个corner就各自与四个anchors偏移了与anchorPosition相联系的一个距离。

因此,我们有理由相信,anchorReferencePoint就是anchorMin、anchorMax对于pivot的线性插值,即:

anchorReferencePoint = anchorMin * (1 - pivot) + anchorMax * piovt

是不是这样呢,让我们验证一下。

void DrawAnchorRefencePoint()
{
if (parentRectTransform == null) {
return;
}
if (m_anchorReferencePoint) {
//** 使用Unity内置方法rect算出parent的矩形框
var rect_parent = parentRectTransform.rect;
var norm_anchor_reference_point = Vector2.Scale(rectTransform.anchorMin,(Vector2.one - rectTransform.pivot))
+ Vector2.Scale(rectTransform.anchorMax,rectTransform.pivot);
var anchor_reference_point = Vector2.Scale(norm_anchor_reference_point,rect_parent.size);
anchor_reference_point += rect_parent.min;
m_anchorReferencePoint.position = parentRectTransform.localToWorldMatrix.MultiplyPoint(anchor_reference_point);
}
}

void OnDrawGizmos ()
{
DrawAnchors ();
DrawAnchorRefencePoint ();
DrawCornersAndPivot ();
LogoutDistanceProp ();
}


运行起来,结果如图:



图中AR点下是我们的Anchor Reference Point的位置。

与Pivot正好相距了一个anchoredPosition的距离。

3. 重新梳理一遍

至此,我们清楚了RectTransform各个属性所代表的意义,让我们来梳理梳理它们之间的关系(上面写的实在有够乱的)。

现在我设想中的Unity中RectTransform的布局流程应该是像下面这样的:

PS : 以下流程图的rect即是指一个具有已确定大小和位置的矩形框(即能从该矩形其中取得明确的四个顶点)

Created with Raphaël 2.1.0开始 A.根据Camera和Canvas配置计算Canvas大小B.计算parent结点的rect(当parent为Canvas时,即是Canvas形成的整个矩形框),结果存在rectParent中C.根据anchorMin、anchorMax(或者简称为anchors)和rectParent,计算出四个anchor点组成的矩形框,结果存在anchorsRect中G.根据sizeDelta、piovt计算出offsetMin、offsetMaxH.根据offsetMin、offsetMax、originPivotRect计算出最终的rect(即RectTransform本身代表的rect) 计算pivot绝对位置用于指代localPosition?D.根据anchorsRect和pivot属性计算出anchor reference point,结果存在anchorsRefPoint中E.根据anchorsRefPoint和anchoredPosition属性计算出pivot的真实坐标值(而不只是一个归一化的值),结果存在absPivot中有孩子结点开始计算孩子的rect结束yesnoyesno

3.1 anchorsRect

C结点计算anchorsRect:

anchorsRect.xMin = rectParent.xMin + anchorMin.x * rectParent.width;
anchorsRect.xMax = rectParent.xMin + anchorMax.x * rectParent.width;
anchorsRect.yMin = rectParent.yMin + anchorMin.y * rectParent.height;
anchorsRect.yMax = rectParent.yMin + anchorMax.y * rectParent.height;


3.2 anchorsRefPoint

D结点计算anchor refence point:

anchorsRefPoint.x = anchorsRect.xMin * (1 - pivot.x) + anchorsRect.xMax * pivot.x;
anchorsRefPoint.y = anchorsRect.yMin * (1 - pivot.y) + anchorsRect.yMax * pivot.y;


3.3 absPivot

E结点计算absPivot:

absPivot = anchorsRefPoint + anchoredPosition

3.4 originPivotRect

F结点计算originPivotRect:

originPivotRect.xMin = absPivot.x - (1-pivot.x) * anchorsRect.width;
originPivotRect.xMax = absPivot.x + pivot.x * anchorsRect.width;
originPivotRect.yMin = absPivot.y - (1-pivot.y) * anchorsRect.height;
originPivotRect.yMax = absPivot.y + pivoty * anchorsRect.height;


3.5 offsetMin和offsetMax

G结点计算offsetMin和offsetMax

offsetMin = sizeDelta * pivot * -1 + anchoredPosition;
offsetMax = sizeDelta * (1 - pivot) + anchoredPosition;


3.6 finalRect

H结点计算最终的rect

finalRect.xMin = originPivotRect.xMin + offsetMin.x;
finalRect.xMax = originPivotRect.xMax + offsetMax.x;
finalRect.yMin = originPivotRect.yMin + offsetMin.y;
finalRect.yMax =originPivotRect.yMax + offsetMax.y;


3.7 验证代码

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class RectTransformInfoCalculator : MonoBehaviour {

private RectTransform m_rectTransform;
private RectTransform rectTransform {
get {
if (null == m_rectTransform) {
m_rectTransform = GetComponent<RectTransform>();
}
return m_rectTransform;
}
}

RectTransform m_parentRectTransform;
private RectTransform parentRectTransform {
get {
if (null == m_parentRectTransform) {
m_parentRectTransform = rectTransform.parent.GetComponent<RectTransform>();
}
return m_parentRectTransform;
}
}

public Text m_text;

void GizmoDrawRect(Rect rect,Color color,Matrix4x4 matrix)
{
Gizmos.matrix = parentRectTransform.localToWorldMatrix;
Gizmos.color = color;
Vector2[] corners = new Vector2[4];
corners [0] = rect.min;
corners [1] = new Vector2 (rect.xMax, rect.yMin);
corners [2] = rect.max;
corners [3] = new Vector2 (rect.xMin, rect.yMax);
for(int idx = 0;idx < 4;++idx){
Gizmos.DrawLine ((Vector3)corners[idx%4], (Vector3)corners[(idx+1)%4]);
}
}

void GizmoDrawRect(Rect rect,Color color)
{
GizmoDrawRect(rect,color,parentRectTransform.localToWorldMatrix);
}

void GizmoDrawPoint(Vector2 point,Color color)
{
Gizmos.matrix = parentRectTransform.localToWorldMatrix;
Gizmos.color = color;
Gizmos.DrawSphere ((Vector3)point, 5f);
}

void OnDrawGizmos ()
{
var rectParent = parentRectTransform.rect;
GizmoDrawRect (rectParent, Color.black);

Rect anchorsRect;
CalcAnchorsRect (rectParent, out anchorsRect);
GizmoDrawRect (anchorsRect, Color.red);

var offsetMin = Vector2.Scale(rectTransform.sizeDelta,rectTransform.pivot) * -1 + m_rectTransform.anchoredPosition;
var offsetMax = Vector2.Scale(rectTransform.sizeDelta,Vector2.one - rectTransform.pivot) + m_rectTransform.anchoredPosition;
if (m_text) {
var str_logout = "";
str_logout += string.Format("offsetMin : {0}",offsetMin.ToString());
str_logout += string.Format("\noffsetMax : {0}",offsetMax.ToString());
m_text.text = str_logout;
}

Rect finalRect;
CalcFinalRect (anchorsRect, offsetMin, offsetMax, out finalRect);
GizmoDrawRect (finalRect, Color.green);

Vector2 anchorsRefPoint;
CalcAnchorRefPoint (anchorsRect, out anchorsRefPoint);
GizmoDrawPoint (anchorsRefPoint, Color.red * 0.5f);

Vector2 absPiovt = anchorsRefPoint + m_rectTransform.anchoredPosition;
GizmoDrawPoint (absPiovt, Color.blue * 0.5f);
}

void CalcAnchorsRect(Rect rectParent,out Rect anchorsRect)
{
anchorsRect = new Rect ();
anchorsRect.xMin = rectParent.xMin + rectTransform.anchorMin.x * rectParent.width;
anchorsRect.xMax = rectParent.xMin + rectTransform.anchorMax.x * rectParent.width;
anchorsRect.yMin = rectParent.yMin + rectTransform.anchorMin.y * rectParent.height;
anchorsRect.yMax = rectParent.yMin + rectTransform.anchorMax.y * rectParent.height;
}

void CalcAnchorRefPoint(Rect anchorsRect,out Vector2 anchorsRefPoint)
{
anchorsRefPoint.x = anchorsRect.xMin * (1 - rectTransform.pivot.x) + anchorsRect.xMax * rectTransform.pivot.x;
anchorsRefPoint.y = anchorsRect.yMin * (1 - rectTransform.pivot.y) + anchorsRect.yMax * rectTransform.pivot.y;
}

void CalcFinalRect(Rect rect,Vector2 offsetMin,Vector2 offsetMax,out Rect finalRect)
{
finalRect = new Rect ();
finalRect.xMin = rect.xMin + offsetMin.x;
finalRect.xMax = rect.xMax + offsetMax.x;
finalRect.yMin = rect.yMin + offsetMin.y;
finalRect.yMax = rect.yMax + offsetMax.y;
}
}


运行结果如图:



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: