574 lines
21 KiB
C#
574 lines
21 KiB
C#
namespace Gpm.Ui
|
|
{
|
|
using System;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using UnityEngine.UI;
|
|
|
|
// 이 partial 클래스는 InfiniteScroll의 스크롤 이동 관련 로직을 담당합니다.
|
|
// 특정 아이템으로 이동하거나, 스크롤 위치를 직접 제어하는 기능을 구현합니다.
|
|
public partial class InfiniteScroll : IMoveScroll
|
|
{
|
|
/// <summary>
|
|
/// 특정 아이템으로 이동할 때, 뷰포트 내에서 해당 아이템을 정렬할 위치를 지정합니다.
|
|
/// </summary>
|
|
public enum MoveToType
|
|
{
|
|
/// <summary>
|
|
/// 아이템을 뷰포트의 상단(Vertical) 또는 왼쪽(Horizontal)에 맞춥니다.
|
|
/// </summary>
|
|
MOVE_TO_TOP = 0,
|
|
|
|
/// <summary>
|
|
/// 아이템을 뷰포트의 중앙에 맞춥니다.
|
|
/// </summary>
|
|
MOVE_TO_CENTER,
|
|
|
|
/// <summary>
|
|
/// 아이템을 뷰포트의 하단(Vertical) 또는 오른쪽(Horizontal)에 맞춥니다.
|
|
/// </summary>
|
|
MOVE_TO_BOTTOM
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스크롤 이동 애니메이션을 제어하는 정적 클래스입니다.
|
|
/// ScrollMoveTo 컴포넌트를 사용하여 부드러운 스크롤 효과를 구현합니다.
|
|
/// </summary>
|
|
public static class Control
|
|
{
|
|
/// <summary>
|
|
/// 지정된 아이템 인덱스로 일정 시간 동안 부드럽게 스크롤합니다.
|
|
/// </summary>
|
|
/// <param name="scroll">대상 InfiniteScroll 인스턴스</param>
|
|
/// <param name="itemIndex">이동할 아이템의 인덱스</param>
|
|
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
|
|
/// <param name="time">애니메이션 시간 (0이면 즉시 이동)</param>
|
|
public static void MoveTo(InfiniteScroll scroll, int itemIndex, MoveToType moveToType, float time = 0)
|
|
{
|
|
// ScrollMoveTo 컴포넌트가 없으면 동적으로 추가합니다.
|
|
ScrollMoveTo moveto = scroll.gameObject.GetComponent<ScrollMoveTo>();
|
|
if (moveto == null)
|
|
{
|
|
moveto = scroll.gameObject.AddComponent<ScrollMoveTo>();
|
|
}
|
|
|
|
// 이동 목표 설정
|
|
moveto.Set(itemIndex, moveToType, time);
|
|
// 애니메이션 커브 설정 (부드러운 시작과 끝)
|
|
moveto.curve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
|
|
|
// 이동 완료 후 컴포넌트 자동 파괴 설정
|
|
moveto.autoDestory = true;
|
|
|
|
// 애니메이션 재생
|
|
moveto.Play();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 스크롤 비율(0.0 ~ 1.0)로 일정 시간 동안 부드럽게 스크롤합니다.
|
|
/// </summary>
|
|
/// <param name="scroll">대상 InfiniteScroll 인스턴스</param>
|
|
/// <param name="scrollRate">이동할 스크롤 비율 (0.0 = 시작, 1.0 = 끝)</param>
|
|
/// <param name="time">애니메이션 시간 (0이면 즉시 이동)</param>
|
|
public static void MoveTo(InfiniteScroll scroll, float scrollRate, float time = 0)
|
|
{
|
|
ScrollMoveTo moveto = scroll.gameObject.GetComponent<ScrollMoveTo>();
|
|
if (moveto == null)
|
|
{
|
|
moveto = scroll.gameObject.AddComponent<ScrollMoveTo>();
|
|
}
|
|
|
|
moveto.Set(scrollRate, time);
|
|
moveto.curve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
|
|
|
moveto.autoDestory = true;
|
|
|
|
moveto.Play();
|
|
}
|
|
}
|
|
|
|
// 스크롤 위치 비교 시 오차를 보정하기 위한 값입니다. (콘텐츠 크기의 0.01%)
|
|
// 부동 소수점 연산의 정밀도 문제로 인해 정확히 0 또는 1이 되지 않는 경우를 처리합니다.
|
|
private const float UPDOWN_MULTIPLY = 2.0f;
|
|
private const int NEEDITEM_MORE_LINE = 1;
|
|
private const int NEEDITEM_EXTRA_ADD = 2;
|
|
|
|
protected ScrollRect scrollRect = null;
|
|
protected RectTransform viewport = null;
|
|
|
|
protected float sizeInterpolationValue = 0.0001f; // 0.01%
|
|
|
|
/// <summary>
|
|
/// 특정 데이터(InfiniteScrollData)에 해당하는 아이템으로 스크롤을 이동합니다.
|
|
/// </summary>
|
|
/// <param name="data">이동할 대상 데이터</param>
|
|
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
|
|
/// <param name="time">이동에 걸리는 시간 (0이면 즉시 이동)</param>
|
|
/// <example>
|
|
/// <code>
|
|
/// InfiniteScrollData targetData = myScroll.GetData(10);
|
|
/// if (targetData != null)
|
|
/// {
|
|
/// myScroll.MoveTo(targetData, InfiniteScroll.MoveToType.MOVE_TO_CENTER, 0.5f);
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public void MoveTo(InfiniteScrollData data, MoveToType moveToType, float time = 0)
|
|
{
|
|
MoveTo(GetItemIndex(data), moveToType, time);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 화면에 보이는 아이템 목록 기준의 인덱스로 스크롤을 이동합니다.
|
|
/// </summary>
|
|
/// <param name="itemIndex">이동할 아이템의 인덱스 (화면에 보이는 아이템 기준)</param>
|
|
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
|
|
/// <param name="time">이동에 걸리는 시간 (0이면 즉시 이동)</param>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 50번째 아이템으로 0.3초 동안 스크롤 (아이템이 뷰포트 상단에 오도록)
|
|
/// myScroll.MoveTo(50, InfiniteScroll.MoveToType.MOVE_TO_TOP, 0.3f);
|
|
/// </code>
|
|
/// </example>
|
|
public void MoveTo(int itemIndex, MoveToType moveToType, float time = 0)
|
|
{
|
|
if (isInitialize == false)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
if (IsValidItemIndex(itemIndex) == true)
|
|
{
|
|
if (time > 0)
|
|
{
|
|
// 시간이 지정된 경우 Control 클래스를 통해 부드럽게 이동
|
|
Control.MoveTo(this, itemIndex, moveToType, time);
|
|
}
|
|
else
|
|
{
|
|
// 즉시 이동
|
|
SetScrollPosition(GetMovePosition(itemIndex, moveToType));
|
|
Control.MoveTo(this, itemIndex, moveToType, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 전체 데이터 목록 기준의 인덱스로 스크롤을 이동합니다.
|
|
/// 필터링이 적용된 상태에서, 필터링과 상관없이 원본 데이터의 인덱스를 사용하고 싶을 때 유용합니다.
|
|
/// </summary>
|
|
/// <param name="dataIndex">이동할 데이터의 인덱스 (전체 데이터 리스트 기준)</param>
|
|
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
|
|
/// <param name="time">이동에 걸리는 시간 (0이면 즉시 이동)</param>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 원본 데이터 리스트의 100번째 데이터로 즉시 이동
|
|
/// myScroll.MoveToFromDataIndex(100, InfiniteScroll.MoveToType.MOVE_TO_CENTER);
|
|
/// </code>
|
|
/// </example>
|
|
public void MoveToFromDataIndex(int dataIndex, MoveToType moveToType, float time = 0)
|
|
{
|
|
if (isInitialize == false)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
if (IsValidDataIndex(dataIndex) == true)
|
|
{
|
|
MoveTo(dataList[dataIndex].itemIndex, moveToType, time);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 전체 스크롤 범위의 특정 비율(0.0 ~ 1.0)로 스크롤을 이동합니다.
|
|
/// </summary>
|
|
/// <param name="scrollRate">이동할 비율 (0.0 = 시작, 0.5 = 중간, 1.0 = 끝)</param>
|
|
/// <param name="time">이동에 걸리는 시간 (0이면 즉시 이동)</param>
|
|
/// <example>
|
|
/// <code>
|
|
/// // 스크롤을 80% 위치로 1초 동안 이동
|
|
/// myScroll.MoveTo(0.8f, 1.0f);
|
|
/// </code>
|
|
/// </example>
|
|
public void MoveTo(float scrollRate, float time = 0)
|
|
{
|
|
if (isInitialize == false)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
if (time > 0)
|
|
{
|
|
Control.MoveTo(this, scrollRate, time);
|
|
}
|
|
else
|
|
{
|
|
SetScrollPosition(GetMovePosition(scrollRate));
|
|
Control.MoveTo(this, scrollRate, 0); // 위치 보정을 위해 호출
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스크롤을 가장 처음 위치로 이동합니다.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <code>
|
|
/// // '맨 위로' 버튼 클릭 시 호출
|
|
/// public void OnClick_MoveToTop()
|
|
/// {
|
|
/// myScroll.MoveToFirstData();
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public void MoveToFirstData()
|
|
{
|
|
if (isInitialize == false)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
UpdateContentSize();
|
|
|
|
Vector2 normalizedPosition;
|
|
if (layout.IsVertical() == true)
|
|
{
|
|
normalizedPosition = Vector2.one; // 세로 스크롤의 시작은 (0, 1)
|
|
}
|
|
else
|
|
{
|
|
normalizedPosition = Vector2.zero; // 가로 스크롤의 시작은 (0, 0)
|
|
}
|
|
|
|
if (scrollRect.normalizedPosition != normalizedPosition)
|
|
{
|
|
scrollRect.normalizedPosition = normalizedPosition;
|
|
isUpdateArea = true;
|
|
}
|
|
|
|
MoveTo(0); // 0% 위치로 이동
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스크롤을 가장 마지막 위치로 이동합니다.
|
|
/// </summary>
|
|
/// <example>
|
|
/// <code>
|
|
/// // '맨 아래로' 버튼 클릭 시 호출
|
|
/// public void OnClick_MoveToBottom()
|
|
/// {
|
|
/// myScroll.MoveToLastData();
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public void MoveToLastData()
|
|
{
|
|
if (isInitialize == false)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
UpdateContentSize();
|
|
|
|
Vector2 normalizedPosition;
|
|
if (layout.IsVertical() == true)
|
|
{
|
|
normalizedPosition = Vector2.zero; // 세로 스크롤의 끝은 (0, 0)
|
|
}
|
|
else
|
|
{
|
|
normalizedPosition = Vector2.one; // 가로 스크롤의 끝은 (1, 0)
|
|
}
|
|
if (scrollRect.normalizedPosition != normalizedPosition)
|
|
{
|
|
scrollRect.normalizedPosition = normalizedPosition;
|
|
isUpdateArea = true;
|
|
}
|
|
|
|
MoveTo(1); // 100% 위치로 이동
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스크롤이 가장 처음에 도달했는지 확인합니다.
|
|
/// </summary>
|
|
/// <returns>처음 위치에 있으면 true, 아니면 false를 반환합니다.</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// if (myScroll.IsMoveToFirstData())
|
|
/// {
|
|
/// Debug.Log("스크롤이 맨 위에 있습니다.");
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public bool IsMoveToFirstData()
|
|
{
|
|
if (isInitialize == false)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
if (NeedUpdateItem() == true)
|
|
{
|
|
UpdateShowItem(false);
|
|
}
|
|
|
|
float contentPosition = GetContentPosition();
|
|
float viewportSize = GetViewportSize();
|
|
float contentSize = GetContentSize();
|
|
|
|
return IsMoveToFirstData(contentPosition, viewportSize, contentSize);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스크롤이 가장 마지막에 도달했는지 확인합니다.
|
|
/// </summary>
|
|
/// <returns>마지막 위치에 있으면 true, 아니면 false를 반환합니다.</returns>
|
|
/// <example>
|
|
/// <code>
|
|
/// if (myScroll.IsMoveToLastData())
|
|
/// {
|
|
/// Debug.Log("스크롤이 맨 아래에 있습니다.");
|
|
/// // 추가 데이터 로딩 로직 호출 가능
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public bool IsMoveToLastData()
|
|
{
|
|
if (isInitialize == false)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
float contentPosition = GetContentPosition();
|
|
float viewportSize = GetViewportSize();
|
|
float contentSize = GetContentSize();
|
|
|
|
return IsMoveToLastData(contentPosition, viewportSize, contentSize);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 스크롤 비율에 해당하는 content의 anchoredPosition을 계산합니다.
|
|
/// </summary>
|
|
/// <param name="scrollRate">계산할 스크롤 비율 (0.0 ~ 1.0)</param>
|
|
/// <returns>계산된 스크롤 위치(Vector2)</returns>
|
|
public Vector2 GetMovePosition(float scrollRate)
|
|
{
|
|
float viewportSize = GetViewportSize();
|
|
float contentSize = GetContentSize();
|
|
|
|
// 전체 스크롤 가능 범위에서 비율만큼의 위치를 계산
|
|
float move = (contentSize - viewportSize) * Mathf.Clamp01(scrollRate);
|
|
move = Math.Max(0.0f, move);
|
|
|
|
move = ItemPostionFromOffset(move);
|
|
|
|
return layout.GetAxisVector(content.anchoredPosition, move);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 아이템 인덱스로 이동하기 위한 content의 anchoredPosition을 계산합니다.
|
|
/// </summary>
|
|
/// <param name="itemIndex">대상 아이템의 인덱스</param>
|
|
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
|
|
/// <returns>계산된 스크롤 위치(Vector2)</returns>
|
|
public Vector2 GetMovePosition(int itemIndex, MoveToType moveToType)
|
|
{
|
|
float move = GetMoveOffset(itemIndex, moveToType);
|
|
|
|
return layout.GetAxisVector(content.anchoredPosition, move);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 스크롤 content의 anchoredPosition을 가져옵니다.
|
|
/// </summary>
|
|
/// <returns>현재 스크롤 위치(Vector2)</returns>
|
|
public Vector2 GetScrollPosition()
|
|
{
|
|
return content.anchoredPosition;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스크롤 content의 anchoredPosition을 지정된 위치로 설정합니다.
|
|
/// 이 메서드는 스크롤 위치를 즉시 변경합니다.
|
|
/// </summary>
|
|
/// <param name="position">설정할 위치(Vector2)</param>
|
|
public void SetScrollPosition(Vector2 position)
|
|
{
|
|
content.anchoredPosition = position;
|
|
|
|
float contentPosition = GetContentPosition();
|
|
if (cachedData.contentPosition != contentPosition)
|
|
{
|
|
cachedData.contentPosition = contentPosition;
|
|
isUpdateArea = true; // 위치가 변경되었으므로 업데이트 필요 플래그 설정
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스크롤의 주 축(Vertical이면 Y, Horizontal이면 X) 위치를 설정합니다.
|
|
/// </summary>
|
|
/// <param name="movePosition">설정할 주 축의 위치 값</param>
|
|
public void SetScrollPosition(float movePosition)
|
|
{
|
|
Vector2 prevPosition = GetScrollPosition();
|
|
|
|
SetScrollPosition(layout.GetAxisVector(prevPosition, movePosition));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스크롤 content의 위치와 크기를 초기화합니다.
|
|
/// 데이터는 그대로 유지됩니다.
|
|
/// </summary>
|
|
public void ClearScrollContent()
|
|
{
|
|
content.anchoredPosition = Vector2.zero;
|
|
content.sizeDelta = Vector2.zero;
|
|
|
|
cachedData.Clear();
|
|
|
|
CheckScrollData();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 내부 ScrollRect의 OnValueChanged 이벤트에 리스너를 추가합니다.
|
|
/// 사용자가 스크롤을 움직일 때마다 콜백을 받고 싶을 때 사용합니다.
|
|
/// </summary>
|
|
/// <param name="listener">추가할 리스너(콜백 함수)</param>
|
|
/// <example>
|
|
/// <code>
|
|
/// void Start()
|
|
/// {
|
|
/// myScroll.AddScrollValueChangedLisnter(OnScrollMoved);
|
|
/// }
|
|
///
|
|
/// void OnScrollMoved(Vector2 position)
|
|
/// {
|
|
/// // position은 0.0 ~ 1.0 사이의 정규화된 값입니다.
|
|
/// Debug.Log($"스크롤 위치 변경: {position}");
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public void AddScrollValueChangedLisnter(UnityAction<Vector2> listener)
|
|
{
|
|
if (isInitialize == false)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
scrollRect.onValueChanged.AddListener(listener);
|
|
}
|
|
|
|
// 스크롤이 시작 지점에 있는지 내부적으로 계산하는 함수
|
|
private bool IsMoveToFirstData(float position, float viewportSize, float contentSize)
|
|
{
|
|
bool isShow = false;
|
|
|
|
// 콘텐츠 크기가 뷰포트보다 작으면 항상 시작/끝에 있는 것으로 간주
|
|
if (viewportSize > contentSize)
|
|
{
|
|
isShow = true;
|
|
}
|
|
else
|
|
{
|
|
// 부동 소수점 오차를 감안하여 비교
|
|
float interpolation = contentSize * sizeInterpolationValue;
|
|
if (-position > -interpolation)
|
|
{
|
|
isShow = true;
|
|
}
|
|
}
|
|
|
|
return isShow;
|
|
}
|
|
|
|
// 스크롤이 끝 지점에 있는지 내부적으로 계산하는 함수
|
|
private bool IsMoveToLastData(float position, float viewportSize, float contentSize)
|
|
{
|
|
bool isShow = false;
|
|
|
|
if (viewportSize > contentSize)
|
|
{
|
|
isShow = true;
|
|
}
|
|
else
|
|
{
|
|
float interpolation = contentSize * sizeInterpolationValue;
|
|
|
|
// 끝 지점 계산 (뷰포트 크기 + 현재 위치 - 콘텐츠 크기)
|
|
if (-(viewportSize + position - contentSize) <= interpolation)
|
|
{
|
|
isShow = true;
|
|
}
|
|
}
|
|
|
|
return isShow;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특정 아이템으로 이동하기 위한 스크롤 content의 오프셋(offset)을 계산합니다.
|
|
/// </summary>
|
|
/// <param name="itemIndex">대상 아이템 인덱스</param>
|
|
/// <param name="moveToType">뷰포트 내 정렬 방식</param>
|
|
/// <returns>계산된 오프셋 값</returns>
|
|
protected float GetMoveOffset(int itemIndex, MoveToType moveToType)
|
|
{
|
|
float viewportSize = GetViewportSize();
|
|
float contentSize = GetContentSize();
|
|
|
|
float move = 0.0f;
|
|
float itemSize = GetItemSize(itemIndex);
|
|
float distance = GetItemDistance(itemIndex); // 아이템의 시작 위치
|
|
|
|
move = distance;
|
|
|
|
// 정렬 방식에 따라 오프셋 조정
|
|
switch (moveToType)
|
|
{
|
|
case MoveToType.MOVE_TO_CENTER:
|
|
{
|
|
// (뷰포트 중앙) - (아이템 중앙)
|
|
move -= viewportSize * 0.5f - itemSize * 0.5f;
|
|
break;
|
|
}
|
|
case MoveToType.MOVE_TO_BOTTOM:
|
|
{
|
|
// (뷰포트 하단) - (아이템 하단)
|
|
move -= viewportSize - itemSize;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 계산된 위치가 스크롤 가능 범위를 벗어나지 않도록 제한
|
|
move = Mathf.Clamp(move, 0.0f, contentSize - viewportSize);
|
|
move = Math.Max(0.0f, move);
|
|
|
|
return ItemPostionFromOffset(move);
|
|
}
|
|
|
|
// ScrollRect의 OnValueChanged 이벤트에 연결되어 호출되는 메서드
|
|
private void OnValueChanged(Vector2 value)
|
|
{
|
|
// 스크롤이 시작/끝 지점에 도달했는지 확인하고 이벤트를 발생시킵니다.
|
|
bool prevIsStartLine = isStartLine;
|
|
isStartLine = IsMoveToFirstData();
|
|
if (prevIsStartLine != isStartLine)
|
|
{
|
|
onStartLine.Invoke(isStartLine);
|
|
|
|
changeValue = true;
|
|
}
|
|
|
|
bool prevIsEndLine = isEndLine;
|
|
isEndLine = IsMoveToLastData();
|
|
if (prevIsEndLine != isEndLine)
|
|
{
|
|
onEndLine.Invoke(isEndLine);
|
|
|
|
changeValue = true;
|
|
}
|
|
|
|
// 스크롤이 움직였으므로 화면에 보이는 아이템을 갱신합니다.
|
|
UpdateShowItem();
|
|
}
|
|
}
|
|
}
|