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