123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828 |
- // -----------------------------------------------------------------------
- // <copyright file="ScrollView.cs" company="AillieoTech">
- // Copyright (c) AillieoTech. All rights reserved.
- // </copyright>
- // -----------------------------------------------------------------------
- namespace TapSDK.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<int, RectTransform> updateFunc;
- protected Func<int, Vector2> itemSizeFunc;
- protected Func<int> itemCountFunc;
- protected Func<int, RectTransform> itemGetFunc;
- protected Action<RectTransform> itemRecycleFunc;
- private readonly List<ScrollItemWithRect> managedItems = new List<ScrollItemWithRect>();
- private Rect refRect;
- // resource management
- private SimpleObjPool<RectTransform> 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<int, RectTransform> func)
- {
- this.updateFunc = func;
- }
- public virtual void SetItemSizeFunc(Func<int, Vector2> func)
- {
- this.itemSizeFunc = func;
- }
- public virtual void SetItemCountFunc(Func<int> func)
- {
- this.itemCountFunc = func;
- }
- public void SetItemGetAndRecycleFunc(Func<int, RectTransform> getFunc, Action<RectTransform> 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<RectTransform>(
- this.poolSize,
- (RectTransform item) =>
- {
- item.transform.SetParent(poolNode.transform, false);
- },
- () =>
- {
- GameObject itemObj = Instantiate(this.itemTemplate.gameObject);
- RectTransform item = itemObj.GetComponent<RectTransform>();
- 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;
- }
- }
- }
|