UGUI 源码之 LayoutGroup

一、重点:

1、LayoutGroup 继承自 UIBehaviour,可以利用其生命周期方法设置脏标记。

2、LayoutGroup 实现了两个接口 ILayoutElement, ILayoutGroup,说明它既是一个布局元素,又是一个布局控制器。

3、CalculateLayoutInputHorizontal() 为子类提供一个通用的、计算有效的 m_RectChildren(没有实现 ILayoutIgnorer 接口的子物体 和 实现了 ILayoutIgnorer 接口 但 ignoreLayout 为 false 的子物体 的列表) 的方法。

4、GetStartOffset(int axis, float requiredSpaceWithoutPadding) 方法返回沿给定轴的第一个子布局元素的位置。(它根据 padding和Alignment共同确定了第一个应该出现的位置)

参数 requiredSpaceWithoutPadding:给定轴上所有布局元素所需的总空间,包括间距和边距。  
如:actualCellCountX * cellSize.x + (actualCellCountX - 1) * spacing.x

算式1:alignmentOnAxis = ((int)childAlignment % 3) * 0.5f;

将枚举 childAlignment 转为小数形式,

水平方向:0~8 转为 0左,0.5中,1右。

竖直方向:0~8 转为 0上,0.5中,1下。

算式2:pos = (axis == 0 ? padding.left : padding.top) + surplusSpace * alignmentOnAxis; 

水平方向从左开始,竖直方向从上开始。
要计入剩余尺寸。以水平方向为例,
alignmentOnAxis 为0时, 结果为 padding.left + 0,可以达到整体居左效果;
alignmentOnAxis 为0.5时, 结果为 padding.left + 0.5*剩余距离,可以达到整体居中效果;
alignmentOnAxis 为1时, 结果为 padding.left + 1*剩余距离,可以达到整体居右效果。

5、LayoutGroup 利用 m_Tracker 驱动子物体的锚点和位置(不可修改)。

子物体的锚点:强制左上。

6、提供通用的设置元素坐标的方法 SetChildAlongAxisWithScale(RectTransform rect, int axis, float pos, float size, float scaleFactor)。

由子类调用,实际是通过初始坐标,计入元素的中心点偏移和缩放系数。

位置x轴:传入的初始位置+宽度*中心偏移*缩放系数 (x轴是向正方向)(从左上到右下)。
位置y轴:-传入的初始位置-宽度*(1-中心偏移)*缩放系数 (y轴是向负方向)(从左上到右下)。

二、标记为需要重新布局的地方:

1、OnEnable() 时,执行 SetDirty();

2、OnDidApplyAnimationProperties() 时,执行 SetDirty();

3、OnRectTransformDimensionsChange() 时,若自身是最顶层 ILayoutGroup 则执行 SetDirty();

4、OnTransformChildrenChanged() 时,执行 SetDirty();

5、SetProperty() 方法被调用时(布局属性发生变化,如宽高、大小、轴向、间距、排布方向等),执行 SetDirty();

6、OnDisable() 时,直接执行 LayoutRebuilder.MarkLayoutForRebuild(rectTransform);

(注意:SetDirty()中,若布局重建正在进行,则延迟一帧执行LayoutRebuilder.MarkLayoutForRebuild(rectTransform);  否则立即执行)

三、源码注释:

更多 UGUI 注释已放入  GitHub - NRatel/uGUI: Source code for the Unity UI system.

 using System.Collections;
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;

namespace UnityEngine.UI
{
    [DisallowMultipleComponent]
    [ExecuteAlways]
    [RequireComponent(typeof(RectTransform))]

    // Abstract base class to use for layout groups.
    // 为一组子元素布局的抽象基类
    public abstract class LayoutGroup : UIBehaviour, ILayoutElement, ILayoutGroup
    {
        [SerializeField] protected RectOffset m_Padding = new RectOffset();

        // The padding to add around the child layout elements.
        // 子布局元素边距。  
        public RectOffset padding { get { return m_Padding; } set { SetProperty(ref m_Padding, value); } }

        [SerializeField] protected TextAnchor m_ChildAlignment = TextAnchor.UpperLeft;

