using System;
using System.Collections.Generic;
using System.Linq;
using EasingCore;
using UnityEngine;
namespace TyphoonUI
{
///
/// 滚动视图(网格排列)(默认模式)
///
public class UIGridView : UIScrollView
{
private const int EXPAND_RENDERER_CELL_COUNT = 1; //拓展渲染数量
[Header("布局参数")] public RectOffset Padding; //留白区域
public Vector2 Spacing; //元素间隔
public GridViewConstraintMode Constraint = GridViewConstraintMode.Flexible; //默认自适应模式
public int FixedCount = 3; //固定数量
private List _groups = new List(); //分组信息
private readonly HashSet _renderIndex = new HashSet(); //渲染索引
private bool IsHorizontal => Scroller.ScrollDirection == ScrollDirection.Horizontal; //是否为横向滚动
protected override void UpdateView(float scrollPosition, bool force)
{
if (DataCount <= 0)
{
return;
}
var rendererRange = CalculateRendererContentRange(scrollPosition);
var startIndex = CalculateStartIndex(scrollPosition);
var beginIndex = Mathf.Clamp(startIndex - EXPAND_RENDERER_CELL_COUNT, 0, int.MaxValue);
var endIndex = beginIndex;
for (int i = beginIndex; i < _groups.Count; i++)
{
var group = _groups[i];
if (group.ContentPositionRange.x > rendererRange.max)
{
//超出渲染范围
break;
}
endIndex = i;
}
//往后拓展渲染元素
endIndex = Mathf.Clamp(endIndex + EXPAND_RENDERER_CELL_COUNT, 0, _groups.Count - 1);
//更新渲染元素
UpdateCells(scrollPosition, startIndex, endIndex);
}
protected override List RebuildCellPositionInfo()
{
var result = new List();
_groups = new List();
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 = IsHorizontal ? (float)Padding.top : Padding.left;
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);
}
}
return result;
}
protected override float RebuildContentLength()
{
if (DataCount <= 0)
{
return 0;
}
var end = _groups.Last();
var tail = IsHorizontal ? Padding.right : Padding.bottom;
return end.ContentPositionRange.y + tail;
}
//滚动范围
protected override Vector2 RebuildScrollRange(float contentLength)
{
if (DataCount <= 0)
{
return Vector2.zero;
}
var length = Mathf.Clamp(contentLength - Scroller.ViewportSize, 0, float.MaxValue);
return new Vector2(0, length);
}
//计算初始索引
protected override int CalculateStartIndex(float scrollPosition)
{
if (DataCount <= 0)
{
return -1;
}
var pos = ClampScrollPosition(scrollPosition);
//二分法先查组,再查元素
var startIndex = 0;
var endIndex = _groups.Count - 1;
while ((endIndex - startIndex) > 1)
{
//二分法查找最近的组
var midIndex = (int)Mathf.Lerp(startIndex, endIndex, 0.5f);
if (IsContainsGroupContentPositionRange(pos, startIndex, midIndex))
{
endIndex = midIndex;
}
else
{
startIndex = midIndex;
}
}
return startIndex;
}
//限制滚动坐标在有效范围内
public float ClampScrollPosition(float scrollPos)
{
return Mathf.Clamp(scrollPos, ScrollRange.x, ScrollRange.y);
}
//判断是否在组
private bool IsContainsGroupContentPositionRange(float contentPos, int groupStartIndex, int groupEndIndex)
{
var spacing = IsHorizontal ? Spacing.x : Spacing.y;
var min = _groups[groupStartIndex].ContentPositionRange.x;
var max = _groups[groupEndIndex].ContentPositionRange.y;
if (groupStartIndex == 0)
{
min = float.MinValue;
}
if (groupEndIndex == _groups.Count - 1)
{
max = float.MaxValue;
}
else
{
max += spacing;
}
return min <= contentPos && max >= contentPos;
}
//计算最靠近的元素位置
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 cellDataIndex = CalculateSnapNearestCell(scrollPosition, SnapAlignment, SnapAnchor);
snapIndex = cellDataIndex;
var posInfo = PositionInfos[cellDataIndex];
var contentPosition = CalculateContentPosition(posInfo, SnapAnchor);
snapPos = contentPosition - Scroller.ViewportSize * Mathf.Clamp01(SnapAlignment);
dataIndex = cellDataIndex;
return new SnapResult(snapPos, snapIndex, dataIndex);
}
//重建分组(自适应模式)
private List RebuildGroupInfosFlexible()
{
var groups = new List();
//元素大小长度(横向滚动以高作为限制区域,垂直滚动以宽作为限制区域)
var cellSizeLength = IsHorizontal ? Scroller.ViewportHeight : Scroller.ViewportWidth;
cellSizeLength -= IsHorizontal ? Padding.vertical : Padding.horizontal;
var cellSpacing = IsHorizontal ? Spacing.y : Spacing.x; //元素间隔
var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y; //组间隔
var paddingHead = IsHorizontal ? (float)Padding.left : (float)Padding.top;
var index = 0;
var occupied = 0f; //占用的高度
var counter = 0; //计数器
foreach (var data in CellInfos)
{
//横向滚动大小取高,垂直滚动大小取宽
var size = IsHorizontal ? data.CellSize.y : data.CellSize.x;
occupied += size;
if (counter > 0)
{
occupied += cellSpacing; //补充spacing
}
if (occupied > cellSizeLength)
{
var beginIndex = -1;
var endIndex = index - 1; //取上一个
var contentStart = paddingHead;
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 paddingHead = IsHorizontal ? (float)Padding.left : (float)Padding.top;
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 = paddingHead;
//计算分组
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;
}
//计算渲染区域
private (float min, float max) CalculateRendererContentRange(float scrollPosition)
{
var min = scrollPosition;
var max = min + Scroller.ViewportSize;
return (min, max);
}
//计算坐标
private Vector3 CalculatePosition(float scrollPosition, int dataIndex)
{
var posInfo = PositionInfos[dataIndex];
var offset = scrollPosition - ScrollRange.x;
var horizontal = Scroller.ScrollDirection == ScrollDirection.Horizontal;
var localPosition = horizontal
? posInfo.LocalPosition - Vector3.right * offset
: posInfo.LocalPosition - Vector3.down * offset;
return CalculateCellLocalPosition(localPosition);
}
//更新元素
private void UpdateCells(float scrollPosition, int beginIndex, int endIndex)
{
var updateCellFlag = false;
//计算元素索引范围
var groupBegin = _groups[beginIndex];
var groupEnd = _groups[endIndex];
var dataStartIndex = groupBegin.BeginDataIndex;
var dataEndIndex = groupEnd.EndDataIndex;
_renderIndex.Clear();
PoolGroup.RecycleInvalid(dataStartIndex, dataEndIndex, _renderIndex);
for (int i = dataStartIndex; i <= dataEndIndex; i++)
{
//已处理,跳过
if (_renderIndex.Contains(i))
{
continue;
}
var data = ItemsSource[i];
var cellInfo = CellInfos[i];
//更新content
var cell = PoolGroup.GetPool(cellInfo.CellName).GetCell(i);
cell.SetVisible(true);
cell.UpdateContent(i, 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 int CalculateSnapNearestGroup(float scrollPosition, float alignment, float anchor)
{
scrollPosition = ClampScrollPosition(scrollPosition);
scrollPosition += Scroller.ViewportSize * Mathf.Clamp01(alignment);
var startIndex = CalculateStartIndex(scrollPosition);
var beginIndex = Mathf.Clamp(startIndex - 1, 0, _groups.Count - 1);
var endIndex = Mathf.Clamp(startIndex + 1, 0, _groups.Count - 1);
var result = beginIndex;
var distance = Mathf.Abs(scrollPosition - CalculateContentPosition(_groups[beginIndex], anchor));
for (int i = beginIndex + 1; i <= endIndex; i++)
{
var group = _groups[i];
var checkDis = Mathf.Abs(scrollPosition - CalculateContentPosition(group, anchor));
if (checkDis < distance)
{
result = i;
distance = checkDis;
}
}
return result;
}
//计算最靠近元素
private int CalculateSnapNearestCell(float scrollPosition, float alignment, float anchor)
{
scrollPosition = ClampScrollPosition(scrollPosition);
var contentPos = scrollPosition + Scroller.ViewportSize * Mathf.Clamp01(alignment);
var groupIndex = CalculateSnapNearestGroup(scrollPosition, alignment, anchor);
//继续判断相邻组的元素
var beginGroupIndex = Mathf.Clamp(groupIndex - 1, 0, _groups.Count - 1);
var endGroupIndex = Mathf.Clamp(groupIndex + 1, 0, _groups.Count - 1);
var beginGroup = _groups[beginGroupIndex];
var endGroup = _groups[endGroupIndex];
var beginIndex = beginGroup.BeginDataIndex;
var endIndex = endGroup.EndDataIndex;
var match = beginIndex;
var distance = Mathf.Abs(contentPos - CalculateContentPosition(PositionInfos[beginIndex], anchor));
for (int i = beginIndex + 1; i <= endIndex; i++)
{
var posInfo = PositionInfos[i];
var dis = Mathf.Abs(contentPos - CalculateContentPosition(posInfo, anchor));
if (dis < distance)
{
match = i;
distance = dis;
}
}
return match;
}
//计算ContentPosition
private float CalculateContentPosition(ScrollCellGroup group, float anchor)
{
return Mathf.Lerp(group.ContentPositionRange.x, group.ContentPositionRange.y, Mathf.Clamp01(anchor));
}
//计算ContentPosition
private float CalculateContentPosition(ScrollCellPositionInfo info, float anchor)
{
return Mathf.Lerp(info.ContentPositionRange.x, info.ContentPositionRange.y, Mathf.Clamp01(anchor));
}
//计算指定的索引滚动位置
public float CalculateScrollPositionFromIndex(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 posInfo = PositionInfos[index];
var contentPosition = CalculateContentPosition(posInfo, Mathf.Clamp01(anchor));
return contentPosition - Scroller.ViewportSize * Mathf.Clamp01(alignment);
}
///
/// 滚动到指定位置
///
/// 滚动值
/// 滚动时间(单位秒)
/// 缓动曲线
/// 完成回调
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 = CalculateScrollPositionFromIndex(index, alignment, anchor);
scrollPos = ClampScrollPosition(scrollPos);
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 = CalculateScrollPositionFromIndex(index, alignment, anchor);
scrollPos = ClampScrollPosition(scrollPos);
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 -= (IsHorizontal ? Padding.vertical : Padding.horizontal); //去除留白区域
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 totalContentSize = Scroller.ViewportSize;
var contentLength = totalContentSize - (IsHorizontal ? Padding.horizontal : Padding.vertical);
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 = IsHorizontal ? Vector3.right * Padding.left : Vector3.down * Padding.top; //组默认偏移
var cellOffset = groupCellDirection * (IsHorizontal ? Padding.top : Padding.left);
for (int i = 0; i < groupNum; i++)
{
//组偏移位置
var groupStartPoint = (i * 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 = Mathf.Clamp(groupNum - 1, 0, int.MaxValue) * groupSpacing;
var contentLength = cellSize * groupNum + totalSpacing;
contentLength += IsHorizontal ? Padding.horizontal : Padding.vertical;
var min = 0;
var max = contentLength - Scroller.ViewportSize;
return (min, max);
}
#endif
}
}