Unity CircleLayoutGroup 如何实现一个圆形自动布局组件


简介

Unity中提供了三种类型的自动布局组件,分别是Grid Layou GroupHorizontal Layout GroupVertical Layout Group,本文自定义了一个圆形的自动布局组件Circle Layout Group,如图所示:

Circle Layout Group

  • Radius:Circle圆的半径
  • Angle Delta:两个元素之间的角度差
  • Start Direction:开始布局的方向(上、下、左、右)
  • Auto Refresh:是否自动刷新,开启后当子物体数量发生变化时自动刷新布局
  • Control Child Size:是否控制元素的大小
  • Child Size:控制元素大小

StartDirection:Right

StartDirection:Up

StartDirection:Left

StartDirection:Down

Auto Refresh

实现原理

已知圆的中心点(x0, y0),半径radius ,通过以下公式求得角度a的圆上的点坐标位置(x,y):

float x = x0 + radius * Mathf.Cos(angle * Mathf.PI / 180f);
float y = y0 + radius * Mathf.Sin(angle * Mathf.PI / 180f);

在这里我们的子物体元素以其父级为圆心,所以不需要考虑(x0,y0):

float x = radius * Mathf.Cos(angle * Mathf.PI / 180f);
float y = radius * Mathf.Sin(angle * Mathf.PI / 180f);

三角函数原理:
Sin正弦:y(对边) / radius(斜边)
Cos余弦:x(邻边)/ radius(斜边)

以右侧为0度起点,当方向为上方时加90度,当方向为左侧时加180度,当方向为下方时加270度,并根据角度差和元素的层级顺序计算其角度。

代码实现如下:

using UnityEngine;
using System.Collections.Generic;

namespace SK.Framework.UI
{
    
    
    /// <summary>
    /// 圆形自动布局组件
    /// </summary>
    public class CircleLayoutGroup : MonoBehaviour
    {
    
    
        //半径
        [SerializeField] private float radius = 100f;
        //角度差
        [SerializeField] private float angleDelta = 30f;
        //开始的方向 0-Right 1-Up 2-Left 3-Down
        [SerializeField] private int startDirection = 0;
        //是否自动刷新布局
        [SerializeField] private bool autoRefresh = true;
        //是否控制子物体的大小
        [SerializeField] private bool controlChildSize = true;
        //子物体大小
        [SerializeField] private Vector2 childSize = Vector2.one * 100f;

        //缓存子物体数量
        private int cacheChildCount;

        private void Start()
        {
    
    
            cacheChildCount = transform.childCount;
            RefreshLayout();
        }

        private void Update()
        {
    
    
            //开启自动刷新
            if (autoRefresh)
            {
    
    
                //检测到子物体数量变动
                if (cacheChildCount != transform.childCount)
                {
    
    
                    //刷新布局
                    RefreshLayout();
                    //再次缓存子物体数量
                    cacheChildCount = transform.childCount;
                }
            }    
        }

        /// <summary>
        /// 刷新布局
        /// </summary>
        public void RefreshLayout()
        {
    
    
            //获取所有非隐藏状态的子物体
            List<RectTransform> children = new List<RectTransform>();
            for (int i = 0; i < transform.childCount; i++)
            {
    
    
                Transform child = transform.GetChild(i);
                if (child.gameObject.activeSelf)
                {
    
    
                    children.Add(child as RectTransform);
                }
            }
            //形成的扇形的角度 = 子物体间隙数量 * 角度差
            float totalAngle = (children.Count - 1) * angleDelta;
            //总角度的一半
            float halfAngle = totalAngle * 0.5f;
            //遍历这些子物体
            for (int i = 0; i < children.Count; i++)
            {
    
    
                RectTransform child = children[i];
                /* 以右侧为0度起点 
                 * 方向为Up时角度+90 Left+180 Down+270
                 * 方向为Right和Up时 倒序计算角度 
                 * 确保层级中的子物体按照从左到右、从上到下的顺序自动布局 */
                float angle = angleDelta * (startDirection < 2 ? children.Count - 1 - i : i) - halfAngle + startDirection * 90f;
                //计算x、y坐标
                float x = radius * Mathf.Cos(angle * Mathf.PI / 180f);
                float y = radius * Mathf.Sin(angle * Mathf.PI / 180f);
                //为子物体赋值坐标
                Vector2 anchorPos = child.anchoredPosition;
                anchorPos.x = x;
                anchorPos.y = y;
                child.anchoredPosition = anchorPos;

                //控制子物体大小
                if (controlChildSize)
                {
    
    
                    child.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, childSize.x);
                    child.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, childSize.y);
                }
            }
        }
    }
}

