Files
XRLib/Assets/Scripts/SHI/modal/HorizontalSplitDrag.cs
2025-11-18 18:14:53 +09:00

173 lines
8.0 KiB
C#

#nullable enable
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace SHI.modal
{
/// <summary>
/// 수평 레이아웃에서 두 패널의 가중치(LayoutElement.flexibleWidth)를 드래그 핸들로 조절하는 간단한 분할기입니다.
/// </summary>
public class HorizontalSplitDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[SerializeField, Range(0.0f, 0.5f)] private float minLeftWeight = 0.1f;
[SerializeField, Range(0.5f, 1.0f)] private float maxLeftWeight = 0.9f;
[SerializeField] private bool useActualWidthForHandle = true; // 실제 패널 폭 기준 위치 보정
private RectTransform _left;
private RectTransform _right;
private LayoutElement _leftLayout;
private LayoutElement _rightLayout;
private RectTransform _parent;
private RectTransform _handleRect; // this splitter's rect
private float _parentWidth; // cached on begin drag
private float _handleY = 42; // keep original y
private RectTransform _leftFixedPanel; // e.g., ModelDetailListView root
private bool _lastLeftPanelActive;
private float _lastParentWidth;
/// <summary>
/// 좌/우 패널을 지정하고(필요 시 좌측 고정 패널 포함) 분할기를 초기화합니다.
/// </summary>
public void Initialize(RectTransform left, RectTransform right, RectTransform? leftFixedPanel = null)
{
_left = left;
_right = right;
_leftFixedPanel = leftFixedPanel;
_parent = left != null ? left.parent as RectTransform : null;
_handleRect = transform as RectTransform;
_leftLayout = _left != null ? _left.GetComponent<LayoutElement>() : null;
if (_left != null && _leftLayout == null) _leftLayout = _left.gameObject.AddComponent<LayoutElement>();
_rightLayout = _right != null ? _right.GetComponent<LayoutElement>() : null;
if (_right != null && _rightLayout == null) _rightLayout = _right.gameObject.AddComponent<LayoutElement>();
_lastLeftPanelActive = _leftFixedPanel != null && _leftFixedPanel.gameObject.activeInHierarchy;
_lastParentWidth = _parent != null ? _parent.rect.width : 0f;
RefreshPosition();
}
/// <inheritdoc />
public void OnBeginDrag(PointerEventData eventData)
{
if (_parent != null) _parentWidth = _parent.rect.width;
if (_handleRect != null) _handleY = _handleRect.anchoredPosition.y;
}
/// <summary>
/// 부모 RectTransform pivot을 고려한 작업 영역 좌/우 경계 계산.
/// </summary>
private void GetWorkArea(out float minX, out float maxX, out float leftOffset)
{
minX = maxX = leftOffset = 0f;
if (_parent == null) return;
float width = _parent.rect.width;
var pivot = _parent.pivot; // (0..1)
float pivotOrigin = -width * pivot.x; // local 좌표계에서 좌측 경계
leftOffset = 0f;
if (_leftFixedPanel != null && _leftFixedPanel.gameObject.activeSelf)
leftOffset = _leftFixedPanel.rect.width; // 고정 패널 폭만큼 이동
minX = pivotOrigin + leftOffset;
maxX = pivotOrigin + width; // 우측 경계 (handle 중심 좌표)
}
/// <inheritdoc />
public void OnDrag(PointerEventData eventData)
{
if (_parent == null || _leftLayout == null || _rightLayout == null) return;
Vector2 local;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(_parent, eventData.position, eventData.pressEventCamera, out local))
return;
float width = _parent.rect.width;
if (width <= 0f) return;
float minX, maxX, leftOffset;
GetWorkArea(out minX, out maxX, out leftOffset);
// 드래그 포인터를 작업 영역으로 정규화하여 [0..1] 비율 t 산출
float t = Mathf.InverseLerp(minX, maxX, local.x);
t = Mathf.Clamp01(t);
// 가변 비율 계산 (minLeftWeight~maxLeftWeight 범위)
float leftWeight = Mathf.Lerp(minLeftWeight, maxLeftWeight, t);
float rightWeight = Mathf.Max(0.0001f, 1f - leftWeight);
_leftLayout.flexibleWidth = leftWeight;
_rightLayout.flexibleWidth = rightWeight;
// 레이아웃 즉시 반영 후 실제 폭 기준 위치 재계산
Canvas.ForceUpdateCanvases();
UpdateHandlePositionFromActualWidths(minX, maxX);
}
/// <summary>
/// 실제 패널 폭을 사용해 핸들 위치를 경계선(왼쪽 패널 오른쪽 끝)에 정렬.
/// 레이아웃 그룹 padding/spacing, 고정 패널 폭을 모두 반영.
/// </summary>
private void UpdateHandlePositionFromActualWidths(float minX, float maxX)
{
if (_handleRect == null || _parent == null || _left == null) return;
if (!useActualWidthForHandle)
{
// 기존 비율 방식 (보정 없이)
float totalFlex = Mathf.Max(0.0001f, _leftLayout.flexibleWidth + _rightLayout.flexibleWidth);
float leftWeight = Mathf.Clamp01(_leftLayout.flexibleWidth / totalFlex);
float normalized = Mathf.InverseLerp(minLeftWeight, maxLeftWeight, Mathf.Clamp(leftWeight, minLeftWeight, maxLeftWeight));
float xRatio = Mathf.Lerp(minX, maxX, normalized);
_handleRect.anchoredPosition = new Vector2(xRatio, _handleY);
return;
}
// 고정 패널 폭 + 가변 왼쪽 패널 실제 폭 = 경계 픽셀
float fixedWidth = (_leftFixedPanel != null && _leftFixedPanel.gameObject.activeSelf) ? _leftFixedPanel.rect.width : 0f;
float variableLeftWidth = _left.rect.width; // 레이아웃 적용 후 실제 계산된 폭
// 부모 pivot 기반 local 좌표 좌측 경계
float widthParent = _parent.rect.width;
float origin = -widthParent * _parent.pivot.x;
float boundaryX = origin + fixedWidth + variableLeftWidth;
// 핸들 자체 폭 중앙 정렬 (핸들 pivot이 중앙이라고 가정)
// 경계 근처에서 과도한 이동 방지 (clamp)
float halfHandle = _handleRect.rect.width * 0.5f;
float minCenter = origin + fixedWidth + halfHandle; // 최소 중앙: 고정 패널 끝 + 반 핸들
float maxCenter = origin + widthParent - halfHandle; // 최대 중앙: 부모 우측 - 반 핸들
float centerX = Mathf.Clamp(boundaryX, minCenter, maxCenter);
_handleRect.anchoredPosition = new Vector2(centerX, _handleY);
}
/// <summary>
/// 현재 레이아웃 상태에 맞게 드래그 핸들의 위치를 동기화합니다.
/// </summary>
public void RefreshPosition()
{
if (_parent == null || _handleRect == null || _leftLayout == null || _rightLayout == null) return;
float width = _parent.rect.width;
if (width <= 0f) return;
float minX, maxX, leftOffset;
GetWorkArea(out minX, out maxX, out leftOffset);
Canvas.ForceUpdateCanvases();
UpdateHandlePositionFromActualWidths(minX, maxX);
}
private void LateUpdate()
{
bool nowActive = _leftFixedPanel != null && _leftFixedPanel.gameObject.activeInHierarchy;
float nowWidth = _parent != null ? _parent.rect.width : 0f;
if (nowActive != _lastLeftPanelActive || !Mathf.Approximately(nowWidth, _lastParentWidth))
{
_lastLeftPanelActive = nowActive;
_lastParentWidth = nowWidth;
RefreshPosition();
}
}
/// <inheritdoc />
public void OnEndDrag(PointerEventData eventData) { }
}
}