        // The alignment to use for the child layout elements in the layout group.
        // some child layout elements specify no flexible width and height, the children may not take up all the available space inside the layout group.
        // The alignment setting specifies how to align them within the layout group when this is the case.
        // 布局组中,子元素的对齐方式
        // 一些子布局元素没有指定灵活的宽度和高度,子布局元素可能不会占用布局组内的所有可用空间。
        // 这种情况下,“子元素对齐方式”将指定如何在布局组中对齐子元素。
        public TextAnchor childAlignment { get { return m_ChildAlignment; } set { SetProperty(ref m_ChildAlignment, value); } }

        [System.NonSerialized] private RectTransform m_Rect;
        protected RectTransform rectTransform
        {
            get
            {
                if (m_Rect == null)
                    m_Rect = GetComponent<RectTransform>();
                return m_Rect;
            }
        }

        protected DrivenRectTransformTracker m_Tracker;     // 用来控制 Transform :https://docs.unity3d.com/cn/2018.4/ScriptReference/DrivenRectTransformTracker.html
        private Vector2 m_TotalMinSize = Vector2.zero;      //该LayoutGroup的MinSize
        private Vector2 m_TotalPreferredSize = Vector2.zero;    //该LayoutGroup的PreferredSize
        private Vector2 m_TotalFlexibleSize = Vector2.zero;  //该LayoutGroup的FlexibleSize

        [System.NonSerialized] private List<RectTransform> m_RectChildren = new List<RectTransform>(); //子布局元素
        protected List<RectTransform> rectChildren { get { return m_RectChildren; } }

        // ILayoutElement Interface
        // 实现 ILayoutElement 的方法
        // 虚方法,只是为子类提供一个通用的、计算有效的 m_RectChildren 的方法。
        public virtual void CalculateLayoutInputHorizontal()
        {
            m_RectChildren.Clear(); //计算前先清理上次计算的子 RectTransform 列表
            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);   //取实现了 ILayoutIgnorer 接口的子物体列表

                if (toIgnoreList.Count == 0)
                {
                    m_RectChildren.Add(rect);   //将没有实现 ILayoutIgnorer 接口的子物体加入 m_RectChildren。
                    continue;
                }

