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 } }