两个调整UI的便捷工具

一、两个需求

在拼UI的过程中,经常需要手动干两件事。
如果能够自动处理,可以节省大量时间,对性能也有好处(不必要的重叠可能导致不能合批)。
(更重要的,这是布局强迫症的福音)

需求1:
        为选中的多个同级物体增加一个Box,
        使其位置等于 所有选中物体的总中心
        使其大小等于 所有选中物体的总大小。
        执行后,选中物体的世界坐标均不变。

结果预览1
结果预览1:创建包围盒

需求2:
        调整选中物体,使其位置等于 所有子物体(不包含自身)的总中心。
        使其大小等于 所有子物体(不包含自身)的总大小。 
        调整后,选中物体的锚点、中心点保持不变。
        调整后,选中物体的所有子物体的世界坐标均不变。

结果预览2:调整父物体

二、两个实现

实现1:

using UnityEngine;
using UnityEditor;
using System.Linq;

namespace NRatel
{
    /// <summary>
    /// 意图:
    ///     为选中的多个同级物体增加一个Box,
    ///     使其位置等于 所有选中物体的总中心
    ///     使其大小等于 所有选中物体的总大小。
    ///     执行后,选中物体的世界坐标均不变。
    /// 思路:
    ///     第一步:计算选中物体的总中心、总大小。
    ///     第二步:创建Box,设置位置和大小。
    ///     第三步:将选中物体依次挂入Box(选中物体世界坐标不发生变化)。
    /// </summary>
    public class CreateUIBox : Editor
    {
        [MenuItem("GameObject/Tools/CreateUIBox", false, 0)]
        public static void DoCreate()
        {
            if (Selection.gameObjects == null || Selection.gameObjects.Length <= 0) { return; }
            if (Selection.gameObjects.Length == 1 && Selection.gameObjects[0].name == "#UIBox#") { return; } //防止多次操作

            Transform parent = Selection.gameObjects[0].transform.parent;

            for (int i = 0; i < Selection.gameObjects.Length; i++)
            {
                if (Selection.gameObjects[i].transform.parent != parent)
                {
                    EditorUtility.DisplayDialog("Error", "暂只允许处理同级物体", "OK");
                    return;
                }
            }

            //计算选择物体的总Bounds
            Bounds bounds = RectTransformUtility.CalculateRelativeRectTransformBounds(parent, Selection.gameObjects[0].transform);
            for (int i = 1; i < Selection.gameObjects.Length; i++)
            {
                Bounds b = RectTransformUtility.CalculateRelativeRectTransformBounds(parent, Selection.gameObjects[i].transform);
                bounds.Encapsulate(b);
            }

            Undo.IncrementCurrentGroup();
            int groupIndex = Undo.GetCurrentGroup();
            Undo.SetCurrentGroupName("CreateUIBox");
            {
                //创建Box
                GameObject box = new GameObject("#UIBox#", typeof(RectTransform));
                Undo.RegisterCreatedObjectUndo(box, "CreateUIBox");

                //设置box位置、大小
                RectTransform boxRT = box.GetComponent<RectTransform>();
                boxRT.SetParent(parent);
                boxRT.localPosition = bounds.center;
                boxRT.localScale = Vector3.one;
                boxRT.sizeDelta = new Vector2(bounds.size.x, bounds.size.y);

                //将选择的物体按原Sibling挂入
                GameObject[] sortedObjs = Selection.gameObjects.OrderBy(x => x.transform.GetSiblingIndex()).ToArray();
                for (int i = 0; i < sortedObjs.Length; i++)
                {
                    Undo.SetTransformParent(sortedObjs[i].transform, boxRT, "MoveItemToBox");
                }
                Selection.activeGameObject = box;
            }
            Undo.CollapseUndoOperations(groupIndex);
        }
    }
}

要注意:
        1、同时选中多个物体,由MenuItem执行时,会触发多次,应想办法避免。我这里的实现可能不太完美。
        2、Bounds bounds = new Bounds(); 这样的创建会将 Vector3.zero 包进去。让我查了大半天。。

        3、操作需要可撤销。

---------------------------------- NRatel割 ----------------------------------

实现2:

using UnityEngine;
using UnityEditor;

