// ----------------------------------------------------------------------- // // Copyright (c) AillieoTech. All rights reserved. // // ----------------------------------------------------------------------- namespace TapTap.UI.AillieoTech { using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Serialization; using UnityEngine.UI; [RequireComponent(typeof(RectTransform))] [DisallowMultipleComponent] public class ScrollView : ScrollRect { [Tooltip("默认item尺寸")] public Vector2 defaultItemSize; [Tooltip("item的模板")] public RectTransform itemTemplate; // 0001 protected const int flagScrollDirection = 1; [SerializeField] [FormerlySerializedAs("m_layoutType")] protected ItemLayoutType layoutType = ItemLayoutType.Vertical; // 只保存4个临界index protected int[] criticalItemIndex = new int[4]; // callbacks for items protected Action updateFunc; protected Func itemSizeFunc; protected Func itemCountFunc; protected Func itemGetFunc; protected Action itemRecycleFunc; private readonly List managedItems = new List(); private Rect refRect; // resource management private SimpleObjPool itemPool = null; private int dataCount = 0; [Tooltip("初始化时池内item数量")] [SerializeField] private int poolSize; // status private bool initialized = false; private int willUpdateData = 0; private Vector3[] viewWorldConers = new Vector3[4]; private Vector3[] rectCorners = new Vector3[2]; // for hide and show public enum ItemLayoutType { // 最后一位表示滚动方向 Vertical = 0b0001, // 0001 Horizontal = 0b0010, // 0010 VerticalThenHorizontal = 0b0100, // 0100 HorizontalThenVertical = 0b0101, // 0101 } public virtual void SetUpdateFunc(Action func) { this.updateFunc = func; } public virtual void SetItemSizeFunc(Func func) { this.itemSizeFunc = func; } public virtual void SetItemCountFunc(Func func) { this.itemCountFunc = func; } public void SetItemGetAndRecycleFunc(Func getFunc, Action recycleFunc) { if (getFunc != null && recycleFunc != null) { this.itemGetFunc = getFunc; this.itemRecycleFunc = recycleFunc; } else { this.itemGetFunc = null; this.itemRecycleFunc = null; } } public void ResetAllDelegates() { this.SetUpdateFunc(null); this.SetItemSizeFunc(null); this.SetItemCountFunc(null); this.SetItemGetAndRecycleFunc(null, null); } public void UpdateData(bool immediately = true) { if (immediately) { this.willUpdateData |= 3; // 0011 this.InternalUpdateData(); } else { if (this.willUpdateData == 0 && this.IsActive()) { this.StartCoroutine(this.DelayUpdateData()); } this.willUpdateData |= 3; } } public void UpdateDataIncrementally(bool immediately = true) { if (immediately) { this.willUpdateData |= 1; // 0001 this.InternalUpdateData(); } else { if (this.willUpdateData == 0) { this.StartCoroutine(this.DelayUpdateData()); } this.willUpdateData |= 1; } } public void ScrollTo(int index) { this.InternalScrollTo(index); } protected override void OnEnable() { base.OnEnable(); if (this.willUpdateData != 0) { this.StartCoroutine(this.DelayUpdateData()); } } protected override void OnDisable() { this.initialized = false; base.OnDisable(); } protected virtual void InternalScrollTo(int index) { index = Mathf.Clamp(index, 0, this.dataCount - 1); this.EnsureItemRect(index); Rect r = this.managedItems[index].rect; var dir = (int)this.layoutType & flagScrollDirection; if (dir == 1) { // vertical var value = 1 - (-r.yMax / (this.content.sizeDelta.y - this.refRect.height)); this.SetNormalizedPosition(value, 1); } else { // horizontal var value = r.xMin / (this.content.sizeDelta.x - this.refRect.width); this.SetNormalizedPosition(value, 0); } } protected override void SetContentAnchoredPosition(Vector2 position) { base.SetContentAnchoredPosition(position); this.UpdateCriticalItems(); } protected override void SetNormalizedPosition(float value, int axis) { base.SetNormalizedPosition(value, axis); this.ResetCriticalItems(); } protected void EnsureItemRect(int index) { if (!this.managedItems[index].rectDirty) { // 已经是干净的了 return; } ScrollItemWithRect firstItem = this.managedItems[0]; if (firstItem.rectDirty) { Vector2 firstSize = this.GetItemSize(0); firstItem.rect = CreateWithLeftTopAndSize(Vector2.zero, firstSize); firstItem.rectDirty = false; } // 当前item之前的最近的已更新的rect var nearestClean = 0; for (var i = index; i >= 0; --i) { if (!this.managedItems[i].rectDirty) { nearestClean = i; break; } } // 需要更新 从 nearestClean 到 index 的尺寸 Rect nearestCleanRect = this.managedItems[nearestClean].rect; Vector2 curPos = GetLeftTop(nearestCleanRect); Vector2 size = nearestCleanRect.size; this.MovePos(ref curPos, size); for (var i = nearestClean + 1; i <= index; i++) { size = this.GetItemSize(i); this.managedItems[i].rect = CreateWithLeftTopAndSize(curPos, size); this.managedItems[i].rectDirty = false; this.MovePos(ref curPos, size); } var range = new Vector2(Mathf.Abs(curPos.x), Mathf.Abs(curPos.y)); switch (this.layoutType) { case ItemLayoutType.VerticalThenHorizontal: range.x += size.x; range.y = this.refRect.height; break; case ItemLayoutType.HorizontalThenVertical: range.x = this.refRect.width; if (curPos.x != 0) { range.y += size.y; } break; default: break; } this.content.sizeDelta = range; } protected override void OnDestroy() { if (this.itemPool != null) { this.itemPool.Purge(); } } protected Rect GetItemLocalRect(int index) { if (index >= 0 && index < this.dataCount) { this.EnsureItemRect(index); return this.managedItems[index].rect; } return (Rect)default; } #if UNITY_EDITOR protected override void OnValidate() { var dir = (int)this.layoutType & flagScrollDirection; if (dir == 1) { // vertical if (this.horizontalScrollbar != null) { this.horizontalScrollbar.gameObject.SetActive(false); this.horizontalScrollbar = null; } } else { // horizontal if (this.verticalScrollbar != null) { this.verticalScrollbar.gameObject.SetActive(false); this.verticalScrollbar = null; } } base.OnValidate(); } #endif private static Vector2 GetLeftTop(Rect rect) { Vector2 ret = rect.position; ret.y += rect.size.y; return ret; } private static Rect CreateWithLeftTopAndSize(Vector2 leftTop, Vector2 size) { Vector2 leftBottom = leftTop - new Vector2(0, size.y); return new Rect(leftBottom, size); } private IEnumerator DelayUpdateData() { yield return new WaitForEndOfFrame(); this.InternalUpdateData(); } private void InternalUpdateData() { if (!this.IsActive()) { this.willUpdateData |= 3; return; } if (!this.initialized) { this.InitScrollView(); } var newDataCount = 0; var keepOldItems = (this.willUpdateData & 2) == 0; if (this.itemCountFunc != null) { newDataCount = this.itemCountFunc(); } if (newDataCount != this.managedItems.Count) { if (this.managedItems.Count < newDataCount) { // 增加 if (!keepOldItems) { foreach (var itemWithRect in this.managedItems) { // 重置所有rect itemWithRect.rectDirty = true; } } while (this.managedItems.Count < newDataCount) { this.managedItems.Add(new ScrollItemWithRect()); } } else { // 减少 保留空位 避免GC for (int i = 0, count = this.managedItems.Count; i < count; ++i) { if (i < newDataCount) { // 重置所有rect if (!keepOldItems) { this.managedItems[i].rectDirty = true; } if (i == newDataCount - 1) { this.managedItems[i].rectDirty = true; } } // 超出部分 清理回收item if (i >= newDataCount) { this.managedItems[i].rectDirty = true; if (this.managedItems[i].item != null) { this.RecycleOldItem(this.managedItems[i].item); this.managedItems[i].item = null; } } } } } else { if (!keepOldItems) { for (int i = 0, count = this.managedItems.Count; i < count; ++i) { // 重置所有rect this.managedItems[i].rectDirty = true; } } } this.dataCount = newDataCount; this.ResetCriticalItems(); this.willUpdateData = 0; } private void ResetCriticalItems() { bool hasItem, shouldShow; int firstIndex = -1, lastIndex = -1; for (var i = 0; i < this.dataCount; i++) { hasItem = this.managedItems[i].item != null; shouldShow = this.ShouldItemSeenAtIndex(i); if (shouldShow) { if (firstIndex == -1) { firstIndex = i; } lastIndex = i; } if (hasItem && shouldShow) { // 应显示且已显示 this.SetDataForItemAtIndex(this.managedItems[i].item, i); continue; } if (hasItem == shouldShow) { // 不应显示且未显示 // if (firstIndex != -1) // { // // 已经遍历完所有要显示的了 后边的先跳过 // break; // } continue; } if (hasItem && !shouldShow) { // 不该显示 但是有 this.RecycleOldItem(this.managedItems[i].item); this.managedItems[i].item = null; continue; } if (shouldShow && !hasItem) { // 需要显示 但是没有 RectTransform item = this.GetNewItem(i); this.OnGetItemForDataIndex(item, i); this.managedItems[i].item = item; continue; } } // content.localPosition = Vector2.zero; this.criticalItemIndex[CriticalItemType.UpToHide] = firstIndex; this.criticalItemIndex[CriticalItemType.DownToHide] = lastIndex; this.criticalItemIndex[CriticalItemType.UpToShow] = Mathf.Max(firstIndex - 1, 0); this.criticalItemIndex[CriticalItemType.DownToShow] = Mathf.Min(lastIndex + 1, this.dataCount - 1); } private RectTransform GetCriticalItem(int type) { var index = this.criticalItemIndex[type]; if (index >= 0 && index < this.dataCount) { return this.managedItems[index].item; } return null; } private void UpdateCriticalItems() { var dirty = true; while (dirty) { dirty = false; for (int i = CriticalItemType.UpToHide; i <= CriticalItemType.DownToShow; i++) { if (i <= CriticalItemType.DownToHide) { // 隐藏离开可见区域的item dirty = dirty || this.CheckAndHideItem(i); } else { // 显示进入可见区域的item dirty = dirty || this.CheckAndShowItem(i); } } } } private bool CheckAndHideItem(int criticalItemType) { RectTransform item = this.GetCriticalItem(criticalItemType); var criticalIndex = this.criticalItemIndex[criticalItemType]; if (item != null && !this.ShouldItemSeenAtIndex(criticalIndex)) { this.RecycleOldItem(item); this.managedItems[criticalIndex].item = null; if (criticalItemType == CriticalItemType.UpToHide) { // 最上隐藏了一个 this.criticalItemIndex[criticalItemType + 2] = Mathf.Max(criticalIndex, this.criticalItemIndex[criticalItemType + 2]); this.criticalItemIndex[criticalItemType]++; } else { // 最下隐藏了一个 this.criticalItemIndex[criticalItemType + 2] = Mathf.Min(criticalIndex, this.criticalItemIndex[criticalItemType + 2]); this.criticalItemIndex[criticalItemType]--; } this.criticalItemIndex[criticalItemType] = Mathf.Clamp(this.criticalItemIndex[criticalItemType], 0, this.dataCount - 1); if (this.criticalItemIndex[CriticalItemType.UpToHide] > this.criticalItemIndex[CriticalItemType.DownToHide]) { // 偶然的情况 拖拽超出一屏 this.ResetCriticalItems(); return false; } return true; } return false; } private bool CheckAndShowItem(int criticalItemType) { RectTransform item = this.GetCriticalItem(criticalItemType); var criticalIndex = this.criticalItemIndex[criticalItemType]; if (item == null && this.ShouldItemSeenAtIndex(criticalIndex)) { RectTransform newItem = this.GetNewItem(criticalIndex); this.OnGetItemForDataIndex(newItem, criticalIndex); this.managedItems[criticalIndex].item = newItem; if (criticalItemType == CriticalItemType.UpToShow) { // 最上显示了一个 this.criticalItemIndex[criticalItemType - 2] = Mathf.Min(criticalIndex, this.criticalItemIndex[criticalItemType - 2]); this.criticalItemIndex[criticalItemType]--; } else { // 最下显示了一个 this.criticalItemIndex[criticalItemType - 2] = Mathf.Max(criticalIndex, this.criticalItemIndex[criticalItemType - 2]); this.criticalItemIndex[criticalItemType]++; } this.criticalItemIndex[criticalItemType] = Mathf.Clamp(this.criticalItemIndex[criticalItemType], 0, this.dataCount - 1); if (this.criticalItemIndex[CriticalItemType.UpToShow] >= this.criticalItemIndex[CriticalItemType.DownToShow]) { // 偶然的情况 拖拽超出一屏 this.ResetCriticalItems(); return false; } return true; } return false; } private bool ShouldItemSeenAtIndex(int index) { if (index < 0 || index >= this.dataCount) { return false; } this.EnsureItemRect(index); return new Rect(this.refRect.position - this.content.anchoredPosition, this.refRect.size).Overlaps(this.managedItems[index].rect); } private bool ShouldItemFullySeenAtIndex(int index) { if (index < 0 || index >= this.dataCount) { return false; } this.EnsureItemRect(index); return this.IsRectContains(new Rect(this.refRect.position - this.content.anchoredPosition, this.refRect.size), this.managedItems[index].rect); } private bool IsRectContains(Rect outRect, Rect inRect, bool bothDimensions = false) { if (bothDimensions) { var xContains = (outRect.xMax >= inRect.xMax) && (outRect.xMin <= inRect.xMin); var yContains = (outRect.yMax >= inRect.yMax) && (outRect.yMin <= inRect.yMin); return xContains && yContains; } else { var dir = (int)this.layoutType & flagScrollDirection; if (dir == 1) { // 垂直滚动 只计算y向 return (outRect.yMax >= inRect.yMax) && (outRect.yMin <= inRect.yMin); } else { // = 0 // 水平滚动 只计算x向 return (outRect.xMax >= inRect.xMax) && (outRect.xMin <= inRect.xMin); } } } private void InitPool() { var poolNode = new GameObject("POOL"); poolNode.SetActive(false); poolNode.transform.SetParent(this.transform, false); this.itemPool = new SimpleObjPool( this.poolSize, (RectTransform item) => { item.transform.SetParent(poolNode.transform, false); }, () => { GameObject itemObj = Instantiate(this.itemTemplate.gameObject); RectTransform item = itemObj.GetComponent(); itemObj.transform.SetParent(poolNode.transform, false); item.anchorMin = Vector2.up; item.anchorMax = Vector2.up; item.pivot = Vector2.zero; itemObj.SetActive(true); return item; }); } private void OnGetItemForDataIndex(RectTransform item, int index) { this.SetDataForItemAtIndex(item, index); item.transform.SetParent(this.content, false); } private void SetDataForItemAtIndex(RectTransform item, int index) { if (this.updateFunc != null) { this.updateFunc(index, item); } this.SetPosForItemAtIndex(item, index); } private void SetPosForItemAtIndex(RectTransform item, int index) { this.EnsureItemRect(index); Rect r = this.managedItems[index].rect; item.localPosition = r.position; item.sizeDelta = r.size; } private Vector2 GetItemSize(int index) { if (index >= 0 && index <= this.dataCount) { if (this.itemSizeFunc != null) { return this.itemSizeFunc(index); } } return this.defaultItemSize; } private RectTransform GetNewItem(int index) { RectTransform item; if (this.itemGetFunc != null) { item = this.itemGetFunc(index); } else { item = this.itemPool.Get(); } return item; } private void RecycleOldItem(RectTransform item) { if (this.itemRecycleFunc != null) { this.itemRecycleFunc(item); } else { this.itemPool.Recycle(item); } } private void InitScrollView() { this.initialized = true; // 根据设置来控制原ScrollRect的滚动方向 var dir = (int)this.layoutType & flagScrollDirection; this.vertical = dir == 1; this.horizontal = dir == 0; this.content.pivot = Vector2.up; this.content.anchorMin = Vector2.up; this.content.anchorMax = Vector2.up; this.content.anchoredPosition = Vector2.zero; this.InitPool(); this.UpdateRefRect(); } // refRect是在Content节点下的 viewport的 rect private void UpdateRefRect() { /* * WorldCorners * * 1 ------- 2 * | | * | | * 0 ------- 3 * */ if (!CanvasUpdateRegistry.IsRebuildingLayout()) { Canvas.ForceUpdateCanvases(); } this.viewRect.GetWorldCorners(this.viewWorldConers); this.rectCorners[0] = this.content.transform.InverseTransformPoint(this.viewWorldConers[0]); this.rectCorners[1] = this.content.transform.InverseTransformPoint(this.viewWorldConers[2]); this.refRect = new Rect((Vector2)this.rectCorners[0] - this.content.anchoredPosition, this.rectCorners[1] - this.rectCorners[0]); } private void MovePos(ref Vector2 pos, Vector2 size) { // 注意 所有的rect都是左下角为基准 switch (this.layoutType) { case ItemLayoutType.Vertical: // 垂直方向 向下移动 pos.y -= size.y; break; case ItemLayoutType.Horizontal: // 水平方向 向右移动 pos.x += size.x; break; case ItemLayoutType.VerticalThenHorizontal: pos.y -= size.y; if (pos.y <= -this.refRect.height) { pos.y = 0; pos.x += size.x; } break; case ItemLayoutType.HorizontalThenVertical: pos.x += size.x; if (pos.x >= this.refRect.width) { pos.x = 0; pos.y -= size.y; } break; default: break; } } // const int 代替 enum 减少 (int)和(CriticalItemType)转换 protected static class CriticalItemType { public static byte UpToHide = 0; public static byte DownToHide = 1; public static byte UpToShow = 2; public static byte DownToShow = 3; } private class ScrollItemWithRect { // scroll item 身上的 RectTransform组件 public RectTransform item; // scroll item 在scrollview中的位置 public Rect rect; // rect 是否需要更新 public bool rectDirty = true; } } }