通过前两篇文章的讲解,我们已经了解了制作需求的全部前置知识,有了完全这个需求的能力了。在制作之前,需要进行程序视角的需求分析和设计。
需求介绍
假设项目中有一个UI公告板的需求,这个公告板的长度或者宽度是固定的。里面有很多带文字的新闻,但是这些个新闻的字数和大小是不固定的,要求模仿自动布局组件,写一个组件满足UI的需求。
需求分析
首先是长度或者宽度是固定,这就意味这不固定的另一边可以动态延申,这就要求我们必须实现Calculate。。。的那几个方法。
第二个是内部的大小不固定,那么我们要实现自动控制孩子大小的功能,来动态的控制孩子的大小,所以参数上control child需要有。
第三个是继承LayoutGroup之后,那两个参数也是需要实现的,但是由于功能的特殊性,导致的是需要特殊的处理方法。
第三个是要控制孩子的换行,而且换行之后还得找到之前一行的最长的另一边,这样才能让不和之前一行的交叉。
因为要又知道行,又知道宽,这和之前给定的分割宽高设计完全不同了,庆幸的是,我们的长度或者宽度是完全固定的。
设计
肯定是要优先实现两个Cal方法,这个方法的主要目的是设置总长和总宽,因为需求中长度或者宽度是固定,也就意味着只有两种情况,在长度是固定时,我们直接设置长度为原来的长度即可,宽度通过计算得出;在宽度时固定时,我们直接设置宽度为原来的宽度即可,长度通过计算得出。
//本质是代表拓宽,但是startAxis代表格子方向,所以刚好和系统设计的相反,这个需要理解。
public override void CalculateLayoutInputHorizontal()
{
base.CalculateLayoutInputHorizontal();
if (startAxis == Axis.Vertical)
SetRange(1);
else
{
//直接设置长或宽
float length = rectTransform.rect.size[0];
needOtherLength = length;
SetLayoutInputForAxis(length, length, -1, 0);
}
}
public override void CalculateLayoutInputVertical()
{
if (startAxis == Axis.Horizontal)
SetRange(0);
else
{
float length = rectTransform.rect.size[1];
needOtherLength = length;
SetLayoutInputForAxis(length, length, -1, 1);
}
}
在设置每个孩子大小的时候,也是在一次同时计算长和宽,在另一次就不会计算。由于两个方法都要执行,我们就采用startAxis判断就行了。
public override void SetLayoutHorizontal()
{
if (startAxis == Axis.Horizontal)
SetChildrenAlongAxis(0);
}
public override void SetLayoutVertical()
{
if (startAxis == Axis.Vertical)
SetChildrenAlongAxis(1);
}
所以我们只要去实现SetRange和SetChildrenAlongAxis方法即可。
需要去考虑几个参数点,一个是是否控制大小,一个是padding,一个是spacing,一个是ChildAliment(设置大小的时候不用考虑),又考虑到,我们在计算范围大小的时候,是不是要计算并得到每个格子的大小,那么我们就可以顺便计算出格子大小,在计算格子大小的时候直接赋值。
我们要得到min值和preferred的计算结果对比大小
以行拓展为例,列长度是固定的,所以我们先计算每一列能容下几个,然后得到这几个中最大的宽度,换行,遍历所有计算,就可以得到所需的结果。
protected void SetRange(int axis)
{
float combinedPadding = (axis == 1 ? padding.horizontal : padding.vertical);
float innerSize = axis == 0 ? rectTransform.rect.width - padding.horizontal : rectTransform.rect.height - padding.vertical;
bool controlSize = (axis == 0 ? m_ChildControlWidth : m_ChildControlHeight);
bool useScale = (axis == 0 ? m_ChildScaleWidth : m_ChildScaleHeight);
bool childForceExpandSize = (axis == 0 ? m_ChildForceExpandWidth : m_ChildForceExpandHeight);
bool otherAxisControlSize = (axis == 1 ? m_ChildControlWidth : m_ChildControlHeight);
bool otherAxisUseScale = (axis == 1 ? m_ChildScaleWidth : m_ChildScaleHeight);
bool oterAxisChildForceExpandSize = (axis == 1 ? m_ChildForceExpandWidth : m_ChildForceExpandHeight);
float spacingLength = spacing[axis];
float spacingOtherLength = spacing[axis ^ 1];
float maxCellMinLength = -999;
float maxCellPreferredLength = -999;
float totalOneMinLength = - spacingLength;
float totalOnePreferredLength = - spacingLength;
float needMinLength = combinedPadding;
float needPreferredLength = combinedPadding;
float totalFlexible = 0;
for (int i = 0; i < rectChildren.Count; i++)
{
RectTransform child = rectChildren[i];
float min, preferred, flexible;
//根据是否控制子物体大小得到对应三个值
GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);
min = min == 0 ? child.sizeDelta[axis] : min;
preferred = preferred == 0 ? child.sizeDelta[axis] : preferred;
min = Mathf.Clamp(min, 0, innerSize);
preferred = Mathf.Clamp(preferred, 0, innerSize);
float otherAxisMin, otherAxisPreferred, otherAxisFlexible;
GetChildSizes(child, axis ^ 1, otherAxisControlSize, oterAxisChildForceExpandSize, out otherAxisMin, out otherAxisPreferred, out otherAxisFlexible);
otherAxisMin = otherAxisMin == 0 ? child.sizeDelta[axis ^ 1] : otherAxisMin;
otherAxisPreferred = otherAxisPreferred == 0 ? child.sizeDelta[axis ^ 1] : otherAxisPreferred;
if (useScale)
{
float scaleFactor = child.localScale[axis];
min *= scaleFactor;
preferred *= scaleFactor;
flexible *= scaleFactor;
}
if (otherAxisUseScale)
{
float scaleFactor = child.localScale[axis ^ 1];
otherAxisMin *= scaleFactor;
otherAxisPreferred *= scaleFactor;
otherAxisFlexible *= scaleFactor;
}
if (controlSize)
{
float size = min > preferred ? min : preferred;
if (size == 0)
{
size = GetChildSizesAfterCal(child, axis);
}
SetChildSize(child, axis, size);
}
if (otherAxisControlSize)
{
float size = otherAxisMin > otherAxisPreferred ? otherAxisMin : otherAxisPreferred;
if (size == 0)
{
size = GetChildSizesAfterCal(child, axis ^ 1);
}
SetChildSize(child, axis ^ 1, size);
}
//算min的一个情况,当一边加起来大于容量,就会自动跳转,选择上次最大的长或宽
if (totalOneMinLength + min + spacingLength > innerSize && totalOneMinLength != 0)
{
needMinLength += maxCellMinLength + spacingOtherLength;
totalFlexible += flexible;
maxCellMinLength = otherAxisMin;
totalOneMinLength = min + (axis == 0 ? padding.top : padding.left);
}
else
{
totalOneMinLength += min + spacingLength;
if (maxCellMinLength < otherAxisMin)
{
maxCellMinLength = otherAxisMin;
}
}
//算preferred的一个情况
if (totalOnePreferredLength + preferred + spacingLength > innerSize && totalOnePreferredLength != 0)
{
needPreferredLength += maxCellPreferredLength + spacingOtherLength;
maxCellPreferredLength = otherAxisPreferred;
totalOnePreferredLength = preferred + (axis == 0 ? padding.top : padding.left);
}
else
{
totalOnePreferredLength += preferred + spacingLength;
if (maxCellPreferredLength < otherAxisPreferred)
{
maxCellPreferredLength = otherAxisPreferred;
}
}
}
needMinLength += maxCellMinLength;
needPreferredLength += maxCellPreferredLength;
//选择最大的一条然后设置
needLength = needMinLength > needPreferredLength ? needMinLength : needPreferredLength;
SetLayoutInputForAxis(needMinLength, needPreferredLength, totalFlexible, axis ^ 1);
}
计算完范围大小和取得对应长度以后,我们就可以设置每一个物体的相应位置了。如果要问为什么不一边计算大小一边设置位置,理论上来说是可以的,但是还是适应这个框架,让更多人理解和看懂,也不会让一个方法过于繁杂。
public override void SetLayoutHorizontal()
{
if (startAxis == Axis.Horizontal)
SetChildrenAlongAxis(0);
}
public override void SetLayoutVertical()
{
if (startAxis == Axis.Vertical)
SetChildrenAlongAxis(1);
}
protected void SetChildrenHorizontalAndVerticalAlongAxis(int axis)
{
bool useScale = (axis == 0 ? m_ChildScaleWidth : m_ChildScaleHeight);
bool otherAxisUseScale = (axis == 1 ? m_ChildScaleWidth : m_ChildScaleHeight);
Vector2 requireSpace = new Vector2(axis == 0? needOtherLength : needLength, axis == 0 ? needLength : needOtherLength);
Vector2 startOffset = new Vector2(GetStartOffset(0, requireSpace.x), GetStartOffset(1, requireSpace.y));
//初始化的一开始的地方,这个startoffset就是父类中的一个方法得到的,用于解决,ChildAliment问题。
//spacing和padding计算大小和位置都有用到,需要理解用法。
float initLength = (axis == 0 ? padding.left : padding.top) + startOffset[axis];
float initOtherLength = (axis == 0 ? padding.top : padding.left) + startOffset[axis ^ 1];
//一行/列的暂时总长
float tempLength = initLength;
float tempOtherLength = initOtherLength;
float tempMaxOtherLength = float.MinValue;
float Fatherlength = axis == 0 ? rectTransform.rect.width : rectTransform.rect.height;
float spacingLength = spacing[axis];
float spacingOtherLength = spacing[axis ^ 1];
for (int i = 0; i < rectChildren.Count; i++)
{
RectTransform child = rectChildren[i];
float innerSize = rectTransform.rect.size[axis] - (axis == 0 ? padding.horizontal : padding.vertical);
float childOldOtherLength = GetChildSizesAfterCal(child, axis ^ 1);
float childLength = GetChildSizesAfterCal(child, axis);
float childScale = useScale ? child.localScale[axis] : 1f;
float childOtherAxisScale = otherAxisUseScale ? child.localScale[axis ^ 1] : 1f;
childLength = childLength * childScale;
float allLength = (((tempLength + childLength) > Fatherlength) && tempLength == initLength) ? Fatherlength : tempLength + childLength;
//发现这个加进去计算后已经超出了最大范围,那么重新设置一些值并且回退重新计算这个值
if (Mathf.Round(allLength * 10000) / 10000 > Mathf.Round(Fatherlength * 10000) / 10000)
{
i--;
tempLength = initLength;
tempMaxOtherLength *= childOtherAxisScale;
tempOtherLength += tempMaxOtherLength + spacingOtherLength;
tempMaxOtherLength = float.MinValue;
}
else
{
if (childOldOtherLength * childOtherAxisScale > tempMaxOtherLength)
{
tempMaxOtherLength = childOldOtherLength * childOtherAxisScale;
}
SetChildPos(rectChildren[i], axis, tempLength);
SetChildPos(rectChildren[i], axis ^ 1, tempOtherLength);
tempLength += childLength + spacingLength;
}
}
}
通过这个需求,我们深刻理解了Unity提供的Layout Group类的用法,也能自己写相关的自动布局组件,还锻炼了自己计算范围和大小的思维能力,在将来编写类似的自动布局组件更加得心应手了,可以说是收获颇丰。源代码文件我会放在下载里,请有需要的自取支持一下。