Editor 编辑器

通过上述代码可以实现Runtime运行时的布局自动刷新,想要在Editor编辑环境编辑时自动刷新还需要自定义Editor编辑器,代码如下:

#if UNITY_EDITOR
    [CustomEditor(typeof(CircleLayoutGroup))]
    public class CircleLayoutGroupEditor : Editor
    {
    
    
        private enum Direction
        {
    
    
            Right = 0,
            Up = 1,
            Left = 2,
            Down = 3
        }

        private CircleLayoutGroup circleLayoutGroup;
        private SerializedProperty radius;
        private SerializedProperty angleDelta;
        private SerializedProperty startDirection;
        private SerializedProperty autoRefresh;
        private SerializedProperty controlChildSize;
        private SerializedProperty childSize;
        private Direction direction;

        private void OnEnable()
        {
    
    
            circleLayoutGroup = target as CircleLayoutGroup;
            radius = serializedObject.FindProperty("radius");
            angleDelta = serializedObject.FindProperty("angleDelta");
            startDirection = serializedObject.FindProperty("startDirection");
            autoRefresh = serializedObject.FindProperty("autoRefresh");
            controlChildSize = serializedObject.FindProperty("controlChildSize");
            childSize = serializedObject.FindProperty("childSize");

            direction = (Direction)startDirection.intValue;
            circleLayoutGroup.RefreshLayout();
        }

        public override void OnInspectorGUI()
        {
    
    
            float newRadius = EditorGUILayout.FloatField("Radius", radius.floatValue);
            if (newRadius != radius.floatValue)
            {
    
    
                Undo.RecordObject(target, "Radius");
                radius.floatValue = newRadius;
                IsChanged();
            }

            float newAngleDelta = EditorGUILayout.FloatField("Angle Delta", angleDelta.floatValue);
            if (newAngleDelta != angleDelta.floatValue)
            {
    
    
                Undo.RecordObject(target, "Angle Delta");
                angleDelta.floatValue = newAngleDelta;
                IsChanged();
            }

            Direction newDirection = (Direction)EditorGUILayout.EnumPopup("Start Direction", direction);
            if (newDirection != direction) 
            {
    
    
                Undo.RecordObject(target, "Start Direction");
                direction = newDirection;
                startDirection.intValue = (int)direction;
                IsChanged();
            }

            bool newAutoRefresh = EditorGUILayout.Toggle("Auto Refresh", autoRefresh.boolValue);
            if (newAutoRefresh != autoRefresh.boolValue)
            {
    
    
                Undo.RecordObject(target, "Angle Refresh");
                autoRefresh.boolValue = newAutoRefresh;
                IsChanged();
            }

            bool newControlChildSize = EditorGUILayout.Toggle("Control Child Size", controlChildSize.boolValue);
            if (newControlChildSize != controlChildSize.boolValue)
            {
    
    
                Undo.RecordObject(target, "Control Child Size");
                controlChildSize.boolValue = newControlChildSize;
                IsChanged();
            }

            if (controlChildSize.boolValue)
            {
    
    
                Vector2 newChildSize = EditorGUILayout.Vector2Field("Child Size", childSize.vector2Value);
                if (newChildSize != childSize.vector2Value)
                {
    
    
                    Undo.RecordObject(target, "Child Size");
                    childSize.vector2Value = newChildSize;
                    IsChanged();
                }
            }
        }

        private void IsChanged()
        {
    
    
            if (GUI.changed)
            {
    
    
                serializedObject.ApplyModifiedProperties();
                EditorUtility.SetDirty(target);
                circleLayoutGroup.RefreshLayout();
            }
        }
    }
#endif

工具已上传SKFramework框架Package Manager中:

SKFramework Package Manager

猜你喜欢

转载自blog.csdn.net/qq_42139931/article/details/129064359