TapCommon-Unity/Runtime/Internal/UI/ScrollViewEx/ScrollView/ScrollView.cs

829 lines
26 KiB
C#

// -----------------------------------------------------------------------
// <copyright file="ScrollView.cs" company="AillieoTech">
// Copyright (c) AillieoTech. All rights reserved.
// </copyright>
// -----------------------------------------------------------------------
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<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;
}
}
}