以下内容是根据Unity 2020.1.01f版本进行编写的
UGUI源代码之Layout—增加MaxWidth和MaxHeight属性
1、目的
在工作中遇到的需求,要求滑动列表的节点需要根据格子的数量进行高度自适应,并且有最小高度和最大高度,目前的排序组件只能设置最小高度而无法设置最大高度,于是想看看UGUI的源代码,看看能不能在LayoutElement组件上增加最大高度和最大宽度的属性。
2、参考
本文参考Unity官方的UGUI源代码
Github地址:https://github.com/Unity-Technologies/uGUI
3、重点代码阅读
首先看看LayoutElementEditor类的代码:
SerializedProperty m_IgnoreLayout;
SerializedProperty m_MinWidth;
SerializedProperty m_MinHeight;
SerializedProperty m_PreferredWidth;
SerializedProperty m_PreferredHeight;
SerializedProperty m_FlexibleWidth;
SerializedProperty m_FlexibleHeight;
SerializedProperty m_LayoutPriority;
protected virtual void OnEnable()
{
m_IgnoreLayout = serializedObject.FindProperty("m_IgnoreLayout");
m_MinWidth = serializedObject.FindProperty("m_MinWidth");
m_MinHeight = serializedObject.FindProperty("m_MinHeight");
m_PreferredWidth = serializedObject.FindProperty("m_PreferredWidth");
m_PreferredHeight = serializedObject.FindProperty("m_PreferredHeight");
m_FlexibleWidth = serializedObject.FindProperty("m_FlexibleWidth");
m_FlexibleHeight = serializedObject.FindProperty("m_FlexibleHeight");
m_LayoutPriority = serializedObject.FindProperty("m_LayoutPriority");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(m_IgnoreLayout);
if (!m_IgnoreLayout.boolValue)
{
EditorGUILayout.Space();
LayoutElementField(m_MinWidth, 0);
LayoutElementField(m_MinHeight, 0);
LayoutElementField(m_PreferredWidth, t => t.rect.width);
LayoutElementField(m_PreferredHeight, t => t.rect.height);
LayoutElementField(m_FlexibleWidth, 1);
LayoutElementField(m_FlexibleHeight, 1);
}
EditorGUILayout.PropertyField(m_LayoutPriority);
serializedObject.ApplyModifiedProperties();
}
这是显示在Inspector面板的属性,实际上就是在显示属性的同时设置其默认值
其中OnEnable函数就是在组件节点显示的时候调用的,用于获取对应的LayoutElement类中的数据
LayoutElement代码(已把注释去掉):
public class LayoutElement : UIBehaviour, ILayoutElement, ILayoutIgnorer
{
[SerializeField] private bool m_IgnoreLayout = false;
[SerializeField] private float m_MinWidth = -1;
[SerializeField] private float m_MinHeight = -1;
[SerializeField] private float m_PreferredWidth = -1;
[SerializeField] private float m_PreferredHeight = -1;
[SerializeField] private float m_FlexibleWidth = -1;
[SerializeField] private float m_FlexibleHeight = -1;
[SerializeField] private int m_LayoutPriority = 1;
public virtual bool ignoreLayout { get { return m_IgnoreLayout; } set { if (SetPropertyUtility.SetStruct(ref m_IgnoreLayout, value)) SetDirty(); } }
public virtual void CalculateLayoutInputHorizontal() {}
public virtual void CalculateLayoutInputVertical() {}
public virtual float minWidth { get { return m_MinWidth; } set { if (SetPropertyUtility.SetStruct(ref m_MinWidth, value)) SetDirty(); } }
public virtual float minHeight { get { return m_MinHeight; } set { if (SetPropertyUtility.SetStruct(ref m_MinHeight, value)) SetDirty(); } }
public virtual float preferredWidth { get { return m_PreferredWidth; } set { if (SetPropertyUtility.SetStruct(ref m_PreferredWidth, value)) SetDirty(); } }
public virtual float preferredHeight { get { return m_PreferredHeight; } set { if (SetPropertyUtility.SetStruct(ref m_PreferredHeight, value)) SetDirty(); } }
public virtual float flexibleWidth { get { return m_FlexibleWidth; } set { if (SetPropertyUtility.SetStruct(ref m_FlexibleWidth, value)) SetDirty(); } }
public virtual float flexibleHeight { get { return m_FlexibleHeight; } set { if (SetPropertyUtility.SetStruct(ref m_FlexibleHeight, value)) SetDirty(); } }
public virtual int layoutPriority { get { return m_LayoutPriority; } set { if (SetPropertyUtility.SetStruct(ref m_LayoutPriority, value)) SetDirty(); } }
protected LayoutElement()
{}
protected override void OnEnable()
{
base.OnEnable();
SetDirty();
}
protected override void OnTransformParentChanged()
{
SetDirty();
}
protected override void OnDisable()
{
SetDirty();
base.OnDisable();
}
protected override void OnDidApplyAnimationProperties()
{
SetDirty();
}
protected override void OnBeforeTransformParentChanged()
{
SetDirty();
}
protected void SetDirty()
{
if (!IsActive())
return;
LayoutRebuilder.MarkLayoutForRebuild(transform as RectTransform);
}
#if UNITY_EDITOR
protected override void OnValidate()
{
SetDirty();
}
#endif
}
这里主要是设置了各种属性,以及在属性值改变时,设置节点大小
再来看看排序组件,这里看HorizontalLayoutGroup的类:
public class HorizontalLayoutGroup : HorizontalOrVerticalLayoutGroup
{
protected HorizontalLayoutGroup()
{}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void CalculateLayoutInputHorizontal()
{
base.CalculateLayoutInputHorizontal();
CalcAlongAxis(0, false);
}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void CalculateLayoutInputVertical()
{
CalcAlongAxis(1, false);
}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void SetLayoutHorizontal()
{
SetChildrenAlongAxis(0, false);
}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void SetLayoutVertical()
{
SetChildrenAlongAxis(1, false);
}
}
可以看出,这个类主要是复写计算函数的。
我们实际上是想要在计算最佳尺寸的时候,如果超过了设置的最大值,就返回最大值。看函数名CalcAlongAxis就是计算尺寸的函数,按F12跳转到CalcAlongAxis函数中:
protected void CalcAlongAxis(int axis, bool isVertical)
{
float combinedPadding = (axis == 0 ? padding.horizontal : 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);
float totalMin = combinedPadding;
float totalPreferred = combinedPadding;
float totalFlexible = 0;
bool alongOtherAxis = (isVertical ^ (axis == 1));
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);
if (useScale)
{
float scaleFactor = child.localScale[axis];
min *= scaleFactor;
preferred *= scaleFactor;
flexible *= scaleFactor;
}
if (alongOtherAxis)
{
totalMin = Mathf.Max(min + combinedPadding, totalMin);
totalPreferred = Mathf.Max(preferred + combinedPadding, totalPreferred);
totalFlexible = Mathf.Max(flexible, totalFlexible);
}
else
{
totalMin += min + spacing;
totalPreferred += preferred + spacing;
// Increment flexible size with element's flexible size.
totalFlexible += flexible;
}
}
if (!alongOtherAxis && rectChildren.Count > 0)
{
totalMin -= spacing;
totalPreferred -= spacing;
}
totalPreferred = Mathf.Max(totalMin, totalPreferred);
SetLayoutInputForAxis(totalMin, totalPreferred, totalFlexible, axis);
}
可以看出,最佳尺寸preferred是从函数GetChildSizes中获得的,按F12跳转到GetChildSizes函数:
private void GetChildSizes(RectTransform child, int axis, bool controlSize, bool childForceExpand,
out float min, out float preferred, out float flexible)
{
if (!controlSize)
{
min = child.sizeDelta[axis];
preferred = min;
flexible = 0;
}
else
{
min = LayoutUtility.GetMinSize(child, axis);
preferred = LayoutUtility.GetPreferredSize(child, axis);
flexible = LayoutUtility.GetFlexibleSize(child, axis);
}
if (childForceExpand)
flexible = Mathf.Max(flexible, 1);
}
可以看出,最佳尺寸是通过LayoutUtility.GetPreferredSize函数获得的,跳转到GetPreferredSize函数:
public static float GetPreferredSize(RectTransform rect, int axis)
{
if (axis == 0)
return GetPreferredWidth(rect);
return GetPreferredHeight(rect);
}
很明显可以看出,排序组件就是通过GetPreferredWidth和GetPreferredHeight函数获取最佳的宽高的,代码:
public static float GetPreferredWidth(RectTransform rect)
{
return Mathf.Max(GetLayoutProperty(rect, e => e.minWidth, 0), GetLayoutProperty(rect, e => e.preferredWidth, 0));
}
public static float GetPreferredHeight(RectTransform rect)
{
return Mathf.Max(GetLayoutProperty(rect, e => e.minHeight, 0), GetLayoutProperty(rect, e => e.preferredHeight, 0));
}
可以看出,这里实际上就是对比最小尺寸和最佳尺寸,取最大值返回,我们只需要修改这里,增加对比最大值属性就可以了
若最小尺寸和最佳尺寸的最大值,比设置的最大值还要大,则返回设置的最大值
4、准备修改UGUI源代码
请看这篇:UGUI源代码之修改源代码的前期准备
已经准备过的同学可以跳过
5、为Layout增加自定义的属性(修改UGUI源代码代码)
首先是LayoutElementEditor类
SerializedProperty m_IgnoreLayout;
SerializedProperty m_MinWidth;
SerializedProperty m_MinHeight;
SerializedProperty m_PreferredWidth;
SerializedProperty m_PreferredHeight;
//增加属性
SerializedProperty m_MaxWidth;
SerializedProperty m_MaxHeight;
SerializedProperty m_FlexibleWidth;
SerializedProperty m_FlexibleHeight;
SerializedProperty m_LayoutPriority;
protected virtual void OnEnable()
{
m_IgnoreLayout = serializedObject.FindProperty("m_IgnoreLayout");
m_MinWidth = serializedObject.FindProperty("m_MinWidth");
m_MinHeight = serializedObject.FindProperty("m_MinHeight");
m_PreferredWidth = serializedObject.FindProperty("m_PreferredWidth");
m_PreferredHeight = serializedObject.FindProperty("m_PreferredHeight");
//增加属性
m_MaxWidth = serializedObject.FindProperty("m_MaxWidth");
m_MaxHeight = serializedObject.FindProperty("m_MaxHeight");
m_FlexibleWidth = serializedObject.FindProperty("m_FlexibleWidth");
m_FlexibleHeight = serializedObject.FindProperty("m_FlexibleHeight");
m_LayoutPriority = serializedObject.FindProperty("m_LayoutPriority");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(m_IgnoreLayout);
if (!m_IgnoreLayout.boolValue)
{
EditorGUILayout.Space();
LayoutElementField(m_MinWidth, 0);
LayoutElementField(m_MinHeight, 0);
LayoutElementField(m_PreferredWidth, t => t.rect.width);
LayoutElementField(m_PreferredHeight, t => t.rect.height);
//增加属性
LayoutElementField(m_MaxWidth, 100);
LayoutElementField(m_MaxHeight, 100);
LayoutElementField(m_FlexibleWidth, 1);
LayoutElementField(m_FlexibleHeight, 1);
}
EditorGUILayout.PropertyField(m_LayoutPriority);
serializedObject.ApplyModifiedProperties();
}
在这个类中增加了两个属性:m_MaxWidth和m_MaxHeight,并完成初始化,默认最大值为100
然后是LayoutElement类(已删除注释):
[SerializeField] private bool m_IgnoreLayout = false;
[SerializeField] private float m_MinWidth = -1;
[SerializeField] private float m_MinHeight = -1;
[SerializeField] private float m_PreferredWidth = -1;
[SerializeField] private float m_PreferredHeight = -1;
//增加属性
[SerializeField] private float m_MaxWidth = -1;
[SerializeField] private float m_MaxHeight = -1;
[SerializeField] private float m_FlexibleWidth = -1;
[SerializeField] private float m_FlexibleHeight = -1;
[SerializeField] private int m_LayoutPriority = 1;
public virtual bool ignoreLayout { get { return m_IgnoreLayout; } set { if (SetPropertyUtility.SetStruct(ref m_IgnoreLayout, value)) SetDirty(); } }
public virtual void CalculateLayoutInputHorizontal() {}
public virtual void CalculateLayoutInputVertical() {}
public virtual float minWidth { get { return m_MinWidth; } set { if (SetPropertyUtility.SetStruct(ref m_MinWidth, value)) SetDirty(); } }
public virtual float minHeight { get { return m_MinHeight; } set { if (SetPropertyUtility.SetStruct(ref m_MinHeight, value)) SetDirty(); } }
public virtual float preferredWidth { get { return m_PreferredWidth; } set { if (SetPropertyUtility.SetStruct(ref m_PreferredWidth, value)) SetDirty(); } }
public virtual float preferredHeight { get { return m_PreferredHeight; } set { if (SetPropertyUtility.SetStruct(ref m_PreferredHeight, value)) SetDirty(); } }
//增加属性
public virtual float maxWidth { get { return m_MaxWidth; } set { if (SetPropertyUtility.SetStruct(ref m_MaxWidth, value)) SetDirty(); } }
//增加属性
public virtual float maxHeight { get { return m_MaxHeight; } set { if (SetPropertyUtility.SetStruct(ref m_MaxHeight, value)) SetDirty(); } }
public virtual float flexibleWidth { get { return m_FlexibleWidth; } set { if (SetPropertyUtility.SetStruct(ref m_FlexibleWidth, value)) SetDirty(); } }
public virtual float flexibleHeight { get { return m_FlexibleHeight; } set { if (SetPropertyUtility.SetStruct(ref m_FlexibleHeight, value)) SetDirty(); } }
public virtual int layoutPriority { get { return m_LayoutPriority; } set { if (SetPropertyUtility.SetStruct(ref m_LayoutPriority, value)) SetDirty(); } }
此时在Inspector面板上已经能看到新增的属性了:
但是此时属性还没有使用,因此,继续增加对此属性的引用
继续修改ILayoutElement类(已删除注释):
public interface ILayoutElement
{
void CalculateLayoutInputHorizontal();
void CalculateLayoutInputVertical();
float minWidth { get; }
float preferredWidth { get; }
float flexibleWidth { get; }
float minHeight { get; }
float preferredHeight { get; }
//增加属性
float maxWidth { get; }
//增加属性
float maxHeight { get; }
float flexibleHeight { get; }
int layoutPriority { get; }
}
在LayoutUtility类中修改获得最佳尺寸的函数:
public static float GetPreferredWidth(RectTransform rect)
{
float max = Mathf.Max(GetLayoutProperty(rect, e => e.minWidth, 0), GetLayoutProperty(rect, e => e.preferredWidth, 0));
if (max > GetLayoutProperty(rect, e => e.maxWidth, int.MaxValue))
{
return GetLayoutProperty(rect, e => e.maxWidth, int.MaxValue);
}
return max;
}
public static float GetPreferredHeight(RectTransform rect)
{
float max = Mathf.Max(GetLayoutProperty(rect, e => e.minHeight, 0), GetLayoutProperty(rect, e => e.preferredHeight, 0));
if (max > GetLayoutProperty(rect, e => e.maxHeight, int.MaxValue))
{
return GetLayoutProperty(rect, e => e.maxHeight, int.MaxValue);
}
return max;
}
在这里只是简单增加了与最大值属性的对比,若最佳尺寸或最小尺寸大于设置的最大尺寸,则直接返回最大尺寸
注意:如果此处设置的最小尺寸比最大尺寸还要大,则会以最大尺寸返回,例如设置minWidth为200,maxWidth为100,则返回的是100
此时再看Unity,会发现有一大堆报错,是因为ILayoutElement被很多类引用到,因此需要每个类都增加其maxWidth和maxHeight属性
其中,LayoutGroup类需要增加以下代码:
public virtual float maxHeight { get { return GetTotalPreferredSize(1); } }
public virtual float maxWidth { get { return GetTotalPreferredSize(0); } }
其它的类(Image、Text、ScrollRect、InputField)只需要把每个类中对应的preferredWidth和preferredHeight属性复制以下,改个名字就行,preferredWidth对应maxWidth,preferredHeight对应maxHeight
6、最终效果
7、项目工程源代码
https://github.com/CHJ-Self/MyUGUI
大佬们找到问题欢迎拍砖~