227 lines
8.8 KiB
C#
227 lines
8.8 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using UnityEngine.EventSystems;
|
|
|
|
namespace Gpm.Ui
|
|
{
|
|
/// <summary>
|
|
/// 이 컴포넌트가 부착된 RectTransform의 크기가 변경될 때, 지정된 'target' RectTransform들의 크기를 동일하게 맞추는 역할을 합니다.
|
|
/// 주로 ContentSizeFitter와 함께 사용되어 동적으로 변하는 콘텐츠의 크기에 맞춰 배경 이미지 등의 크기를 조절할 때 유용합니다.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <b>UI 계층 구조 예시:</b>
|
|
/// <code>
|
|
/// - Background (RectTransform)
|
|
/// - Content (RectTransform, VerticalLayoutGroup, ContentSizeFitter, ContentSizeSetter)
|
|
/// - Item1 (Image)
|
|
/// - Item2 (Image)
|
|
/// - ...
|
|
/// </code>
|
|
/// 위와 같은 구조에서 'Content' GameObject에 이 스크립트를 추가하고,
|
|
/// 'target' 배열에 'Background'의 RectTransform을 할당하면,
|
|
/// 'Content'의 크기가 내부 아이템(Item1, Item2)에 의해 변경될 때마다 'Background'의 크기도 함께 조절됩니다.
|
|
/// </example>
|
|
[ExecuteAlways] // 에디터 모드에서도 스크립트가 실행되도록 하여, UI 변경을 실시간으로 확인할 수 있게 합니다.
|
|
[RequireComponent(typeof(RectTransform))] // 이 컴포넌트는 RectTransform이 필수적으로 필요함을 명시합니다.
|
|
public class ContentSizeSetter : UIBehaviour
|
|
{
|
|
// 성능 최적화를 위해 RectTransform 컴포넌트를 캐싱하는 변수입니다.
|
|
[System.NonSerialized]
|
|
private RectTransform m_Rect;
|
|
|
|
/// <summary>
|
|
/// 이 컴포넌트가 부착된 GameObject의 RectTransform에 대한 참조입니다.
|
|
/// 처음 접근할 때 GetComponent를 통해 초기화하고, 이후에는 캐시된 값을 사용합니다.
|
|
/// </summary>
|
|
private RectTransform rectTransform
|
|
{
|
|
get
|
|
{
|
|
if (m_Rect == null)
|
|
{
|
|
m_Rect = GetComponent<RectTransform>();
|
|
}
|
|
return m_Rect;
|
|
}
|
|
}
|
|
|
|
[Tooltip("넓이에 크기 조절 적용")]
|
|
[SerializeField]
|
|
public bool EnableWidth = true;
|
|
|
|
[Tooltip("높이에 크기 조절 적용")]
|
|
[SerializeField]
|
|
public bool EnableHeight = true;
|
|
|
|
/// <summary>
|
|
/// 크기를 조절할 때 추가할 여백(margin)입니다.
|
|
/// x, y 값을 설정하여 target의 너비와 높이에 각각 추가적인 공간을 줄 수 있습니다.
|
|
/// </summary>
|
|
[Tooltip("크기를 조절할 때 추가할 여백(margin)입니다.")]
|
|
[SerializeField]
|
|
public Vector2 margin;
|
|
|
|
/// <summary>
|
|
/// 이 컴포넌트의 크기에 맞춰 함께 크기가 조절될 RectTransform(들)의 배열입니다.
|
|
/// 인스펙터 창에서 크기를 동기화할 UI 요소들을 여기에 할당합니다.
|
|
/// </summary>
|
|
[Tooltip("이 컴포넌트의 크기에 맞춰 함께 크기가 조절될 RectTransform(들)의 배열입니다.")]
|
|
[SerializeField]
|
|
public RectTransform[] target;
|
|
|
|
// 중복 마킹을 방지하기 위한 정적 캐시
|
|
private static HashSet<RectTransform> s_MarkedTargets = new HashSet<RectTransform>();
|
|
private static int s_LastFrameMarked = -1;
|
|
private static Dictionary<RectTransform, float> s_LastMarkedTime = new Dictionary<RectTransform, float>();
|
|
|
|
// 예약 플래그
|
|
private bool m_PendingUpdate = false;
|
|
|
|
[SerializeField]
|
|
[Tooltip("같은 target에 대해 재마킹하기 전 최소 대기 시간(초).0이면 제한 없음.")]
|
|
private float minMarkInterval = 0f;
|
|
|
|
/// <summary>
|
|
/// 컴포넌트가 활성화될 때 호출되는 Unity 생명주기 함수입니다.
|
|
/// 레이아웃을 다시 계산하도록 시스템에 요청합니다.
|
|
/// </summary>
|
|
protected override void OnEnable()
|
|
{
|
|
// 활성화 시 즉시 반영이 필요할 수 있으므로 예약 대신 즉시 마킹
|
|
ScheduleUpdate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이 컴포넌트가 부착된 RectTransform의 크기나 앵커 등이 변경될 때 호출됩니다.
|
|
/// 이 스크립트의 핵심 로직으로, target 배열에 있는 모든 RectTransform의 크기를 현재 RectTransform의 크기에 맞게 업데이트합니다.
|
|
/// </summary>
|
|
protected override void OnRectTransformDimensionsChange()
|
|
{
|
|
base.OnRectTransformDimensionsChange();
|
|
|
|
// 변경이 발생하면 즉시 적용하지 않고 다음 프레임에 일괄 처리하도록 예약합니다.
|
|
ScheduleUpdate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 예약 처리: 다음 LateUpdate에서 적용
|
|
/// </summary>
|
|
private void ScheduleUpdate()
|
|
{
|
|
if (!IsActive())
|
|
return;
|
|
|
|
m_PendingUpdate = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// LateUpdate에서 예약된 변경을 한 번에 처리합니다.
|
|
/// 동일 프레임/짧은 시간 내 중복 레이아웃 마킹을 방지합니다.
|
|
/// </summary>
|
|
private void LateUpdate()
|
|
{
|
|
if (!m_PendingUpdate)
|
|
return;
|
|
|
|
// 프레임이 바뀌면 정적 캐시를 리셋
|
|
if (s_LastFrameMarked != Time.frameCount)
|
|
{
|
|
s_LastFrameMarked = Time.frameCount;
|
|
s_MarkedTargets.Clear();
|
|
}
|
|
|
|
if (rectTransform == null || target == null || target.Length == 0)
|
|
{
|
|
m_PendingUpdate = false;
|
|
return;
|
|
}
|
|
|
|
// 원본 크기 계산(여기서 sizeDelta는 변경하지 않음)
|
|
Vector2 desired = new Vector2(rectTransform.sizeDelta.x + margin.x, rectTransform.sizeDelta.y + margin.y);
|
|
|
|
for (int i = 0; i < target.Length; i++)
|
|
{
|
|
var t = target[i];
|
|
if (t == null)
|
|
continue;
|
|
|
|
// 계산된 최종 크기
|
|
float w = EnableWidth ? desired.x : t.sizeDelta.x;
|
|
float h = EnableHeight ? desired.y : t.sizeDelta.y;
|
|
|
|
// 필요할 경우에만 할당하여 변경 횟수 감소
|
|
if (t.sizeDelta.x != w || t.sizeDelta.y != h)
|
|
{
|
|
t.sizeDelta = new Vector2(w, h);
|
|
}
|
|
|
|
TryMarkTarget(t);
|
|
}
|
|
|
|
m_PendingUpdate = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 주어진 target을 마킹(및 시간 제한 검사)합니다.
|
|
/// </summary>
|
|
/// <param name="t">마킹 대상</param>
|
|
private void TryMarkTarget(RectTransform t)
|
|
{
|
|
if (t == null)
|
|
return;
|
|
|
|
// 이미 같은 프레임에 마킹했다면 중복 방지
|
|
if (s_MarkedTargets.Contains(t))
|
|
return;
|
|
|
|
// 최소 재마킹 간격 검사
|
|
if (minMarkInterval > 0f)
|
|
{
|
|
if (s_LastMarkedTime.TryGetValue(t, out float lastTime))
|
|
{
|
|
if (Time.unscaledTime - lastTime < minMarkInterval)
|
|
return;
|
|
}
|
|
}
|
|
|
|
s_MarkedTargets.Add(t);
|
|
s_LastMarkedTime[t] = Time.unscaledTime;
|
|
LayoutRebuilder.MarkLayoutForRebuild(t);
|
|
}
|
|
|
|
/// <summary>
|
|
/// UI 레이아웃을 다시 계산해야 함을 시스템에 알립니다.
|
|
/// </summary>
|
|
/// <param name="force">true일 경우, 즉시 레이아웃을 다시 계산합니다. false일 경우, 다음 프레임에 다시 계산하도록 예약합니다.</param>
|
|
protected void SetDirty(bool force = false)
|
|
{
|
|
if (IsActive() == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (force == true)
|
|
{
|
|
// 즉시 레이아웃을 강제로 다시 계산합니다.
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
|
|
}
|
|
else
|
|
{
|
|
// 다음 레이아웃 계산 주기에 맞춰 다시 계산하도록 표시만 해둡니다. (일반적인 경우)
|
|
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// (Unity 에디터 전용) 스크립트가 로드되거나 인스펙터에서 값이 변경될 때 호출됩니다.
|
|
/// 에디터에서 margin과 같은 값을 변경했을 때, 실시간으로 UI에 반영되도록 레이아웃 업데이트를 요청합니다.
|
|
/// </summary>
|
|
protected override void OnValidate()
|
|
{
|
|
SetDirty(false);
|
|
}
|
|
#endif
|
|
}
|
|
} |