using System;
using System.Collections.Generic;
using System.Linq;
using EasingCore;
using UnityEngine;
namespace TyphoonUI
{
///
/// 滚动视图(单向排列)(默认模式)
///
public class UIScrollRect : UIScrollView
{
private const int EXPAND_RENDERER_CELL_COUNT = 1; //拓展渲染数量
[Header("布局参数")] public RectOffset Padding; //边界范围
public float Spacing; //元素间隔
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 startIndex = CalculateStartIndex(scrollPosition); //起始元素索引
var visibleContent = CalculateVisibleContentRange(scrollPosition); //可见范围
var minIndex =
Mathf.Clamp(startIndex - EXPAND_RENDERER_CELL_COUNT, 0, ItemsSource.Count - 1); //向前偏移2个元素开始渲染
var maxIndex = minIndex;
for (int i = minIndex; i < ItemsSource.Count; i++)
{
var info = PositionInfos[i];
maxIndex = i;
if (info.ContentPositionRange.y > visibleContent.max)
{
break;
}
}
//往后拓展渲染元素
maxIndex = Mathf.Clamp(maxIndex + EXPAND_RENDERER_CELL_COUNT, 0, ItemsSource.Count - 1);
//更新渲染元素
UpdateCells(scrollPosition, minIndex, maxIndex);
}
//重建元素坐标信息
protected override List RebuildCellPositionInfo()
{
var result = new List();
if (DataCount <= 0)
{
return result;
}
//更新元素大小
var index = 0;
var horizontal = Scroller.ScrollDirection == ScrollDirection.Horizontal;
float pos = horizontal ? Padding.left : Padding.top;
var localPosOffset = horizontal ? Vector3.down * Padding.top : Vector3.right * Padding.left;
//分配元素在内容框的范围
foreach (var element in CellInfos)
{
var cellSize = element.CellSize;
pos += index > 0 ? Spacing : 0; //补充spacing
var min = pos;
pos += horizontal ? cellSize.x : cellSize.y; //补充cell大小
var max = pos;
var localPos = horizontal ? Vector3.right * min : Vector3.down * min;
localPos += localPosOffset;
//计算元素localPosition
result.Add(new ScrollCellPositionInfo()
{
ContentPositionRange = new Vector2(min, max),
LocalPosition = localPos,
});
index += 1;
}
return result;
}
//重建内容框长度
protected override float RebuildContentLength()
{
var result = 0f;
if (DataCount <= 0)
{
return result;
}
if (IsHorizontal)
{
//横向长度
result += Padding.horizontal;
foreach (var element in CellInfos)
{
result += element.CellSize.x;
}
//补充间隔大小
result += (ItemsSource.Count - 1) * Spacing;
}
else
{
//垂直长度
result += Padding.vertical;
foreach (var element in CellInfos)
{
result += element.CellSize.y;
}
result += (ItemsSource.Count - 1) * Spacing;
}
return result;
}
//重建滚动范围
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 contentPos = scrollPosition;
if (contentPos < 0)
{
//从0开始
return 0;
}
var startIndex = 0;
var endIndex = PositionInfos.Count - 1;
//不足2个跳出检查
while ((endIndex - startIndex) > 2)
{
//二分法查找最近的起始元素
var midIndex = (int)Mathf.Lerp(startIndex, endIndex, 0.5f);
if (IsContainsContentPos(startIndex, midIndex, contentPos))
{
endIndex = midIndex;
}
else
{
startIndex = midIndex;
}
}
//从最后一个元素往前找,找到第一个小于content pos的元素
for (int i = endIndex; i >= startIndex; i--)
{
var posInfo = PositionInfos[endIndex];
if (posInfo.ContentPositionRange.y < contentPos)
{
return i;
}
}
//找不到,取最前的索引
return startIndex;
}
//计算吸附坐标
public override SnapResult CalculateSnapPosition(float scrollPosition)
{
var snapPos = 0f;
var snapIndex = -1;
var dataIndex = -1;
if (DataCount <= 0)
{
return new SnapResult(snapPos, snapIndex, dataIndex);
}
scrollPosition = ClampScrollPosition(scrollPosition);
var startIndex = CalculateStartIndex(scrollPosition) - 1;
startIndex = Mathf.Clamp(startIndex, 0, ItemsSource.Count - 1);
var temOffset = float.MaxValue;
for (int i = startIndex; i < ItemsSource.Count; i++)
{
var scrollPos = CalculateScrollPositionFromIndex(i, SnapAlignment, SnapAnchor);
var offset = Mathf.Abs(scrollPosition - scrollPos); //偏移量
if (offset < temOffset)
{
snapIndex = i;
temOffset = offset;
snapPos = scrollPos;
}
else
{
break;
}
}
dataIndex = snapIndex;
return new SnapResult(snapPos, snapIndex, dataIndex);
}
//计算可见元素的Content范围
private (float min, float max) CalculateVisibleContentRange(float scrollPosition)
{
var min = scrollPosition;
var max = min + Scroller.ViewportSize;
return (min, max);
}
private bool IsContainsContentPos(int startIndex, int endIndex, float contentPos)
{
var range = CalculateContentRange(startIndex, endIndex);
return contentPos >= range.x && contentPos <= range.y;
}
//计算元素Content 范围
private Vector2 CalculateContentRange(int startIndex, int endIndex)
{
var min = PositionInfos[startIndex].ContentPositionRange.x;
var max = PositionInfos[endIndex].ContentPositionRange.y;
if (startIndex == 0)
{
//第一个元素取最小值
min = float.MinValue;
}
if (endIndex == PositionInfos.Count - 1)
{
//最后一个元素,取最大值
max = float.MaxValue;
}
else
{
max += Spacing;
}
return new Vector2(min, max);
}
//更新元素
private void UpdateCells(float scrollPosition, int startIndex, int endIndex)
{
var updateCellFlag = false;
// Debug.Log($"{startIndex}->{endIndex}");
_renderIndex.Clear();
//回收无效实例
PoolGroup.RecycleInvalid(startIndex, endIndex, _renderIndex);
for (int i = startIndex; i <= endIndex; 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 Vector3 CalculatePosition(float scrollPosition, int cellIndex)
{
var posInfo = PositionInfos[cellIndex];
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);
}
///
/// 计算指定元素对应的滚动坐标
/// (返回理想坐标,可能超出滚动范围,可用 ClampScrollPosition 卡有效滚动范围)
///
/// 数据索引
/// 对齐系数,范围:[0,1],0:靠左/上,1:靠右/下
/// 锚点系数,范围:[0,1],0:元素左/上作为锚点,1:元素右/下作为锚点
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 scrollPos = posInfo.ContentPositionRange.x;
var size = posInfo.ContentPositionRange.y - posInfo.ContentPositionRange.x;
var offset = 0f;
offset -= Scroller.ViewportSize * alignment;
offset += Mathf.Lerp(0, size, Mathf.Clamp01(anchor));
scrollPos += offset;
return scrollPos;
}
///
/// 限制滚动值在有效范围内
///
/// 滚动值
public float ClampScrollPosition(float scrollPosition)
{
return Mathf.Clamp(scrollPosition, ScrollRange.x, ScrollRange.y);
}
///
/// 滚动到指定位置
///
/// 滚动值
/// 滚动时间(单位秒)
/// 缓动曲线
/// 完成回调
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 totalContentSize = Scroller.ViewportSize;
var contentLength = totalContentSize - (IsHorizontal ? Padding.horizontal : Padding.vertical);
var unitSize = cellSize + Spacing;
var count = Mathf.CeilToInt(contentLength / unitSize) + 1;
//生成预览实例
var cellPoints = new List();
var offsetDirection = IsHorizontal ? Vector3.right : Vector3.down;
var defaultOffset = Vector3.right * Padding.left + Vector3.down * Padding.top;
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 = Mathf.Clamp(count - 1, 0, int.MaxValue) * Spacing;
var contentLength = cellSize * count + totalSpacing;
contentLength += IsHorizontal ? Padding.horizontal : Padding.vertical;
var min = 0;
var max = contentLength - Scroller.ViewportSize;
return (min, max);
}
#endif
}
}