2025/Unity

[Unity] UI - 아코디언 메뉴 만드는 방법 <펼치고 접기>

rimugiri 2025. 3. 1. 05:13
728x90

접기/펼치기 토글로 내용이 스르륵 나타났다 사라지는 메뉴, 흔히 '아코디언 메뉴'라고 불리는 UI를 직접 구현해봤습니다.
굳이 에셋을 사거나 별도 트윈 시스템을 구현하는 게 귀찮아서, 그냥 간단하게 구성했습니다.

아래는 제가 실제로 구현한 과정과 팁을 정리한 내용입니다.


1. UI 세팅

아코디언 메뉴는 다음처럼 3단계로 구성됩니다.

Panel
   └ Item
      ├ Header
      └ Content

① Panel

Panel은 전체적인 레이아웃을 관리하는 부모 오브젝트입니다.

  • Vertical Layout Group : 아이템들이 세로로 정렬되도록 설정
  • Content Size Fitter : 내용물에 따라 자동으로 크기 조정

⚠️ Control Child Size 옵션을 켜서 Content의 높이가 제대로 반영되게 만들어야 합니다.


② Item

각각의 접기/펼치기 단위가 되는 개별 아이템입니다.

  • Vertical Layout Group : Header와 Content의 배치 및 크기 균형 유지
  • Layout Element : LayoutGroup을 쓴다면, 이걸 통해 사이즈 제어하는 게 중요 (안 그러면 예상치 못한 문제 발생 가능)

✅ Use Child Scale 체크해서, 자식 오브젝트들의 Preferred 크기들이 자동 반영되도록 설정합니다.


③ Header & Content

구성설명

Header 접기/펼치기 버튼이 들어가는 영역. RectTransform Height로 고정 크기 설정
Content 펼쳐질 내용 영역. LayoutElement의 Preferred Height를 원하는 크기로 설정 (RectTransform도 동기화 필요)

⚠️ Content의 높이 자동화 방법도 있지만, 이게 최적화 측면에서 좋은지는 고민해볼 부분입니다.


2. 코드 구성

① AccordionPanel (Panel에 부착)

  • 추후 아이템 동적 생성 관리까지 고려해서 만든 매니저 역할 클래스
  • 기본 기능: 전체 접기/펼치기, 속도 조절, 애니메이션 방식 설정 등
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(LayoutGroup))]
public class AccordionPanel : MonoBehaviour
{
    [SerializeField] private List<AccordionItem> items = new();
    [field : SerializeField] public float EaseDuration { get; private set; } = 0.3f;
    [field: SerializeField] public DG.Tweening.Ease EaseOut { get; private set; } = DG.Tweening.Ease.OutCubic;
    [field: SerializeField] public DG.Tweening.Ease EaseIn { get; private set; } = DG.Tweening.Ease.InCubic;

    private void Start()
    {
        FindAllItems();

        foreach (var item in items)
        {
            item.Initialize(this);
        }
    }

    private void FindAllItems()
    {
        items.Clear();
        foreach (Transform child in transform)
        {
            var item = child.GetComponent<AccordionItem>();
            if (item != null)
            {
                items.Add(item);
            }
        }
    }

    
}

② AccordionItem (각 Item에 부착)

  • 실제 접기/펼치기 기능을 담당
  • 크기 변경 후 LayoutRebuilder.ForceRebuildLayoutImmediate() 를 호출해 레이아웃 갱신을 강제합니다.

⚠️ 강제 갱신은 편하지만, 성능상 어떤 영향이 있는지는 더 테스트해봐야 합니다.

using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

[RequireComponent(typeof(LayoutElement))]
public class AccordionItem : MonoBehaviour
{
    [SerializeField] private RectTransform _content;
    [SerializeField] private RectTransform _header;
    [SerializeField] private Button _toggleButton;

    private LayoutElement _layoutElement;
    private AccordionPanel _accordionPanel;

    private bool _isExpanded = false;

    private float _contentPreferredHeight = 0f;
    private float _headerHeight = 0f; 

    public VerticalLayoutGroup verticalLayoutGroup;
    public void Initialize(AccordionPanel parent)
    {
        _accordionPanel = parent;

        _layoutElement = GetComponent<LayoutElement>();
        if (_content == null)
        {
            Debug.LogError($"{name}: _content is not assigned!");
        }
        if (_toggleButton == null)
        {
            Debug.LogError($"{name}: _toggleButton is not assigned!");
        }
        if(_header == null)
        {
            Debug.LogError($"{name}: _header is not assigned!");
        }

        LayoutRebuilder.ForceRebuildLayoutImmediate(_content);

        _contentPreferredHeight = LayoutUtility.GetPreferredHeight(_content);

        _headerHeight = _header.sizeDelta.y;

        _toggleButton.onClick.AddListener(Toggle);
        CollapseImmediate();  // 초기에 content들을 모두 닫아둔다.
    }

    private void Toggle()
    {
        if (_isExpanded)
        {
            Collapse();
        }
        else
        {
            Expand();
        }
    }

    public void Expand()
    {
        _content.DOScaleY(1, 0.3f).SetEase(Ease.OutCubic);

        float targetHeight = _contentPreferredHeight + _headerHeight;
        _layoutElement.DOPreferredSize(new Vector2(_layoutElement.preferredWidth, targetHeight), _accordionPanel.EaseDuration).SetEase(_accordionPanel.EaseOut);

        _isExpanded = true;
    }

    public void Collapse()
    {
        _content.DOScaleY(0, 0.3f).SetEase(Ease.InCubic);

        float targetHeight = _headerHeight;
        
        _layoutElement.DOPreferredSize(new Vector2(_layoutElement.preferredWidth, targetHeight), _accordionPanel.EaseDuration).SetEase(_accordionPanel.EaseIn);

        _isExpanded = false;
    }

    public void CollapseImmediate()
    {
        _content.localScale = new Vector3(1, 0, 1);
        _layoutElement.preferredHeight = _headerHeight;
        _isExpanded = false;
    }
}

마무리

 

 

Dotween 무료버전으로 충분히 구현가능하지만 코루틴으로 이를 충분히 대체할수 있으니 다들 좋은 글이 되었기를..

 

피드벡은 언제나 환영입니다.

728x90