namespace NRatel
{
    /// <summary>
    /// 意图: 
    ///     调整选中物体
    ///     使其位置等于 所有子物体(不包含自身)的总中心。
    ///     使其大小等于 所有子物体(不包含自身)的总大小。
    ///     调整后,选中物体的锚点、中心点保持不变。
    ///     调整后,选中物体的所有子物体的世界坐标均不变。
    /// 思路:
    ///     第一步:计算所有子物体(不包含自身)的总中心 和 总大小,
    ///     第二步:将Box大小设为所有子物体的总大小、将Box移动至所有子物体的总中心。
    ///     第三步:将子物体移动至原位置。
    /// </summary>
    public class AdjustUIBox : Editor
    {
        [MenuItem("GameObject/Tools/AdjustUIBox", false, 0)]
        public static void DoAdjust()
        {
            if (Selection.gameObjects == null || Selection.gameObjects.Length <= 0) { return; }
            if (Selection.gameObjects.Length > 1)
            {
                EditorUtility.DisplayDialog("Error", "只允许同时处理一个物体", "OK");
                return;
            }

            Transform box = Selection.gameObjects[0].transform;
            Transform boxParent = box.parent;
            Transform child0 = box.GetChild(0);

            //计算所有子物体(不包含自身)的总中心 和 总大小
            Bounds boundsInBox = RectTransformUtility.CalculateRelativeRectTransformBounds(box, child0); //box坐标系下
            Bounds boundsInBoxParent = RectTransformUtility.CalculateRelativeRectTransformBounds(boxParent, child0); //boxParent坐标系下
            for (int i = 1; i < box.childCount; i++)
            {
                Transform childi = box.GetChild(i);
                boundsInBox.Encapsulate(RectTransformUtility.CalculateRelativeRectTransformBounds(box, childi));
                boundsInBoxParent.Encapsulate(RectTransformUtility.CalculateRelativeRectTransformBounds(boxParent, childi));
            }

            //Debug.Log("box: " + boundsInBox.center + ", " + boundsInBox.size);
            //Debug.Log("boxParent: " + boundsInBoxParent.center + ", " + boundsInBoxParent.size);

            Undo.IncrementCurrentGroup();
            int groupIndex = Undo.GetCurrentGroup();
            Undo.SetCurrentGroupName("AdjustUIBox");
            {
                //设置box位置
                RectTransform boxRT = box as RectTransform;
                Undo.RecordObject(boxRT, "boxRT");
                boxRT.localPosition = boundsInBoxParent.center;
                boxRT.sizeDelta = new Vector2(boundsInBoxParent.size.x, boundsInBoxParent.size.y);  //修改box大小,只会影响 anchoredPosition,不会影响localPosition。

                //设置子物体位置
                for (int i = 0; i < box.childCount; i++)
                {
                    Transform child = box.GetChild(i);
                    Undo.RecordObject(child, "boxChild" + i);
                    child.localPosition = child.localPosition - boundsInBox.center;
                }
            }
            Undo.CollapseUndoOperations(groupIndex);
        }
    }
}

要注意:
        1、需求1中要注意的2、3条事项。
        2、对 box 的操作应该在 boxParent 坐标系下进行;对 box子物体的操作应该在box坐标系下进行。

---------------------------------- NRatel割 ----------------------------------

三、其他

顺便分析一下 RectTransformUtility.CalculateRelativeRectTransformBounds 的源码。

1、取 child 及其所有子物体的角世界坐标。
2、变换到 root 本地坐标系下。
3、遍历融合所有角坐标,得到最小坐标和最大坐标,确定出Bounds。

public static Bounds CalculateRelativeRectTransformBounds(Transform root, Transform child)
{
    RectTransform[] rects = child.GetComponentsInChildren<RectTransform>(false);

    if (rects.Length > 0)
    {
        Vector3 vMin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
        Vector3 vMax = new Vector3(float.MinValue, float.MinValue, float.MinValue);

        Matrix4x4 toLocal = root.worldToLocalMatrix;

        for (int i = 0, imax = rects.Length; i < imax; i++)
        {
            rects[i].GetWorldCorners(s_Corners);
            for (int j = 0; j < 4; j++)
            {
                Vector3 v = toLocal.MultiplyPoint3x4(s_Corners[j]);
                vMin = Vector3.Min(v, vMin);
                vMax = Vector3.Max(v, vMax);
            }
        }

        Bounds b = new Bounds(vMin, Vector3.zero);
        b.Encapsulate(vMax);
        return b;
    }
    return new Bounds(Vector3.zero, Vector3.zero);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

NRatel

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

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

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

打赏作者

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

抵扣说明:

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

余额充值