                for (int j = 0; j < toIgnoreList.Count; j++)
                {
                    var ignorer = (ILayoutIgnorer)toIgnoreList[j];
                    if (!ignorer.ignoreLayout)
                    {
                        m_RectChildren.Add(rect); //将实现了 ILayoutIgnorer 接口,但 ignoreLayout 为false 的子物体加入 m_RectChildren。
                        break;
                    }
                }
            }
            ListPool<Component>.Release(toIgnoreList);
            m_Tracker.Clear();
        }

        // 实现 ILayoutElement 的方法
        // 抽象方法
        public abstract void CalculateLayoutInputVertical();

        // See LayoutElement.minWidth
        // 实现 ILayoutElement 的方法
        public virtual float minWidth { get { return GetTotalMinSize(0); } }

        // See LayoutElement.preferredWidth
        // 实现 ILayoutElement 的方法
        public virtual float preferredWidth { get { return GetTotalPreferredSize(0); } }

        // See LayoutElement.flexibleWidth
        // 实现 ILayoutElement 的方法
        public virtual float flexibleWidth { get { return GetTotalFlexibleSize(0); } }

        // See LayoutElement.minHeight
        // 实现 ILayoutElement 的方法
        public virtual float minHeight { get { return GetTotalMinSize(1); } }

        // See LayoutElement.preferredHeight
        // 实现 ILayoutElement 的方法
        public virtual float preferredHeight { get { return GetTotalPreferredSize(1); } }

        // See LayoutElement.flexibleHeight
        // 实现 ILayoutElement 的方法
        public virtual float flexibleHeight { get { return GetTotalFlexibleSize(1); } }

        // See LayoutElement.layoutPriority
        // 实现 ILayoutElement 的方法
        public virtual int layoutPriority { get { return 0; } }

        // ILayoutController Interface
        // 实现 ILayoutElement 的方法
        public abstract void SetLayoutHorizontal();
        // 实现 ILayoutElement 的方法
        public abstract void SetLayoutVertical();

        protected LayoutGroup()
        {
            if (m_Padding == null)
                m_Padding = new RectOffset();   //默认边距
        }

        // 重写 UIBehaviour 的方法
        protected override void OnEnable()
        {
            base.OnEnable();
            SetDirty();     //组件启用时标记为需要重新布局
        }

        // 重写 UIBehaviour 的方法
        // 疑问???
        // 为什么OnEnable中用SetDirty(); 而这里直接调用 LayoutRebuilder.MarkLayoutForRebuild(rectTransform)
        // 估计是因为 OnDisable 时铁定要立即标记为需要重新布局
        protected override void OnDisable()
        {
            m_Tracker.Clear(); //清除DrivenRectTransformTracker对子元素的控制
            LayoutRebuilder.MarkLayoutForRebuild(rectTransform);    //组件禁用时标记为需要重新布局
            base.OnDisable();
        }
        
        // 重写 UIBehaviour 的方法
        // Callback for when properties have been changed by animation.
        // 当动画属性变化时,方法被调用。
        protected override void OnDidApplyAnimationProperties()
        {
            SetDirty();
        }

        // The min size for the layout group on the given axis.
        // 给定轴上,LayoutGroup 的最小尺寸
        protected float GetTotalMinSize(int axis)
        {
            return m_TotalMinSize[axis];
        }

        // 给定轴上,LayoutGroup 的偏好尺寸
        protected float GetTotalPreferredSize(int axis)
        {
            return m_TotalPreferredSize[axis];
        }

        // The flexible size for the layout group on the given axis.
        // 给定轴上,LayoutGroup 的灵活尺寸
        protected float GetTotalFlexibleSize(int axis)
        {
            return m_TotalFlexibleSize[axis];
        }

        // Returns the calculated position of the first child layout element along the given axis.
        // 返回沿给定轴的第一个子布局元素的位置。  
        // 参数 "axis":The axis index. 0 is horizontal and 1 is vertical. //轴索引,0是水平的,1是垂直的。
        // 参数 "requiredSpaceWithoutPadding":The total space required on the given axis for all the layout elements including spacing and excluding padding. //给定轴上所有布局元素所需的总空间,包括间距和边距。  
        // 返回值:The position of the first child along the given axis. //沿给定轴的第一个子节点的位置
        //---------------------------------------------------------
        // 影响顺序:子元素对齐设置 =》 布局起点 =》 整体位置对齐
        // 注意影响因素:左边距、上边距、剩余尺寸(总需要-实际)。
        protected float GetStartOffset(int axis, float requiredSpaceWithoutPadding)
        {
            float requiredSpace = requiredSpaceWithoutPadding + (axis == 0 ? padding.horizontal : padding.vertical);  //该轴上子元素需要的总尺寸 + 边距
            float availableSpace = rectTransform.rect.size[axis];   //该轴上 LayoutGroup 的实际有效尺寸
            float surplusSpace = availableSpace - requiredSpace;  //剩余尺寸(可以是负的)
            float alignmentOnAxis = GetAlignmentOnAxis(axis);   //获取小数形式的子元素对齐方式

            //水平方向从左开始,竖直方向从上开始。
            // 要计入剩余尺寸。以水平方向为例,
            // 若对齐方式为居左,则 alignmentOnAxis 为 0, 结果为 padding.left + 0,可以达到居左效果;
            // 若对齐方式为居中,则 alignmentOnAxis 为 0.5, 结果为 padding.left + 0.5*剩余距离,可以达到居中效果;
            // 若对齐方式为居右,则 alignmentOnAxis 为 1, 结果为 padding.left + 1*剩余距离,可以达到居右效果。
            return (axis == 0 ? padding.left : padding.top) + surplusSpace * alignmentOnAxis; 
        }

        // Returns the alignment on the specified axis as a fraction where 0 is left/top, 0.5 is middle, and 1 is right/bottom.
        // 以小数形式返回指定轴上的对齐方式,其中0为左/上,0.5为中,1为右/下。(水平方向:0左,0.5中,1右)(竖直方向:0上,0.5中,1下)
        // 参数 "axis":The axis to get alignment along. 0 is horizontal and 1 is vertical.    //轴索引,0是水平的,1是垂直的。
        // 返回值:The alignment as a fraction where 0 is left/top, 0.5 is middle, and 1 is right/bottom. //小数形式的对齐方式
        protected float GetAlignmentOnAxis(int axis)
        {
            if (axis == 0)
                return ((int)childAlignment % 3) * 0.5f;  // TextAnchor 水平方向 0~8 转为 0左,0.5中,1右。
            else
                return ((int)childAlignment / 3) * 0.5f;  // TextAnchor 竖直方向 0~8 转为 0上,0.5中,1下。
        }

        // Used to set the calculated layout properties for the given axis.
        // 用于为指定轴设置布局属性
        // 参数"totalMin":The min size for the layout group.    //LayoutGroup 的最小尺寸
        // 参数"totalPreferred":The preferred size for the layout group. //LayoutGroup 的偏好尺寸
        // 参数"totalFlexible":The flexible size for the layout group.  //LayoutGroup 的灵活尺寸
        // 参数"axis":The axis to set sizes for. 0 is horizontal and 1 is vertical. //要设置尺寸的轴,0是水平的,1是垂直的。
        protected void SetLayoutInputForAxis(float totalMin, float totalPreferred, float totalFlexible, int axis)
        {
            m_TotalMinSize[axis] = totalMin;
            m_TotalPreferredSize[axis] = totalPreferred;
            m_TotalFlexibleSize[axis] = totalFlexible;
        }

        // Set the position and size of a child layout element along the given axis.
        // 沿给定轴设置子布局元素的位置和大小。  
        // 参数"rect":The RectTransform of the child layout element. //子布局元素 RectTransform
        // 参数"axis":The axis to set the position and size along. 0 is horizontal and 1 is vertical. //要设置尺寸的轴,0是水平的,1是垂直的。
        // 参数"pos":The position from the left side or top. // 从左/上开始的位置
        protected void SetChildAlongAxis(RectTransform rect, int axis, float pos)
        {
            if (rect == null)
                return;

            SetChildAlongAxisWithScale(rect, axis, pos, 1.0f);
        }

        // Set the position and size of a child layout element along the given axis.
        // 沿给定轴设置子布局元素的位置和大小。  
        // 参数"rect":The RectTransform of the child layout element. //子布局元素 RectTransform
        // 参数"axis":The axis to set the position and size along. 0 is horizontal and 1 is vertical. //要设置尺寸的轴,0是水平的,1是垂直的。
        // 参数"pos":The position from the left side or top. // 从左/上开始的位置
        // 参数"scaleFactor":缩放因子
        protected void SetChildAlongAxisWithScale(RectTransform rect, int axis, float pos, float scaleFactor)
        {
            if (rect == null)
                return;

            // 驱动子物体的锚点和位置
            m_Tracker.Add(this, rect,
                DrivenTransformProperties.Anchors |
                (axis == 0 ? DrivenTransformProperties.AnchoredPositionX : DrivenTransformProperties.AnchoredPositionY));

            // Inlined rect.SetInsetAndSizeFromParentEdge(...) and refactored code in order to multiply desired size by scaleFactor.
            // sizeDelta must stay the same but the size used in the calculation of the position must be scaled by the scaleFactor.
            // 内联 rect.SetInsetAndSizeFromParentEdge(…) 并且 重构代码,以便将所需的大小乘以scaleFactor。  
            // sizelta 必须保持不变,但在计算位置时使用的大小必须由scaleFactor缩放。

            // 强制设置锚点为左上
            rect.anchorMin = Vector2.up;
            rect.anchorMax = Vector2.up;

            // 设置子物体位置
            // x轴:初始位置+宽度*中心点偏移*缩放系数 (x轴是向正方向)(从左上到右下)
            // y轴:-初始位置-宽度*(1-中心点偏移)*缩放系数 (y轴是向负方向)(从左上到右下)
            Vector2 anchoredPosition = rect.anchoredPosition;
            anchoredPosition[axis] = (axis == 0) ? (pos + rect.sizeDelta[axis] * rect.pivot[axis] * scaleFactor) : (-pos - rect.sizeDelta[axis] * (1f - rect.pivot[axis]) * scaleFactor);
            rect.anchoredPosition = anchoredPosition;
        }

        // Set the position and size of a child layout element along the given axis.
        // 沿给定轴设置子布局元素的位置和大小。  
        // 参数"rect":The RectTransform of the child layout element. //子布局元素 RectTransform
        // 参数"axis":The axis to set the position and size along. 0 is horizontal and 1 is vertical. //要设置尺寸的轴,0是水平的,1是垂直的。
        // 参数"pos":The position from the left side or top. // 从左/上开始的位置
        // 参数"size":大小
        protected void SetChildAlongAxis(RectTransform rect, int axis, float pos, float size)
        {
            if (rect == null)
                return;

            SetChildAlongAxisWithScale(rect, axis, pos, size, 1.0f);
        }

        // Set the position and size of a child layout element along the given axis.
        // 沿给定轴设置子布局元素的位置和大小。  
        // 参数"rect":The RectTransform of the child layout element. //子布局元素 RectTransform
        // 参数"axis":The axis to set the position and size along. 0 is horizontal and 1 is vertical. //要设置尺寸的轴,0是水平的,1是垂直的。
        // 参数"pos":The position from the left side or top. // 从左/上开始的位置
        // 参数"size":大小
        protected void SetChildAlongAxisWithScale(RectTransform rect, int axis, float pos, float size, float scaleFactor)
        {
            if (rect == null)
                return;

            m_Tracker.Add(this, rect,
                DrivenTransformProperties.Anchors |
                (axis == 0 ?
                    (DrivenTransformProperties.AnchoredPositionX | DrivenTransformProperties.SizeDeltaX) :
                    (DrivenTransformProperties.AnchoredPositionY | DrivenTransformProperties.SizeDeltaY)
                )
            );

            // Inlined rect.SetInsetAndSizeFromParentEdge(...) and refactored code in order to multiply desired size by scaleFactor.
            // sizeDelta must stay the same but the size used in the calculation of the position must be scaled by the scaleFactor.

            // 强制设置锚点为左上
            rect.anchorMin = Vector2.up;
            rect.anchorMax = Vector2.up;

            // 设置大小为传入的size
            Vector2 sizeDelta = rect.sizeDelta;
            sizeDelta[axis] = size;
            rect.sizeDelta = sizeDelta;

            // 算法同上
            Vector2 anchoredPosition = rect.anchoredPosition;
            anchoredPosition[axis] = (axis == 0) ? (pos + size * rect.pivot[axis] * scaleFactor) : (-pos - size * (1f - rect.pivot[axis]) * scaleFactor);
            rect.anchoredPosition = anchoredPosition;
        }

        //是否最顶层的 LayoutGroup
        private bool isRootLayoutGroup
        {
            get
            {
                Transform parent = transform.parent;
                if (parent == null)
                    return true;
                return transform.parent.GetComponent(typeof(ILayoutGroup)) == null;
            }
        }

        protected override void OnRectTransformDimensionsChange()
        {
            base.OnRectTransformDimensionsChange();
            if (isRootLayoutGroup)
                SetDirty();
        }

        protected virtual void OnTransformChildrenChanged()
        {
            SetDirty();
        }

        // 帮助方法,用于在给定属性发生更改时设置该属性。
        // currentValue:A reference to the member value. //成员值的引用
        // newValue:The new value.  //新值
        protected void SetProperty<T>(ref T currentValue, T newValue)
        {
            if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))  //过滤无效和未变
                return;
            currentValue = newValue;
            SetDirty();
        }

        // Mark the LayoutGroup as dirty.
        // 标记 LayoutGroup 为脏
        // 若布局重建正在进行,则延迟一帧
        protected void SetDirty()
        {
            if (!IsActive())
                return;

            if (!CanvasUpdateRegistry.IsRebuildingLayout())
                LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
            else
                StartCoroutine(DelayedSetDirty(rectTransform));
        }

        IEnumerator DelayedSetDirty(RectTransform rectTransform)
        {
            yield return null;
            LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
        }

    #if UNITY_EDITOR
        protected override void OnValidate()
        {
            SetDirty();
        }

    #endif
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NRatel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值