using System;
using System.Collections.Generic;
using System.Linq;
using EasingCore;
using UnityEngine;
namespace TyphoonUI
{
///
/// 滚动视图(网格排列)(循环模式)
///
public class UIGridViewLoop : UIScrollView
{
private const int EXPAND_RENDERER_GROUP_COUNT = 0; //向外渲染多少组元素
[Header("布局参数")] public Vector2 Spacing; //元素间隔
public float HeadOffset = 0; //子元素坐标偏移量
public GridViewConstraintMode Constraint = GridViewConstraintMode.Flexible; //默认自适应模式
public int FixedCount = 3; //固定数量
public List Groups { get; private set; } = new List(); //分组信息
private readonly HashSet _renderIndex = new HashSet(); //渲染索引
private float PageSize { get; set; } //单页大小
private bool IsHorizontal => Scroller.ScrollDirection == ScrollDirection.Horizontal; //是否为横向滚动
//更新视图
protected override void UpdateView(float scrollPosition, bool force)
{
if (DataCount <= 0)
{
return;
}
//计算渲染范围
var range = CalculateContentRange(scrollPosition);
var startRenderGroupIndex = CalculateStartIndex(scrollPosition); //开始渲染组的索引
//拓展渲染范围
var startIndex = startRenderGroupIndex - EXPAND_RENDERER_GROUP_COUNT;
var limitMax = startIndex + Groups.Count - 1;
var endIndex = startIndex;
for (int i = startIndex + 1; i <= limitMax; i++)
{
var contentRange = CalculateRendererContentRangeByGroupRenderIndex(i);
if (contentRange.min > range.max)
{
//超出视窗范围
break;
}
endIndex = i;
}
//更新元素
UpdateCells(scrollPosition, startIndex, endIndex);
}
//重建元素坐标信息
protected override List RebuildCellPositionInfo()
{
//计算元素坐标信息
var result = new List();
PageSize = 0;
if (DataCount <= 0)
{
return result;
}
//固定数量不可小于1
if (FixedCount <= 0)
{
Debug.LogError("FixedCount 需要 >=1");
FixedCount = 1;
}
switch (Constraint)
{
case GridViewConstraintMode.Flexible:
Groups = RebuildGroupInfosFlexible();
break;
case GridViewConstraintMode.Fixed:
Groups = RebuildGroupInfosFixed(FixedCount);
break;
default:
throw new Exception($"未处理的类型:{Constraint}");
}
var index = 0;
var paddingHead = HeadOffset;
var cellSpacing = IsHorizontal ? Spacing.y : Spacing.x;
foreach (var group in Groups)
{
var groupContent = group.ContentPositionRange;
var localPos = IsHorizontal ? Vector3.right * groupContent.x : Vector3.down * groupContent.x;
var cellOffset = paddingHead;
for (int i = group.BeginDataIndex, counter = 0; i <= group.EndDataIndex; counter++, i++)
{
//计算坐标
var cellSize = CellInfos[i].CellSize;
var cellContentSize = IsHorizontal ? cellSize.x : cellSize.y;
if (counter > 0)
{
//补充元素间隔
cellOffset += cellSpacing;
}
var info = new ScrollCellPositionInfo()
{
ContentPositionRange = new Vector2(groupContent.x, groupContent.x + cellContentSize),
LocalPosition = localPos +
(IsHorizontal ? Vector3.down * cellOffset : Vector3.right * cellOffset)
};
cellOffset += IsHorizontal ? cellSize.y : cellSize.x;
result.Add(info);
}
}
var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y;
PageSize = Groups.Last().ContentPositionRange.y + groupSpacing; //计算分页大小
return result;
}
//内容框长度
protected override float RebuildContentLength()
{
return float.MaxValue;
}
//滚动范围
protected override Vector2 RebuildScrollRange(float contentLength)
{
return new Vector2(float.MinValue, float.MaxValue);
}
//计算起始索引
protected override int CalculateStartIndex(float scrollPosition)
{
if (DataCount <= 0)
{
return -1;
}
//取3页的数据进行判断,哪个起始索引最靠近
var pageIndex = GetPageIndexByScrollPosition(scrollPosition);
var beginPage = pageIndex - 1; //起始页
var endPage = pageIndex + 1; //结束页
var startRange = GetGroupRenderIndexRangeByPageIndex(beginPage); //起始页组索引范围
var endRange = GetGroupRenderIndexRangeByPageIndex(endPage); //结束页组索引范围
var beginGroup = startRange.begin; //起始组
var endGroup = endRange.end; //结束组
while ((endGroup - beginGroup) > 1)
{
var mid = (int)Mathf.Lerp(beginGroup, endGroup, 0.5f);
if (IsInRendererContentRangeByGroupRenderIndex(scrollPosition, beginGroup, mid))
{
endGroup = mid;
}
else
{
beginGroup = mid;
}
}
return beginGroup;
}
//计算最佳吸附位置
public override SnapResult CalculateSnapPosition(float scrollPosition)
{
var snapPos = 0f;
var snapIndex = -1;
var dataIndex = -1;
if (DataCount <= 0)
{
return new SnapResult(snapPos, snapIndex, dataIndex);
}
var checkScrollPosition = scrollPosition + Scroller.ViewportSize * Mathf.Clamp01(SnapAlignment);
//获取起始组
var groupStart = CalculateStartIndex(checkScrollPosition);
//从三组元素中找最接近的元素
var cellStart = GetCellRenderIndexRangeByGroupRenderIndex(groupStart - 1).begin;
var cellEnd = GetGroupRenderIndexRangeByPageIndex(groupStart + 1).end;
var nearestPos = CalculateCellContentPosition(cellStart, SnapAnchor);
var distance = Mathf.Abs(nearestPos - checkScrollPosition);
for (int i = cellStart + 1; i <= cellEnd; i++)
{
var pos = CalculateCellContentPosition(i, SnapAnchor);
var checkDis = Mathf.Abs(pos - checkScrollPosition); //计算距离
if (checkDis < distance)
{
snapIndex = i;
distance = checkDis;
nearestPos = pos;
}
}
var offset = nearestPos - checkScrollPosition; //计算滚动偏移
snapPos = scrollPosition + offset;
dataIndex = GetDataIndexByCellRenderIndex(snapIndex);
return new SnapResult(snapPos, snapIndex, dataIndex);
}
//重建分组(自适应模式)
private List RebuildGroupInfosFlexible()
{
var groups = new List();
//元素大小长度(横向滚动以高作为限制区域,垂直滚动以宽作为限制区域)
var cellSizeLength = IsHorizontal ? Scroller.ViewportHeight : Scroller.ViewportWidth;
cellSizeLength -= HeadOffset;
var cellSpacing = IsHorizontal ? Spacing.y : Spacing.x; //元素间隔
var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y; //组间隔
var index = 0;
var occupied = 0f; //占用的高度
var counter = 0; //计数器
foreach (var element in CellInfos)
{
//横向滚动大小取高,垂直滚动大小取宽
var size = IsHorizontal ? element.CellSize.y : element.CellSize.x;
occupied += size;
if (counter > 0)
{
occupied += cellSpacing; //补充spacing
}
if (occupied > cellSizeLength)
{
var beginIndex = -1;
var endIndex = index - 1; //取上一个
var contentStart = 0f;
if (groups.Count > 0)
{
var last = groups.Last();
beginIndex = last.EndDataIndex + 1;
contentStart = groups.Last().ContentPositionRange.y + groupSpacing;
}
else
{
beginIndex = 0;
}
//单元素占满空间
if (endIndex < beginIndex)
{
endIndex = beginIndex;
counter = 0;
}
else
{
occupied = size;
counter = 1;
}
//计算content range
var range = CalculateGroupContentRange(contentStart, beginIndex, endIndex);
//创建元素分组
groups.Add(new ScrollCellGroup()
{
GroupIndex = groups.Count,
BeginDataIndex = beginIndex,
EndDataIndex = endIndex,
ContentPositionRange = range,
});
}
else
{
counter += 1;
}
index += 1;
}
//补充最后一组
var lastGroup = groups.Last();
var dataLastIndex = ItemsSource.Count - 1;
if (lastGroup.EndDataIndex < dataLastIndex)
{
var beginIndex = lastGroup.EndDataIndex + 1;
var endIndex = dataLastIndex;
var contentStart = lastGroup.ContentPositionRange.y + groupSpacing;
//计算content range
var range = CalculateGroupContentRange(contentStart, beginIndex, endIndex);
//创建元素分组
groups.Add(new ScrollCellGroup()
{
GroupIndex = groups.Count,
BeginDataIndex = beginIndex,
EndDataIndex = endIndex,
ContentPositionRange = range,
});
}
return groups;
}
//重建分组(固定模式)
private List RebuildGroupInfosFixed(int count)
{
var groups = new List();
var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y;
var index = 0;
while (index < ItemsSource.Count)
{
var startIndex = index;
index += count;
var endIndex = Mathf.Clamp(index - 1, startIndex, ItemsSource.Count - 1);
var contentStart = 0f;
//计算分组
if (groups.Count > 0)
{
var last = groups.Last();
contentStart = last.ContentPositionRange.y + groupSpacing;
}
//加入组
groups.Add(new ScrollCellGroup()
{
GroupIndex = groups.Count,
BeginDataIndex = startIndex,
EndDataIndex = endIndex,
ContentPositionRange = CalculateGroupContentRange(contentStart, startIndex, endIndex)
});
}
return groups;
}
private Vector2 CalculateGroupContentRange(float start, int cellStartIndex, int cellEndIndex)
{
if (IsHorizontal)
{
return new Vector2(start, start + CalculateCellMaxWidth(cellStartIndex, cellEndIndex));
}
return new Vector2(start, start + CalculateCellMaxHeight(cellStartIndex, cellEndIndex));
}
//计算元素最大宽度
private float CalculateCellMaxWidth(int startIndex, int endIndex)
{
var maxWidth = CellInfos[startIndex].CellSize.x;
for (int i = startIndex + 1; i <= endIndex; i++)
{
var cellWidth = CellInfos[i].CellSize.x;
if (cellWidth > maxWidth)
{
maxWidth = cellWidth;
}
}
return maxWidth;
}
//计算元素最大高度
private float CalculateCellMaxHeight(int startIndex, int endIndex)
{
var maxHeight = CellInfos[startIndex].CellSize.y;
for (int i = startIndex + 1; i <= endIndex; i++)
{
var cellHeight = CellInfos[i].CellSize.y;
if (cellHeight > maxHeight)
{
maxHeight = cellHeight;
}
}
return maxHeight;
}
//获取页索引,传入scrollPosition
private int GetPageIndexByScrollPosition(float scrollPosition)
{
if (DataCount <= 0)
{
return 0;
}
return (int)(scrollPosition / PageSize);
}
//组渲染索引->页码索引
private int GetPageIndexByGroupRenderIndex(int groupRenderIndex)
{
return groupRenderIndex >= 0
? groupRenderIndex / Groups.Count
: -Mathf.CeilToInt((float)Mathf.Abs(groupRenderIndex) / Groups.Count);
}
//组渲染索引->组索引
private int GetGroupIndexByGroupRenderIndex(int groupRenderIndex)
{
if (groupRenderIndex < 0)
{
groupRenderIndex = groupRenderIndex % Groups.Count + Groups.Count;
}
return groupRenderIndex % Groups.Count;
}
//计算渲染区域范围
private (float min, float max) CalculateRendererContentRangeByGroupRenderIndex(int groupRenderIndex)
{
//计算页码索引
var pageIndex = GetPageIndexByGroupRenderIndex(groupRenderIndex);
var index = GetGroupIndexByGroupRenderIndex(groupRenderIndex);
var group = Groups[index];
return CalculateRendererContentRange(group, pageIndex);
}
//计算渲染区域范围
private (float min, float max) CalculateRendererContentRange(ScrollCellGroup group, int pageIndex)
{
var offset = PageSize * pageIndex;
var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y;
var min = group.ContentPositionRange.x;
var max = group.ContentPositionRange.y + groupSpacing;
return (min + offset, max + offset);
}
//是否在渲染范围内
private bool IsInRendererContentRangeByGroupRenderIndex(float contentPosition, int groupRenderBeginIndex,
int groupRenderEndIndex)
{
var startRange = CalculateRendererContentRangeByGroupRenderIndex(groupRenderBeginIndex);
var endRange = CalculateRendererContentRangeByGroupRenderIndex(groupRenderEndIndex);
return startRange.min <= contentPosition && endRange.max >= contentPosition;
}
//是否在组范围内
private bool IsInGroupContentRange(float contentPosition, int startGroupIndex, int endGroupIndex)
{
var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y;
var groupStart = Groups[startGroupIndex];
var groupEnd = Groups[endGroupIndex];
return groupStart.ContentPositionRange.x <= contentPosition &&
contentPosition <= groupEnd.ContentPositionRange.y + groupSpacing;
}
//计算渲染区域范围
private (float min, float max) CalculateContentRange(float scrollPosition)
{
var min = scrollPosition;
var max = min + Scroller.ViewportSize;
return (min, max);
}
//计算组渲染范围
private (int begin, int end) GetGroupRenderIndexRangeByPageIndex(int pageIndex)
{
var begin = Groups.Count * pageIndex;
var end = begin + Groups.Count - 1;
return (begin, end);
}
//计算元素渲染范围
private (int begin, int end) GetCellRenderIndexRangeByGroupRenderIndex(
int groupRenderIndex)
{
var groupIndex = GetGroupIndexByGroupRenderIndex(groupRenderIndex);
var pageIndex = GetPageIndexByGroupRenderIndex(groupRenderIndex);
var group = Groups[groupIndex];
var beginDataIndex = group.BeginDataIndex;
var endDataIndex = group.EndDataIndex;
var indexOffset = pageIndex * ItemsSource.Count;
return (beginDataIndex + indexOffset, endDataIndex + indexOffset);
}
//获取数据索引
public int GetDataIndexByCellRenderIndex(int cellRenderIndex)
{
if (cellRenderIndex < 0)
{
cellRenderIndex = cellRenderIndex % ItemsSource.Count + ItemsSource.Count;
}
return cellRenderIndex % ItemsSource.Count;
}
//获取页码索引
private int GetPageIndexByCellRenderIndex(int cellRenderIndex)
{
return cellRenderIndex >= 0
? cellRenderIndex / ItemsSource.Count
: -Mathf.CeilToInt((float)Mathf.Abs(cellRenderIndex) / ItemsSource.Count);
}
//计算元素坐标
private Vector3 CalculatePosition(float scrollPosition, int cellRenderIndex)
{
var pageIndex = GetPageIndexByCellRenderIndex(cellRenderIndex); //页码索引
var offsetLength = pageIndex * PageSize - scrollPosition; //计算偏移坐标
var dataIndex = GetDataIndexByCellRenderIndex(cellRenderIndex); //数据索引
var posInfo = PositionInfos[dataIndex];
var offset = IsHorizontal ? offsetLength * Vector3.right : offsetLength * Vector3.down;
return CalculateCellLocalPosition(posInfo.LocalPosition + offset); //实际坐标
}
//渲染元素
private void UpdateCells(float scrollPosition, int beginIndex, int endIndex)
{
var updateCellFlag = false;
//计算元素渲染索引
var dataRenderBeginRange = GetCellRenderIndexRangeByGroupRenderIndex(beginIndex);
var dataRenderEndRange = GetCellRenderIndexRangeByGroupRenderIndex(endIndex);
var cellRenderBeginIndex = dataRenderBeginRange.begin;
var cellRenderEndIndex = dataRenderEndRange.end;
_renderIndex.Clear(); //清空渲染索引
PoolGroup.RecycleInvalid(cellRenderBeginIndex, cellRenderEndIndex, _renderIndex);
for (int i = cellRenderBeginIndex; i <= cellRenderEndIndex; i++)
{
//已处理,跳过
if (_renderIndex.Contains(i))
{
continue;
}
var dataIndex = GetDataIndexByCellRenderIndex(i);
var data = ItemsSource[dataIndex];
var cellInfo = CellInfos[dataIndex];
//更新content
var cell = PoolGroup.GetPool(cellInfo.CellName).GetCell(i);
cell.SetVisible(true);
cell.UpdateContent(dataIndex, data); //更新数据
updateCellFlag = true;
}
//遍历所有的使用中的元素,更新坐标
var pools = PoolGroup.Pools;
foreach (var pair in pools)
{
var pool = pair.Value;
foreach (var poolObject in pool.PoolObjects)
{
if (!poolObject.IsBusy)
{
continue;
}
//更新坐标
var localPos = CalculatePosition(scrollPosition, poolObject.CellIndex);
poolObject.Cell.Root.localPosition = localPos;
poolObject.Cell.OnUpdatePosition(scrollPosition, localPos, this); //触发坐标变动
}
}
if (updateCellFlag)
{
InvokeOnCellUpdate();
}
}
//计算元素区域坐标
private float CalculateCellContentPosition(int cellRenderIndex, float anchor)
{
var pageIndex = GetPageIndexByCellRenderIndex(cellRenderIndex); //页码索引
var offset = pageIndex * PageSize; //计算偏移坐标
var dataIndex = GetDataIndexByCellRenderIndex(cellRenderIndex); //数据索引
var posInfo = PositionInfos[dataIndex];
var min = posInfo.ContentPositionRange.x + offset;
var max = posInfo.ContentPositionRange.y + offset;
return Mathf.Lerp(min, max, Mathf.Clamp01(anchor));
}
//计算最近索引滚动坐标
public float CalculateNearestScrollPositionFromIndex(int index, float alignment = 0.5f,
float anchor = 0.5f)
{
if (index < 0 || index >= PositionInfos.Count)
{
throw new Exception($"超出元素索引范围!当前length:{PositionInfos.Count} index:{index}");
}
var scrollPosition = Scroller.Position;
var checkScrollPosition = scrollPosition + Scroller.ViewportSize * Mathf.Clamp01(SnapAlignment);
//获取起始组
var pageIndex = GetPageIndexByScrollPosition(checkScrollPosition);
//只从相邻的5页去找
var startPageIndex = pageIndex - 2;
var endPageIndex = pageIndex + 2;
var startIndex = index + startPageIndex * ItemsSource.Count;
var pos = CalculateCellContentPosition(startIndex, anchor);
var distance = Mathf.Abs(pos - checkScrollPosition);
//跳过第一个(已被计算)
for (int i = startPageIndex + 1; i <= endPageIndex; i++)
{
var idx = index + i * ItemsSource.Count;
var temPos = CalculateCellContentPosition(idx, anchor);
var temDis = Mathf.Abs(temPos - checkScrollPosition);
if (temDis < distance)
{
distance = temDis;
pos = temPos;
}
}
//计算偏移量
var offset = pos - checkScrollPosition;
return scrollPosition + offset;
}
///
/// 滚动到指定位置
///
/// 滚动值
/// 滚动时间(单位秒)
/// 缓动曲线
/// 完成回调
public void ScrollToPosition(float scrollPosition, float duration, Ease easing = Ease.OutCubic,
Action onComplete = null)
{
Scroller.ScrollTo(scrollPosition, duration, easing, onComplete);
}
///
/// 滚动到指定数据索引位置
///
/// 数据索引
/// 滚动时间(单位秒)
/// 对齐系数,范围:[0,1],0:靠左/上,1:靠右/下
/// 锚点系数,范围:[0,1],0:元素左/上作为锚点,1:元素右/下作为锚点
/// 缓动曲线
/// 完成回调
public void ScrollTo(int index, float duration, float alignment = 0.5f, float anchor = 0.5f,
Ease easing = Ease.OutCubic, Action onComplete = null)
{
var scrollPos = CalculateNearestScrollPositionFromIndex(index, alignment, anchor);
ScrollToPosition(scrollPos, duration, easing, onComplete);
}
///
/// 跳转到指定索引位置
///
/// 数据索引
/// 对齐方式, 参考ViewPort
/// 元素锚点,范围:[0,1] 0表示左上,1表示右下
public void JumpTo(int index, float alignment = 0.5f, float anchor = 0.5f)
{
//找最近的索引位置
var scrollPos = CalculateNearestScrollPositionFromIndex(index, alignment, anchor);
Scroller.JumpToScrollPosition(scrollPos);
}
#if UNITY_EDITOR
protected override void GeneratePreviewCells()
{
base.GeneratePreviewCells();
ClearPreviewCells();
if (!CanPreview())
{
return;
}
//检查大小是否匹配
var cellSize = IsHorizontal ? CellSize.x : CellSize.y;
if (cellSize <= 0)
{
Debug.LogError("Cell Size <=0,无法预览!");
return;
}
var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y;
var groupLength = IsHorizontal ? Scroller.ViewportHeight : Scroller.ViewportWidth; //组长度
groupLength -= HeadOffset; //去除留白区域
var groupCellSpacing = IsHorizontal ? Spacing.y : Spacing.x; //组元素间隔
var groupCellSize = IsHorizontal ? CellSize.y : CellSize.x; //组元素大小
//计算每组能显示的数量
var groupCellCount = (int)((groupLength + groupCellSpacing) / (groupCellSpacing + groupCellSize));
var len = (Mathf.Clamp(groupCellCount - 1, 0, int.MaxValue)) * groupCellCount +
groupCellCount * groupCellSize;
if (len > groupLength)
{
groupCellCount -= 1;
}
groupCellCount = Mathf.Clamp(groupCellCount, 1, int.MaxValue); //每组显示的数量
if (Constraint == GridViewConstraintMode.Fixed)
{
if (FixedCount <= 0)
{
FixedCount = 1;
}
groupCellCount = FixedCount;
}
//计算分组数量
var contentLength = Scroller.ViewportSize;
var unitSize = cellSize + groupSpacing;
var groupNum = Mathf.CeilToInt(contentLength / unitSize) + 1;
//生成预览实例
var cellPoints = new List();
var groupOffsetDirection = IsHorizontal ? Vector3.right : Vector3.down;
var groupCellDirection = IsHorizontal ? Vector3.down : Vector3.right;
var groupOffset = Vector3.zero; //组默认偏移
var cellOffset = groupCellDirection * HeadOffset;
for (int i = 0; i < groupNum; i++)
{
//组偏移位置
var groupStartPoint = (groupSpacing + cellSize) * i * groupOffsetDirection + groupOffset;
for (int j = 0; j < groupCellCount; j++)
{
var cellStartPoint = (groupCellSpacing + groupCellSize) * j * groupCellDirection + cellOffset;
var localPos = groupStartPoint + cellStartPoint;
cellPoints.Add(localPos);
}
}
//计算滚动视窗大小
var scrollRange = CalculatePreviewScrollRange(groupNum);
//修正偏移位置
var scrollOffset = Mathf.Lerp(scrollRange.min, scrollRange.max, PreviewScrollPercent);
cellPoints = cellPoints.Select(e => e - groupOffsetDirection * scrollOffset).ToList();
//实例化预览元素
InstantiatePreviewCells(cellPoints);
}
//计算滚动范围
private (float min, float max) CalculatePreviewScrollRange(int groupNum)
{
var cellSize = IsHorizontal ? CellSize.x : CellSize.y;
var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y;
var totalSpacing = groupNum * groupSpacing;
var contentLength = cellSize * groupNum + totalSpacing;
var min = 0;
var max = contentLength - Scroller.ViewportSize;
return (min, max);
}
#endif
}
}