using System;
using System.Collections.Generic;
using System.Linq;
using EasingCore;
using UnityEngine;
using UnityEngine.Serialization;
namespace TyphoonUI
{
///
/// 滚动视图(单向排列)(默认模式)
///
public class UIScrollRectLoop : UIScrollView
{
[Header("布局参数")] public float Spacing; //元素间隔
public float HeadOffset = 0; //子元素坐标偏移量
private float PageSize { get; set; } //单页大小
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 range = CalculateContentRangeByScrollPosition(scrollPosition);
var startIndex = CalculateStartIndex(scrollPosition); //开始渲染组的索引
var limitMax = startIndex + ItemsSource.Count - 1;
var endIndex = startIndex;
for (int i = startIndex + 1; i <= limitMax; i++)
{
var contentRange = CalculateContentRange(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;
}
var index = 0;
var pos = 0f;
var horizontal = IsHorizontal;
var localPosOffset = horizontal ? Vector3.right * HeadOffset : Vector3.down * HeadOffset;
//分配元素在内容框的范围
foreach (var element in CellInfos)
{
var min = pos;
pos += horizontal ? element.CellSize.x : element.CellSize.y; //补充cell大小
var max = pos;
var localPos = horizontal ? Vector3.right * min : Vector3.down * min;
pos += Spacing; //补充空隙
//计算元素localPosition
result.Add(new ScrollCellPositionInfo()
{
ContentPositionRange = new Vector2(min, max),
LocalPosition = localPos + localPosOffset,
});
index += 1;
}
PageSize = pos;
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 begin = GetRenderIndexRangeByPageIndex(beginPage).begin;
var end = GetRenderIndexRangeByPageIndex(endPage).end;
while ((end - begin) > 1)
{
var mid = (int)Mathf.Lerp(begin, end, 0.5f);
if (IsInRendererContentRangeByGroupRenderIndex(scrollPosition, begin, mid))
{
end = mid;
}
else
{
begin = mid;
}
}
return begin;
}
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 startIndex = CalculateStartIndex(checkScrollPosition);
//从相邻的三个元素中找最接近的
var renderStart = startIndex - 1;
var renderEnd = startIndex + 1;
var nearestPos = CalculateCellContentPosition(renderStart, SnapAnchor);
var distance = Mathf.Abs(nearestPos - checkScrollPosition);
for (int i = renderStart + 1; i <= renderEnd; 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 = GetDataIndexByRenderIndex(snapIndex);
return new SnapResult(snapPos, snapIndex, dataIndex);
}
//计算元素区域坐标
private float CalculateCellContentPosition(int renderIndex, float anchor)
{
var pageIndex = GetPageIndexByRenderIndex(renderIndex); //页码索引
var offset = pageIndex * PageSize; //计算偏移坐标
var dataIndex = GetDataIndexByRenderIndex(renderIndex); //数据索引
var posInfo = PositionInfos[dataIndex];
var min = posInfo.ContentPositionRange.x + offset;
var max = posInfo.ContentPositionRange.y + offset;
return Mathf.Lerp(min, max, Mathf.Clamp01(anchor));
}
//获取页索引,传入scrollPosition
private int GetPageIndexByScrollPosition(float scrollPosition)
{
if (DataCount <= 0)
{
return 0;
}
return (int)(scrollPosition / PageSize);
}
//获取页索引,传入renderIndex
private int GetPageIndexByRenderIndex(int renderIndex)
{
return renderIndex >= 0
? renderIndex / ItemsSource.Count
: -Mathf.CeilToInt((float)Mathf.Abs(renderIndex) / ItemsSource.Count);
}
//计算渲染范围
private (int begin, int end) GetRenderIndexRangeByPageIndex(int pageIndex)
{
var begin = ItemsSource.Count * pageIndex;
var end = begin + ItemsSource.Count - 1;
return (begin, end);
}
//获取数据索引
private int GetDataIndexByRenderIndex(int renderIndex)
{
if (renderIndex < 0)
{
renderIndex = renderIndex % ItemsSource.Count + ItemsSource.Count;
}
return renderIndex % ItemsSource.Count;
}
//计算渲染索引对应的ContentRange
private (float min, float max) CalculateContentRange(int renderIndex)
{
var pageIndex = GetPageIndexByRenderIndex(renderIndex);
var dataIndex = GetDataIndexByRenderIndex(renderIndex);
var posInfo = PositionInfos[dataIndex];
return CalculateContentRange(posInfo, pageIndex);
}
//计算渲染区域范围
private (float min, float max) CalculateContentRange(ScrollCellPositionInfo info, int pageIndex)
{
var offset = PageSize * pageIndex;
var min = info.ContentPositionRange.x;
var max = info.ContentPositionRange.y + Spacing;
return (min + offset, max + offset);
}
//计算渲染区域范围
private (float min, float max) CalculateContentRangeByScrollPosition(float scrollPosition)
{
var min = scrollPosition;
var max = min + Scroller.ViewportSize;
return (min, max);
}
//是否在渲染范围内
private bool IsInRendererContentRangeByGroupRenderIndex(float contentPosition, int renderBeginIndex,
int renderEndIndex)
{
var startRange = CalculateContentRange(renderBeginIndex);
var endRange = CalculateContentRange(renderEndIndex);
return startRange.min <= contentPosition && endRange.max >= contentPosition;
}
private void UpdateCells(float scrollPosition, int startIndex, int endIndex)
{
var updateCellFlag = false;
_renderIndex.Clear();
//回收无效实例
PoolGroup.RecycleInvalid(startIndex, endIndex, _renderIndex);
for (int i = startIndex; i <= endIndex; i++)
{
//已处理,跳过
if (_renderIndex.Contains(i))
{
continue;
}
var dataIndex = GetDataIndexByRenderIndex(i);
var data = ItemsSource[dataIndex];
var cellInfo = CellInfos[dataIndex];
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 Vector3 CalculatePosition(float scrollPosition, int renderIndex)
{
var pageIndex = GetPageIndexByRenderIndex(renderIndex); //页码索引
var offsetLength = pageIndex * PageSize - scrollPosition; //计算偏移坐标
var dataIndex = GetDataIndexByRenderIndex(renderIndex); //数据索引
var posInfo = PositionInfos[dataIndex];
var offset = IsHorizontal ? offsetLength * Vector3.right : offsetLength * Vector3.down;
return CalculateCellLocalPosition(posInfo.LocalPosition + offset); //实际坐标
}
//计算最近索引滚动坐标
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);
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 int GetRenderIndexByDataIndex(int dataIndex, int pageIndex)
{
if (pageIndex >= 0)
{
return pageIndex * ItemsSource.Count + dataIndex;
}
return (pageIndex * ItemsSource.Count) + dataIndex + 1;
}
///
/// 滚动到指定位置
///
/// 滚动值
/// 滚动时间(单位秒)
/// 缓动曲线
/// 完成回调
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 contentLength = Scroller.ViewportSize;
var unitSize = cellSize + Spacing;
var count = Mathf.CeilToInt(contentLength / unitSize) + 1;
//生成预览实例
var cellPoints = new List();
var offsetDirection = IsHorizontal ? Vector3.right : Vector3.down;
var defaultOffset = IsHorizontal ? Vector3.right * HeadOffset : Vector3.down * HeadOffset;
var localPos = defaultOffset;
for (int i = 0; i < count; i++)
{
if (i != 0)
{
//补充间隔
localPos += offsetDirection * Spacing;
}
cellPoints.Add(localPos);
//累积元素大小
localPos += offsetDirection * cellSize;
}
//计算滚动视窗大小
var scrollRange = CalculatePreviewScrollRange(cellPoints.Count);
//修正偏移位置
var scrollOffset = Mathf.Lerp(scrollRange.min, scrollRange.max, PreviewScrollPercent);
cellPoints = cellPoints.Select(e => e - offsetDirection * scrollOffset).ToList();
//实例化预览元素
InstantiatePreviewCells(cellPoints);
}
private (float min, float max) CalculatePreviewScrollRange(int count)
{
var cellSize = IsHorizontal ? CellSize.x : CellSize.y;
var totalSpacing = count * Spacing;
var contentLength = cellSize * count + totalSpacing;
var min = 0;
var max = contentLength - Scroller.ViewportSize;
return (min, max);
}
#endif
}
}