文章目录
- 简介
- 实现原理
- Editor 编辑器
简介
Unity中提供了三种类型的自动布局组件,分别是Grid Layou Group
、Horizontal Layout Group
、Vertical Layout Group
,本文自定义了一个圆形的自动布局组件Circle Layout Group
,如图所示:
Radius
:Circle圆的半径Angle Delta
:两个元素之间的角度差Start Direction
:开始布局的方向(上、下、左、右)Auto Refresh
:是否自动刷新,开启后当子物体数量发生变化时自动刷新布局Control Child Size
:是否控制元素的大小Child Size
:控制元素大小
实现原理
已知圆的中心点(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
中: