using System; using UnityEngine; using UnityEngine.UI; using UnityEngine.Events; namespace Gpm.Ui { /// /// 무한 스크롤 기능을 구현하는 클래스입니다. /// 이 클래스는 UI 항목의 동적 로딩 및 재활용을 처리하여 성능을 최적화합니다. /// public partial class InfiniteScroll : MonoBehaviour { // 초기화 여부를 확인하는 플래그입니다. protected bool isInitialize = false; // 스크롤 뷰의 콘텐츠 RectTransform에 대한 참조입니다. protected RectTransform content = null; // 스크롤 값이 변경되었는지 여부를 나타내는 플래그입니다. private bool changeValue = false; [Header("Event", order = 4)] /// /// 스크롤 값이 변경될 때 발생하는 이벤트입니다. /// 매개변수: int (첫 번째 보이는 아이템 인덱스), int (마지막 보이는 아이템 인덱스), bool (시작 라인 여부), bool (끝 라인 여부) /// /// /// /// infiniteScroll.onChangeValue.AddListener((first, last, isStart, isEnd) => { /// Debug.Log($"First visible item: {first}, Last visible item: {last}, Is at start: {isStart}, Is at end: {isEnd}"); /// }); /// /// public ChangeValueEvent onChangeValue = new ChangeValueEvent(); /// /// 아이템의 활성 상태가 변경될 때 발생하는 이벤트입니다. /// 매개변수: int (데이터 인덱스), bool (활성 상태) /// /// /// /// infiniteScroll.onChangeActiveItem.AddListener((dataIndex, isActive) => { /// Debug.Log($"Item {dataIndex} is now {(isActive ? "active" : "inactive")}"); /// }); /// /// public ItemActiveEvent onChangeActiveItem = new ItemActiveEvent(); /// /// 스크롤이 시작 지점에 도달했을 때 발생하는 이벤트입니다. /// 매개변수: bool (시작 라인 여부) /// /// /// /// infiniteScroll.onStartLine.AddListener((isAtStart) => { /// if (isAtStart) { /// Debug.Log("Scrolled to the beginning."); /// } /// }); /// /// public StateChangeEvent onStartLine = new StateChangeEvent(); /// /// 스크롤이 끝 지점에 도달했을 때 발생하는 이벤트입니다. /// 매개변수: bool (끝 라인 여부) /// /// /// /// infiniteScroll.onEndLine.AddListener((isAtEnd) => { /// if (isAtEnd) { /// Debug.Log("Scrolled to the end."); /// } /// }); /// /// public StateChangeEvent onEndLine = new StateChangeEvent(); // 데이터 필터링에 사용되는 델리게이트입니다. private Predicate onFilter = null; private void Awake() { Initialize(); } /// /// InfiniteScroll을 초기화합니다. /// ScrollRect 컴포넌트를 가져오고, 이벤트를 설정하며, 초기 아이템을 생성합니다. /// protected void Initialize() { if (isInitialize == false) { scrollRect = GetComponent(); content = scrollRect.content; viewport = scrollRect.viewport; CheckScrollAxis(); ClearScrollContent(); RectTransform itemTransform = (RectTransform)itemPrefab.transform; defaultItemPrefabSize = itemTransform.sizeDelta; itemObjectList.Clear(); dataList.Clear(); scrollRect.onValueChanged.AddListener(OnValueChanged); CreateNeedItem(); CheckScrollData(); isInitialize = true; needReBuildLayout = true; } } /// /// 스크롤에 단일 데이터 항목을 삽입합니다. /// /// 삽입할 데이터입니다. /// true이면 즉시 레이아웃을 업데이트합니다. /// /// /// InfiniteScrollData myData = new InfiniteScrollData(); /// infiniteScroll.InsertData(myData, true); /// /// public void InsertData(InfiniteScrollData data, bool immediately = false) { if (isInitialize == false) { Initialize(); } AddData(data); UpdateAllData(immediately); } /// /// 지정된 인덱스에 단일 데이터 항목을 삽입합니다. /// /// 삽입할 데이터입니다. /// 데이터를 삽입할 인덱스입니다. /// true이면 즉시 레이아웃을 업데이트합니다. /// /// /// InfiniteScrollData myData = new InfiniteScrollData(); /// infiniteScroll.InsertData(myData, 0, true); /// /// public void InsertData(InfiniteScrollData data, int insertIndex, bool immediately = false) { if (insertIndex < 0 || insertIndex > dataList.Count) { throw new ArgumentOutOfRangeException(); } if (isInitialize == false) { Initialize(); } InsertData(data, insertIndex); UpdateAllData(immediately); } /// /// 스크롤에 여러 데이터 항목을 삽입합니다. /// /// 삽입할 데이터 배열입니다. /// true이면 즉시 레이아웃을 업데이트합니다. /// /// /// InfiniteScrollData[] myDataArray = new InfiniteScrollData[10]; /// // myDataArray 초기화 /// infiniteScroll.InsertData(myDataArray, true); /// /// public void InsertData(InfiniteScrollData[] datas, bool immediately = false) { if (isInitialize == false) { Initialize(); } foreach (InfiniteScrollData data in datas) { AddData(data); } UpdateAllData(immediately); } /// /// 지정된 인덱스부터 여러 데이터 항목을 삽입합니다. /// /// 삽입할 데이터 배열입니다. /// 데이터 삽입을 시작할 인덱스입니다. /// true이면 즉시 레이아웃을 업데이트합니다. /// /// /// InfiniteScrollData[] myDataArray = new InfiniteScrollData[10]; /// // myDataArray 초기화 /// infiniteScroll.InsertData(myDataArray, 5, true); /// /// public void InsertData(InfiniteScrollData[] datas, int insertIndex, bool immediately = false) { if (insertIndex < 0 || insertIndex > dataList.Count) { throw new ArgumentOutOfRangeException(); } if (isInitialize == false) { Initialize(); } foreach (InfiniteScrollData data in datas) { InsertData(data, insertIndex++); } UpdateAllData(immediately); } /// /// 지정된 데이터 항목을 제거합니다. /// /// 제거할 데이터입니다. /// true이면 즉시 레이아웃을 업데이트합니다. /// /// /// InfiniteScrollData dataToRemove = infiniteScroll.GetData(0); /// if (dataToRemove != null) { /// infiniteScroll.RemoveData(dataToRemove, true); /// } /// /// public void RemoveData(InfiniteScrollData data, bool immediately = false) { if (isInitialize == false) { Initialize(); } int dataIndex = GetDataIndex(data); RemoveData(dataIndex, immediately); } /// /// 지정된 인덱스의 데이터 항목을 제거합니다. /// /// 제거할 데이터의 인덱스입니다. /// true이면 즉시 레이아웃을 업데이트합니다. /// /// /// infiniteScroll.RemoveData(0, true); /// /// public void RemoveData(int dataIndex, bool immediately = false) { if (isInitialize == false) { Initialize(); } if (IsValidDataIndex(dataIndex) == true) { selectDataIndex = -1; int removeShowIndex = -1; if(dataList[dataIndex].itemIndex != -1) { removeShowIndex = dataList[dataIndex].itemIndex; } dataList[dataIndex].UnlinkItem(true); dataList.RemoveAt(dataIndex); for(int i= dataIndex; i< dataList.Count;i++) { dataList[i].index--; if(removeShowIndex != -1) { if (dataList[i].itemIndex != -1) { dataList[i].itemIndex--; } } } if (removeShowIndex != -1) { if (removeShowIndex < firstItemIndex) { firstItemIndex--; } if (removeShowIndex < lastItemIndex) { lastItemIndex--; } itemCount--; } needReBuildLayout = true; UpdateAllData(immediately); } } /// /// 모든 데이터와 아이템 상태를 초기화하지만, 생성된 아이템 객체는 유지합니다. /// /// true이면 즉시 레이아웃을 업데이트합니다. /// /// /// infiniteScroll.ClearData(true); /// /// public void ClearData(bool immediately = false) { if (isInitialize == false) { Initialize(); } itemCount = 0; selectDataIndex = -1; dataList.Clear(); lineLayout.Clear(); layoutSize = 0; lineCount = 0; ClearItemsData(); lastItemIndex = 0; firstItemIndex = 0; showLineIndex = 0; showLineCount = 0; isStartLine = false; isEndLine = false; needUpdateItemList = true; needReBuildLayout = true; isUpdateArea = true; onFilter = null; ClearScrollContent(); cachedData.Clear(); UpdateAllData(immediately); } /// /// 모든 데이터와 아이템 객체를 완전히 제거하고 스크롤을 초기 상태로 되돌립니다. /// /// /// /// infiniteScroll.Clear(); /// /// public void Clear() { if (isInitialize == false) { Initialize(); } itemCount = 0; selectDataIndex = -1; dataList.Clear(); lineLayout.Clear(); layoutSize = 0; lineCount = 0; ClearItems(); lastItemIndex = 0; firstItemIndex = 0; showLineIndex = 0; showLineCount = 0; isStartLine = false; isEndLine = false; needUpdateItemList = true; needReBuildLayout = true; isUpdateArea = true; onFilter = null; cachedData.Clear(); ClearScrollContent(); } /// /// 기존 데이터 항목을 업데이트합니다. /// /// 업데이트할 데이터입니다. 기존 데이터와 동일한 키를 가져야 합니다. /// /// /// InfiniteScrollData existingData = infiniteScroll.GetData(0); /// if (existingData != null) { /// // 데이터 속성 수정 /// infiniteScroll.UpdateData(existingData); /// } /// /// public void UpdateData(InfiniteScrollData data) { if (isInitialize == false) { Initialize(); } var context = GetDataContext(data); if (context != null) { context.UpdateData(data); needReBuildLayout = true; } } /// /// 모든 데이터를 업데이트하고 스크롤 뷰를 새로 고칩니다. /// /// true이면 즉시 레이아웃을 업데이트합니다. /// /// /// infiniteScroll.UpdateAllData(true); /// /// public void UpdateAllData(bool immediately = true) { if (isInitialize == false) { Initialize(); } needReBuildLayout = true; isUpdateArea = true; CreateNeedItem(); if (immediately == true) { UpdateShowItem(true); } } /// /// 스크롤에 표시될 데이터를 필터링하는 조건을 설정합니다. /// /// 필터링을 위한 Predicate입니다. true를 반환하는 데이터만 표시됩니다. /// /// /// infiniteScroll.SetFilter(data => { /// MyCustomData myData = data as MyCustomData; /// return myData.someValue > 10; /// }); /// infiniteScroll.UpdateAllData(true); /// /// public void SetFilter(Predicate onFilter) { this.onFilter = onFilter; needUpdateItemList = true; } /// /// 뷰포트의 크기를 가져옵니다. /// /// 뷰포트의 주 축 크기입니다. public float GetViewportSize() { return layout.GetMainSize(viewport); } /// /// 콘텐츠의 전체 크기를 가져옵니다. /// /// 콘텐츠의 주 축 크기입니다. public float GetContentSize() { UpdateContentSize(); return layout.GetMainSize(content); } /// /// 콘텐츠의 현재 위치를 가져옵니다. /// /// 콘텐츠의 주 축 위치입니다. public float GetContentPosition() { return layout.GetAxisPosition(content); } /// /// 스크롤 뷰의 크기를 다시 계산하고 업데이트합니다. /// public void ResizeScrollView() { if (isInitialize == false) { Initialize(); } UpdateContentSize(); } /// /// 지정된 아이템 인덱스의 위치를 가져옵니다. /// /// 위치를 가져올 아이템의 인덱스입니다. /// 아이템의 위치입니다. public float GetItemPosition(int itemIndex) { float distance = GetItemDistance(itemIndex); return -layout.GetAxisPostionFromOffset(distance); } /// /// 스크롤 뷰를 수동으로 새로 고칩니다. /// 아이템 목록이나 레이아웃에 변경이 있을 때 호출합니다. /// public void RefreshScroll() { if (isInitialize == false) { Initialize(); } if (needUpdateItemList == true) { BuildItemList(); needUpdateItemList = false; } if (NeedUpdateItem() == true) { UpdateShowItem(); } } /// /// 콘텐츠의 교차 축 크기를 가져옵니다. /// protected float GetCrossSize() { return layout.GetCrossSize(content.rect); } /// /// 모든 아이템의 총 크기에 따라 콘텐츠의 크기를 조절합니다. /// protected void ResizeContent() { cachedData.contentSize = GetItemTotalSize(); content.sizeDelta = layout.GetAxisVector(-layout.padding, cachedData.contentSize); } /// /// 필요한 경우 레이아웃을 다시 빌드하여 콘텐츠 크기를 업데이트합니다. /// protected void UpdateContentSize() { if (needReBuildLayout == true) { BuildLayout(); needReBuildLayout = false; } } /// /// 아이템을 업데이트해야 하는지 확인합니다. /// protected bool NeedUpdateItem() { CheckScrollData(); if (needReBuildLayout == true || isRebuildLayout == true || isUpdateArea == true) { return true; } return false; } /// /// 지정된 위치가 현재 콘텐츠 위치보다 이전에 있는지 확인합니다. (화면 위/왼쪽) /// protected bool IsShowBeforePosition(float position, float contentPosition) { float viewPosition = position - contentPosition; if (viewPosition < 0) { return true; } return false; } /// /// 지정된 위치가 현재 뷰포트 크기를 벗어나는지 확인합니다. (화면 아래/오른쪽) /// protected bool IsShowAfterPosition(float position, float contentPosition, float viewportSize) { float viewPosition = position - contentPosition; if (viewPosition >= viewportSize) { return true; } return false; } private void Update() { if (isInitialize == true) { RefreshScroll(); } } // Inspector에서 값이 변경될 때 호출됩니다. private void OnValidate() { layout.SetDefaults(); } /// /// 스크롤 값 변경 시 발생하는 이벤트를 위한 클래스입니다. /// [Serializable] public class ChangeValueEvent : UnityEvent { public ChangeValueEvent() { } } /// /// 아이템 활성 상태 변경 시 발생하는 이벤트를 위한 클래스입니다. /// [Serializable] public class ItemActiveEvent : UnityEvent { public ItemActiveEvent() { } } /// /// 스크롤 상태 변경(시작/끝 도달) 시 발생하는 이벤트를 위한 클래스입니다. /// [Serializable] public class StateChangeEvent : UnityEvent { public StateChangeEvent() { } } } }