UGUI源码分析:LayoutGroup中的纵横布局组件(HorizontalOrVerticalLayoutGroup)

系列

UGUI源码分析系列总览
相关前置:
UGUI CanvasUpdateSystem源码分析
UGUI源码分析:LayoutSystem布局系统


UML图一览

在这里插入图片描述


LayoutGroup

BaseClass: UIBehaviour

Interface: ILayoutElement, ILayoutGroup

Intro:布局系统的实施组件的基类

虽然UGUI组件中有一些组件都继承了ILayoutElement接口,但并不会涉及对接口方法的实现。这是因为这些组件主要是布局操作的接收方,只需要通过该接口被布局实施方所发现即可

LayoutGroup,是布局组件的基类(GridLayoutGroup、HorizontalOrVerticalLayoutGroup)。本文主要针对纵横布局组件(HorizontalLayoutGroup、VerticalLayoutGroup)进行分析。

属性介绍

在这里插入图片描述

  • Padding:内部边距,调整实际用于布局区域的大小
  • Spacing :子物体直接的间隔
  • Child Alignment :子物体对齐方式
  • Child Controls Size :组件控制子物体尺寸开关,开启时组件可以更改物体尺寸。
  • Child Force Expand :组件控制子物体填充区域开关,若可以修改尺寸则会改变子物体尺寸填充区域,若不可以修改尺寸,则根据区域大小均衡分布子物体。

布局过程

接下来,作者以HorizontalLayoutGroup为例,逐步进行布局实现的分析。

扫描二维码关注公众号,回复: 11259535 查看本文章

延续LayoutRebuilderRebuild方法(详情:UGUI源码分析:LayoutSystem布局系统),首先被执行的是ILayoutElementCalculateLayoutInputHorizontal方法。该方法将收集其子节点下所有没有被标记ignoreLayout的物体(m_RectChildren)。

// HorizontalLayoutGroup
public override void CalculateLayoutInputHorizontal()
{
    //  基类(LayoutGroup)方法
    base.CalculateLayoutInputHorizontal();
    CalcAlongAxis(0, false);
}
//LayoutGroup 
public virtual void CalculateLayoutInputHorizontal()
{
    //清空list,准备收集子节点下没有被ignoreLayout的物体
    m_RectChildren.Clear();
    var toIgnoreList = ListPool<Component>.Get();
    for (int i = 0; i < rectTransform.childCount; i++)
    {
        var rect = rectTransform.GetChild(i) as RectTransform;
        if (rect == null || !rect.gameObject.activeInHierarchy)
            continue;
        rect.GetComponents(typeof(ILayoutIgnorer), toIgnoreList);
        if (toIgnoreList.Count == 0)
        {
            m_RectChildren.Add(rect);
            continue;
        }
        for (int j = 0; j < toIgnoreList.Count; j++)
        {
            var ignorer = (ILayoutIgnorer)toIgnoreList[j];
            if (!ignorer.ignoreLayout)
            {
                m_RectChildren.Add(rect);
                break;
            }
        }
    }
    ListPool<Component>.Release(toIgnoreList);
    m_Tracker.Clear();
}

CalcAlongAxis 主要是做LayoutGroup的一些初始化参数的计算。

//LayoutGroup 
protected void CalcAlongAxis(int axis, bool isVertical)
{
    float combinedPadding = (axis == 0 ? padding.horizontal : padding.vertical);
    bool controlSize = (axis == 0 ? m_ChildControlWidth : m_ChildControlHeight);
    bool childForceExpandSize = (axis == 0 ? childForceExpandWidth : 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 (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);
    //根据轴设置 m_TotalXXX值
    SetLayoutInputForAxis(totalMin, totalPreferred, totalFlexible, axis);
}

STEP2:接着会执行ILayoutControllerSetLayoutHorizontal方法。这在GridLayoutGroupHorizontalLayoutGroupVerticalLayoutGroup中有不同的处理。

//HorizontalLayoutGroup
public override void SetLayoutHorizontal()
{
    //根据轴设置子物体的布局
    SetChildrenAlongAxis(0, false);
}

LayoutGroup组件如何调整子物体的位置与大小一句话概括:利用了Unity RectTransform中的一个方法

SetInsetAndSizeFromParentEdge(RectTransform.Edge edge, float inset, float size);

布局物体的方法主要是在 选择出目标边(Edge),计算出距离(inset),计算出子物体的大小(size)

//LayoutGroup 
protected void SetChildrenAlongAxis(int axis, bool isVertical)
{
    //获取跟坐标轴有关的设置
    float size = rectTransform.rect.size[axis];
    bool controlSize = (axis == 0 ? m_ChildControlWidth : m_ChildControlHeight);
    bool childForceExpandSize = (axis == 0 ? childForceExpandWidth : childForceExpandHeight);
    float alignmentOnAxis = GetAlignmentOnAxis(axis);

    bool alongOtherAxis = (isVertical ^ (axis == 1)); // 当二者不同时为true  例(水平 y轴,垂直 x轴)
    if (alongOtherAxis)
    {
        //在水平或垂直布局中,另外一条轴的布局操作相对简单一些
        //实际尺寸,根据padding计算
        float innerSize = size - (axis == 0 ? padding.horizontal : padding.vertical);
        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);
            //若强制填充,则会以该部件组件的尺寸来决定,反之则以子物体的最佳尺寸
            float requiredSpace = Mathf.Clamp(innerSize, min, flexible > 0 ? size : preferred);
            //计算距离边的距离
            float startOffset = GetStartOffset(axis, requiredSpace);
            if (controlSize)
            {
                // 根据轴选取矩形的边,以及距离、尺寸,设置子物体的位置(API:SetInsetAndSizeFromParentEdge)
                SetChildAlongAxis(child, axis, startOffset, requiredSpace);
            }
            else
            {
                float offsetInCell = (requiredSpace - child.sizeDelta[axis]) * alignmentOnAxis;
                SetChildAlongAxis(child, axis, startOffset + offsetInCell);
            }
        }
    }
    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, preferred, flexible;
            GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);

            float childSize = Mathf.Lerp(min, preferred, minMaxLerp);
            childSize += flexible * itemFlexibleMultiplier;
            if (controlSize)
            {
                // 根据轴选取矩形的边,以及距离、尺寸,设置子物体的位置(API:SetInsetAndSizeFromParentEdge)
                SetChildAlongAxis(child, axis, pos, childSize);
            }
            else
            {
                float offsetInCell = (childSize - child.sizeDelta[axis]) * alignmentOnAxis;
                SetChildAlongAxis(child, axis, pos + offsetInCell);
            }
            //更新距离,累计子物体尺寸与间隔
            pos += childSize + spacing;
        }
    }
}

到此,纵横布局组件的布局分析就完毕啦,下篇文章作者会对GridLayoutGroup(网格布局组件)以及ILayoutController另一个衍生ContentSizeFitter(尺寸调节组件)进行详细分析。


.
.
.
.
.


嗨,我是作者Vin129,逐儿时之梦正在游戏制作的技术海洋中漂泊。知道的越多,不知道的也越多。希望我的文章对你有所帮助:)


猜你喜欢

转载自blog.csdn.net/qq_28820675/article/details/106304743
今日推荐