Files
EnglewoodLAB/Assets/Scripts/NHN/InfiniteScroll.Scroll.cs

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();
}
}
}