2023-08-02 06:08:03 +00:00
// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved.
// This code can only be used under the standard Unity Asset Store End User License Agreement
// A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms
using System.Collections.Generic ;
using Doozy.Runtime.Common.Utils ;
using Doozy.Runtime.Reactor.Animations ;
using Doozy.Runtime.Reactor.Animators ;
using Doozy.Runtime.UIManager.Animators ;
using Doozy.Runtime.UIManager.Components ;
using Doozy.Runtime.UIManager.Layouts.Internal ;
using UnityEngine ;
using UnityEngine.UI ;
// ReSharper disable MemberCanBePrivate.Global
namespace Doozy.Runtime.UIManager.Layouts
{
/// <summary>
/// The Radial Layout component sets child elements in a radial or circular arrangement.
/// </summary>
2023-12-05 06:20:20 +00:00
[AddComponentMenu("Doozy/UI/Layouts/UI RadialLayout")]
2023-08-02 06:08:03 +00:00
[RequireComponent(typeof(RectTransform))]
[DisallowMultipleComponent]
[ExecuteAlways]
public class UIRadialLayout : UILayoutGroup
{
#if UNITY_EDITOR
2023-12-05 06:20:20 +00:00
[UnityEditor.MenuItem("GameObject/Doozy/UI/Layouts/UI RadialLayout", false, 8)]
2023-08-02 06:08:03 +00:00
private static void CreateComponent ( UnityEditor . MenuCommand menuCommand )
{
GameObjectUtils . AddToScene < UIRadialLayout > ( "UI RadialLayout" , false , true ) ;
}
#endif
public const bool k_AutoRebuildDefaultValue = true ;
public const bool k_ClockwiseDefaultValue = true ;
public const bool k_ControlChildHeightDefaultValue = false ;
public const bool k_ControlChildWidthDefaultValue = false ;
public const bool k_RadiusControlsHeightDefaultValue = false ;
public const bool k_RadiusControlsWidthDefaultValue = false ;
public const bool k_RotateChildrenDefaultValue = false ;
public const float k_ChildHeightDefaultValue = k_RadiusDefaultValue ;
public const float k_ChildRotationDefaultValue = 0f ;
public const float k_ChildWidthDefaultValue = k_RadiusDefaultValue ;
public const float k_MAXAngle = 360f ;
public const float k_MAXAngleDefaultValue = 360f ;
public const float k_MAXRadiusDefaultValue = 1000f ;
public const float k_MINAngle = 0f ;
public const float k_MINAngleDefaultValue = 0f ;
public const float k_RadiusDefaultValue = 100f ;
public const float k_RadiusHeightFactorDefaultValue = 1f ;
public const float k_RadiusWidthFactorDefaultValue = 1f ;
public const float k_SpacingDefaultValue = 0f ;
public const float k_StartAngleDefaultValue = 180f ;
[SerializeField] protected bool AutoRebuild = k_AutoRebuildDefaultValue ;
[SerializeField] protected float ChildHeight = k_ChildHeightDefaultValue ;
[SerializeField] protected float ChildRotation = k_ChildRotationDefaultValue ;
[SerializeField] protected float ChildWidth = k_ChildWidthDefaultValue ;
[SerializeField] protected bool Clockwise = k_ClockwiseDefaultValue ;
[SerializeField] protected bool ControlChildHeight = k_ControlChildHeightDefaultValue ;
[SerializeField] protected bool ControlChildWidth = k_ControlChildWidthDefaultValue ;
[Range(k_MINAngle, k_MAXAngle)]
[SerializeField] protected float MaxAngle = k_MAXAngleDefaultValue ;
[SerializeField] protected float MaxRadius = k_MAXRadiusDefaultValue ;
[Range(k_MINAngle, k_MAXAngle)]
[SerializeField] protected float MinAngle = k_MINAngleDefaultValue ;
[SerializeField] protected float Radius = k_RadiusDefaultValue ;
[SerializeField] protected bool RadiusControlsHeight = k_RadiusControlsHeightDefaultValue ;
[SerializeField] protected bool RadiusControlsWidth = k_RadiusControlsWidthDefaultValue ;
[SerializeField] protected float RadiusHeightFactor = k_RadiusHeightFactorDefaultValue ;
[SerializeField] protected float RadiusWidthFactor = k_RadiusWidthFactorDefaultValue ;
[SerializeField] protected bool RotateChildren = k_RotateChildrenDefaultValue ;
[SerializeField] protected float Spacing = k_SpacingDefaultValue ;
[Range(k_MINAngle, k_MAXAngle)]
[SerializeField] protected float StartAngle = k_StartAngleDefaultValue ;
/// <summary> Internal list used to count the number of child elements this layout has. It's main purpose is to improve layout performance by reducing GC </summary>
private List < RectTransform > m_ChildList = new List < RectTransform > ( ) ;
/// <summary> Automatically rebuild the layout when a parameter has changed and update the layout </summary>
public bool autoRebuild
{
get = > AutoRebuild ;
set
{
if ( AutoRebuild = = value ) return ;
AutoRebuild = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Child elements height when control child height is enabled </summary>
public float childHeight
{
get = > ChildHeight ;
set
{
if ( Mathf . Approximately ( ChildHeight , value ) ) return ;
ChildHeight = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Child elements custom rotation </summary>
public float childRotation
{
get = > ChildRotation ;
set
{
if ( Mathf . Approximately ( ChildRotation , value ) ) return ;
ChildRotation = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Child elements width when control child width is enabled </summary>
public float childWidth
{
get = > ChildWidth ;
set
{
if ( Mathf . Approximately ( ChildWidth , value ) ) return ;
ChildWidth = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Order the child elements clockwise and update the layout </summary>
public bool clockwise
{
get = > Clockwise ;
set
{
if ( Clockwise = = value ) return ;
Clockwise = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Override the child elements height and update the layout </summary>
public bool controlChildHeight
{
get = > ControlChildHeight ;
set
{
ControlChildHeight = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Override the child elements width and update the layout </summary>
public bool controlChildWidth
{
get = > ControlChildWidth ;
set
{
ControlChildWidth = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Maximum angle a child element can have inside the layout. Used to make the radial layout look as an arc </summary>
public float maxAngle
{
get = > MaxAngle ;
set
{
if ( Mathf . Approximately ( MaxAngle , value ) ) return ;
MaxAngle = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Minimum angle a child element can have inside the layout. Used to make the radial layout look as an arc </summary>
public float minAngle
{
get = > MinAngle ;
set
{
if ( Mathf . Approximately ( MinAngle , value ) ) return ;
MinAngle = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Layout radius that determines the size of the circle </summary>
public float radius
{
get = > Radius ;
set
{
if ( Mathf . Approximately ( Radius , value ) ) return ;
Radius = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Set the child elements height to be influenced by the layout radius and update the layout </summary>
public bool radiusControlsHeight
{
get = > RadiusControlsHeight ;
set
{
RadiusControlsHeight = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Set the child elements width to be influenced by the layout radius and update the layout </summary>
public bool radiusControlsWidth
{
get = > RadiusControlsWidth ;
set
{
RadiusControlsWidth = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Factor by which the radius influences the child elements height, if radius controls height is enabled </summary>
public float radiusHeightFactor
{
get = > RadiusHeightFactor ;
set
{
if ( Mathf . Approximately ( RadiusHeightFactor , value ) ) return ;
RadiusHeightFactor = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Factor by which the radius influences the child elements width, if the radius controls width is enabled </summary>
public float radiusWidthFactor
{
get = > RadiusWidthFactor ;
set
{
if ( Mathf . Approximately ( RadiusWidthFactor , value ) ) return ;
RadiusWidthFactor = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Automatically rotate child elements with the layout, when the start angle changes and update the layout </summary>
public bool rotateChildren
{
get = > RotateChildren ;
set
{
RotateChildren = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Extra spacing between child elements </summary>
public float spacing
{
get = > Spacing ;
set
{
if ( Mathf . Approximately ( Spacing , value ) ) return ;
Spacing = value ;
OnValueChanged ( ) ;
}
}
/// <summary> Start angle for the first child element of the layout. This places all the child elements around the layout radius </summary>
public float startAngle
{
get = > StartAngle ;
set
{
if ( Mathf . Approximately ( StartAngle , value ) ) return ;
StartAngle = value ;
OnValueChanged ( ) ;
}
}
private bool runUpdateAnimatorsStartPosition { get ; set ; }
#if UNITY_EDITOR
protected override void Reset ( )
{
base . Reset ( ) ;
CalculateRadial ( ) ;
}
#endif
protected override void OnEnable ( )
{
if ( ! Application . isPlaying ) return ;
// base.OnEnable();
runUpdateAnimatorsStartPosition = false ;
CalculateRadial ( ) ;
}
public override void SetLayoutHorizontal ( ) { }
public override void SetLayoutVertical ( ) { }
public override void CalculateLayoutInputVertical ( ) = >
CalculateRadial ( ) ;
public override void CalculateLayoutInputHorizontal ( ) = >
CalculateRadial ( ) ;
/// <summary> Rebuild the layout </summary>
public void CalculateRadial ( )
{
m_ChildList ? ? = new List < RectTransform > ( ) ;
m_ChildList . Clear ( ) ;
int activeChildCount = 0 ;
for ( int i = 0 ; i < transform . childCount ; i + + )
{
var child = transform . GetChild ( i ) as RectTransform ;
if ( child = = null ) continue ;
LayoutElement childLayout = child . GetComponent < LayoutElement > ( ) ;
if ( child = = null | | ! child . gameObject . activeSelf | | ( childLayout ! = null & & childLayout . ignoreLayout ) ) continue ;
m_ChildList . Add ( child ) ;
activeChildCount + + ;
}
m_Tracker . Clear ( ) ;
if ( activeChildCount = = 0 ) return ;
if ( Application . isPlaying & ! runUpdateAnimatorsStartPosition )
{
runUpdateAnimatorsStartPosition = true ;
UpdateAnimatorsStartValues ( ) ;
}
rectTransform . sizeDelta = new Vector2 ( Radius , Radius ) * 2f ;
float sAngle = 360f / activeChildCount * ( activeChildCount - 1f ) ;
float angleOffset = MinAngle ;
if ( angleOffset > sAngle ) angleOffset = sAngle ;
float maximumAngle = 360f - MaxAngle ;
if ( maximumAngle > sAngle ) maximumAngle = sAngle ;
if ( angleOffset > sAngle ) angleOffset = sAngle ;
float buff = sAngle - angleOffset ;
float fOffsetAngle = ( ( buff - maximumAngle ) ) / ( activeChildCount - 1f ) + Spacing ;
float fAngle = StartAngle + angleOffset ;
bool controlChildrenSize = ControlChildWidth | ControlChildHeight ;
DrivenTransformProperties drivenTransformProperties = DrivenTransformProperties . Anchors | DrivenTransformProperties . AnchoredPosition | DrivenTransformProperties . Pivot ;
if ( ControlChildWidth ) drivenTransformProperties | = DrivenTransformProperties . SizeDeltaX ;
if ( ControlChildHeight ) drivenTransformProperties | = DrivenTransformProperties . SizeDeltaY ;
if ( RotateChildren ) drivenTransformProperties | = DrivenTransformProperties . Rotation ;
if ( Clockwise ) fOffsetAngle * = - 1f ;
foreach ( RectTransform child in m_ChildList )
{
if ( child = = null | | ! child . gameObject . activeSelf ) continue ; //if child is null or not active -> continue
m_Tracker . Add ( this , child , drivenTransformProperties ) ; //add elements to the tracker to stop the user from modifying their positions via the editor
var vPos = new Vector3 ( Mathf . Cos ( fAngle * Mathf . Deg2Rad ) , Mathf . Sin ( fAngle * Mathf . Deg2Rad ) , 0 ) ; //calculate the child position
child . localPosition = vPos * Radius ; //set the child position
child . anchorMin = child . anchorMax = child . pivot = new Vector2 ( 0.5f , 0.5f ) ; //force children to be center aligned, to keep all of the objects with the same anchor points
float elementAngle = ChildRotation ;
if ( RotateChildren ) elementAngle + = fAngle ;
child . localEulerAngles = new Vector3 ( 0f , 0f , elementAngle ) ;
if ( controlChildrenSize )
{
Vector2 childSizeDelta = child . sizeDelta ;
if ( controlChildWidth )
childSizeDelta . x = RadiusControlsWidth
? ChildWidth * Radius * RadiusWidthFactor / 100
: ChildWidth ;
if ( controlChildHeight )
childSizeDelta . y = RadiusControlsHeight
? ChildHeight * Radius * RadiusHeightFactor / 100
: ChildHeight ;
child . sizeDelta = childSizeDelta ;
}
fAngle + = fOffsetAngle ;
}
}
private void UpdateAnimatorsStartValues ( )
{
LayoutRebuilder . MarkLayoutForRebuild ( rectTransform ) ;
for ( int i = 0 ; i < transform . childCount ; i + + )
{
var child = transform . GetChild ( i ) as RectTransform ;
if ( child = = null ) continue ;
UIAnimator uiAnimator = child . GetComponent < UIAnimator > ( ) ;
if ( uiAnimator ! = null )
{
uiAnimator . animation . startPosition = uiAnimator . rectTransform . anchoredPosition3D ;
uiAnimator . animation . startRotation = uiAnimator . rectTransform . localEulerAngles ;
if ( uiAnimator . animation . isPlaying ) uiAnimator . UpdateValues ( ) ;
}
UIContainerUIAnimator uiContainerUIAnimator = child . GetComponent < UIContainerUIAnimator > ( ) ;
if ( uiContainerUIAnimator ! = null )
{
if ( uiContainerUIAnimator . isConnected & & uiContainerUIAnimator . controller . isVisible )
{
uiContainerUIAnimator . showAnimation . startPosition = uiContainerUIAnimator . rectTransform . anchoredPosition3D ;
uiContainerUIAnimator . showAnimation . startRotation = uiContainerUIAnimator . rectTransform . localEulerAngles ;
}
// uiContainerAnimator.UpdateSettings();
}
UISelectableUIAnimator uiSelectableUIAnimator = child . GetComponent < UISelectableUIAnimator > ( ) ;
if ( uiSelectableUIAnimator ! = null )
{
if ( uiSelectableUIAnimator . isConnected & & uiSelectableUIAnimator . controller . currentUISelectionState = = UISelectionState . Normal & ! uiSelectableUIAnimator . anyAnimationIsActive )
{
foreach ( UISelectionState state in UISelectable . uiSelectionStates )
{
UIAnimation uiAnimation = uiSelectableUIAnimator . GetAnimation ( state ) ;
if ( uiAnimation = = null ) continue ;
uiAnimation . startPosition = uiAnimation . rectTransform . anchoredPosition3D ;
uiAnimation . startRotation = uiAnimation . rectTransform . localEulerAngles ;
}
}
// uiSelectableUIAnimator.UpdateSettings();
}
}
runUpdateAnimatorsStartPosition = false ;
}
private void OnValueChanged ( )
{
if ( ! AutoRebuild ) return ;
CalculateRadial ( ) ;
}
}
}