using System; using System.Collections.Generic; using System.Linq; using EasingCore; using UnityEngine; namespace TyphoonUI { /// /// 滚动视图(网格排列)(循环模式) /// public class UIGridViewLoop : UIScrollView { private const int EXPAND_RENDERER_GROUP_COUNT = 0; //向外渲染多少组元素 [Header("布局参数")] public Vector2 Spacing; //元素间隔 public float HeadOffset = 0; //子元素坐标偏移量 public GridViewConstraintMode Constraint = GridViewConstraintMode.Flexible; //默认自适应模式 public int FixedCount = 3; //固定数量 public List Groups { get; private set; } = new List(); //分组信息 private readonly HashSet _renderIndex = new HashSet(); //渲染索引 private float PageSize { get; set; } //单页大小 private bool IsHorizontal => Scroller.ScrollDirection == ScrollDirection.Horizontal; //是否为横向滚动 //更新视图 protected override void UpdateView(float scrollPosition, bool force) { if (DataCount <= 0) { return; } //计算渲染范围 var range = CalculateContentRange(scrollPosition); var startRenderGroupIndex = CalculateStartIndex(scrollPosition); //开始渲染组的索引 //拓展渲染范围 var startIndex = startRenderGroupIndex - EXPAND_RENDERER_GROUP_COUNT; var limitMax = startIndex + Groups.Count - 1; var endIndex = startIndex; for (int i = startIndex + 1; i <= limitMax; i++) { var contentRange = CalculateRendererContentRangeByGroupRenderIndex(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; } //固定数量不可小于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 = HeadOffset; 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); } } var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y; PageSize = Groups.Last().ContentPositionRange.y + groupSpacing; //计算分页大小 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 startRange = GetGroupRenderIndexRangeByPageIndex(beginPage); //起始页组索引范围 var endRange = GetGroupRenderIndexRangeByPageIndex(endPage); //结束页组索引范围 var beginGroup = startRange.begin; //起始组 var endGroup = endRange.end; //结束组 while ((endGroup - beginGroup) > 1) { var mid = (int)Mathf.Lerp(beginGroup, endGroup, 0.5f); if (IsInRendererContentRangeByGroupRenderIndex(scrollPosition, beginGroup, mid)) { endGroup = mid; } else { beginGroup = mid; } } return beginGroup; } //计算最佳吸附位置 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 groupStart = CalculateStartIndex(checkScrollPosition); //从三组元素中找最接近的元素 var cellStart = GetCellRenderIndexRangeByGroupRenderIndex(groupStart - 1).begin; var cellEnd = GetGroupRenderIndexRangeByPageIndex(groupStart + 1).end; var nearestPos = CalculateCellContentPosition(cellStart, SnapAnchor); var distance = Mathf.Abs(nearestPos - checkScrollPosition); for (int i = cellStart + 1; i <= cellEnd; 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 = GetDataIndexByCellRenderIndex(snapIndex); return new SnapResult(snapPos, snapIndex, dataIndex); } //重建分组(自适应模式) private List RebuildGroupInfosFlexible() { var groups = new List(); //元素大小长度(横向滚动以高作为限制区域,垂直滚动以宽作为限制区域) var cellSizeLength = IsHorizontal ? Scroller.ViewportHeight : Scroller.ViewportWidth; cellSizeLength -= HeadOffset; var cellSpacing = IsHorizontal ? Spacing.y : Spacing.x; //元素间隔 var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y; //组间隔 var index = 0; var occupied = 0f; //占用的高度 var counter = 0; //计数器 foreach (var element in CellInfos) { //横向滚动大小取高,垂直滚动大小取宽 var size = IsHorizontal ? element.CellSize.y : element.CellSize.x; occupied += size; if (counter > 0) { occupied += cellSpacing; //补充spacing } if (occupied > cellSizeLength) { var beginIndex = -1; var endIndex = index - 1; //取上一个 var contentStart = 0f; 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 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 = 0f; //计算分组 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; } //获取页索引,传入scrollPosition private int GetPageIndexByScrollPosition(float scrollPosition) { if (DataCount <= 0) { return 0; } return (int)(scrollPosition / PageSize); } //组渲染索引->页码索引 private int GetPageIndexByGroupRenderIndex(int groupRenderIndex) { return groupRenderIndex >= 0 ? groupRenderIndex / Groups.Count : -Mathf.CeilToInt((float)Mathf.Abs(groupRenderIndex) / Groups.Count); } //组渲染索引->组索引 private int GetGroupIndexByGroupRenderIndex(int groupRenderIndex) { if (groupRenderIndex < 0) { groupRenderIndex = groupRenderIndex % Groups.Count + Groups.Count; } return groupRenderIndex % Groups.Count; } //计算渲染区域范围 private (float min, float max) CalculateRendererContentRangeByGroupRenderIndex(int groupRenderIndex) { //计算页码索引 var pageIndex = GetPageIndexByGroupRenderIndex(groupRenderIndex); var index = GetGroupIndexByGroupRenderIndex(groupRenderIndex); var group = Groups[index]; return CalculateRendererContentRange(group, pageIndex); } //计算渲染区域范围 private (float min, float max) CalculateRendererContentRange(ScrollCellGroup group, int pageIndex) { var offset = PageSize * pageIndex; var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y; var min = group.ContentPositionRange.x; var max = group.ContentPositionRange.y + groupSpacing; return (min + offset, max + offset); } //是否在渲染范围内 private bool IsInRendererContentRangeByGroupRenderIndex(float contentPosition, int groupRenderBeginIndex, int groupRenderEndIndex) { var startRange = CalculateRendererContentRangeByGroupRenderIndex(groupRenderBeginIndex); var endRange = CalculateRendererContentRangeByGroupRenderIndex(groupRenderEndIndex); return startRange.min <= contentPosition && endRange.max >= contentPosition; } //是否在组范围内 private bool IsInGroupContentRange(float contentPosition, int startGroupIndex, int endGroupIndex) { var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y; var groupStart = Groups[startGroupIndex]; var groupEnd = Groups[endGroupIndex]; return groupStart.ContentPositionRange.x <= contentPosition && contentPosition <= groupEnd.ContentPositionRange.y + groupSpacing; } //计算渲染区域范围 private (float min, float max) CalculateContentRange(float scrollPosition) { var min = scrollPosition; var max = min + Scroller.ViewportSize; return (min, max); } //计算组渲染范围 private (int begin, int end) GetGroupRenderIndexRangeByPageIndex(int pageIndex) { var begin = Groups.Count * pageIndex; var end = begin + Groups.Count - 1; return (begin, end); } //计算元素渲染范围 private (int begin, int end) GetCellRenderIndexRangeByGroupRenderIndex( int groupRenderIndex) { var groupIndex = GetGroupIndexByGroupRenderIndex(groupRenderIndex); var pageIndex = GetPageIndexByGroupRenderIndex(groupRenderIndex); var group = Groups[groupIndex]; var beginDataIndex = group.BeginDataIndex; var endDataIndex = group.EndDataIndex; var indexOffset = pageIndex * ItemsSource.Count; return (beginDataIndex + indexOffset, endDataIndex + indexOffset); } //获取数据索引 public int GetDataIndexByCellRenderIndex(int cellRenderIndex) { if (cellRenderIndex < 0) { cellRenderIndex = cellRenderIndex % ItemsSource.Count + ItemsSource.Count; } return cellRenderIndex % ItemsSource.Count; } //获取页码索引 private int GetPageIndexByCellRenderIndex(int cellRenderIndex) { return cellRenderIndex >= 0 ? cellRenderIndex / ItemsSource.Count : -Mathf.CeilToInt((float)Mathf.Abs(cellRenderIndex) / ItemsSource.Count); } //计算元素坐标 private Vector3 CalculatePosition(float scrollPosition, int cellRenderIndex) { var pageIndex = GetPageIndexByCellRenderIndex(cellRenderIndex); //页码索引 var offsetLength = pageIndex * PageSize - scrollPosition; //计算偏移坐标 var dataIndex = GetDataIndexByCellRenderIndex(cellRenderIndex); //数据索引 var posInfo = PositionInfos[dataIndex]; var offset = IsHorizontal ? offsetLength * Vector3.right : offsetLength * Vector3.down; return CalculateCellLocalPosition(posInfo.LocalPosition + offset); //实际坐标 } //渲染元素 private void UpdateCells(float scrollPosition, int beginIndex, int endIndex) { var updateCellFlag = false; //计算元素渲染索引 var dataRenderBeginRange = GetCellRenderIndexRangeByGroupRenderIndex(beginIndex); var dataRenderEndRange = GetCellRenderIndexRangeByGroupRenderIndex(endIndex); var cellRenderBeginIndex = dataRenderBeginRange.begin; var cellRenderEndIndex = dataRenderEndRange.end; _renderIndex.Clear(); //清空渲染索引 PoolGroup.RecycleInvalid(cellRenderBeginIndex, cellRenderEndIndex, _renderIndex); for (int i = cellRenderBeginIndex; i <= cellRenderEndIndex; i++) { //已处理,跳过 if (_renderIndex.Contains(i)) { continue; } var dataIndex = GetDataIndexByCellRenderIndex(i); var data = ItemsSource[dataIndex]; var cellInfo = CellInfos[dataIndex]; //更新content 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 float CalculateCellContentPosition(int cellRenderIndex, float anchor) { var pageIndex = GetPageIndexByCellRenderIndex(cellRenderIndex); //页码索引 var offset = pageIndex * PageSize; //计算偏移坐标 var dataIndex = GetDataIndexByCellRenderIndex(cellRenderIndex); //数据索引 var posInfo = PositionInfos[dataIndex]; var min = posInfo.ContentPositionRange.x + offset; var max = posInfo.ContentPositionRange.y + offset; return Mathf.Lerp(min, max, Mathf.Clamp01(anchor)); } //计算最近索引滚动坐标 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); //只从相邻的5页去找 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 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 groupSpacing = IsHorizontal ? Spacing.x : Spacing.y; var groupLength = IsHorizontal ? Scroller.ViewportHeight : Scroller.ViewportWidth; //组长度 groupLength -= HeadOffset; //去除留白区域 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 contentLength = Scroller.ViewportSize; 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 = Vector3.zero; //组默认偏移 var cellOffset = groupCellDirection * HeadOffset; for (int i = 0; i < groupNum; i++) { //组偏移位置 var groupStartPoint = (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 = groupNum * groupSpacing; var contentLength = cellSize * groupNum + totalSpacing; var min = 0; var max = contentLength - Scroller.ViewportSize; return (min, max); } #endif } }