using System; using System.Collections.Generic; using System.Linq; using EasingCore; using UnityEngine; namespace TyphoonUI { /// /// 滚动视图(网格排列)(默认模式) /// public class UIGridView : UIScrollView { private const int EXPAND_RENDERER_CELL_COUNT = 1; //拓展渲染数量 [Header("布局参数")] public RectOffset Padding; //留白区域 public Vector2 Spacing; //元素间隔 public GridViewConstraintMode Constraint = GridViewConstraintMode.Flexible; //默认自适应模式 public int FixedCount = 3; //固定数量 private List _groups = new List(); //分组信息 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 rendererRange = CalculateRendererContentRange(scrollPosition); var startIndex = CalculateStartIndex(scrollPosition); var beginIndex = Mathf.Clamp(startIndex - EXPAND_RENDERER_CELL_COUNT, 0, int.MaxValue); var endIndex = beginIndex; for (int i = beginIndex; i < _groups.Count; i++) { var group = _groups[i]; if (group.ContentPositionRange.x > rendererRange.max) { //超出渲染范围 break; } endIndex = i; } //往后拓展渲染元素 endIndex = Mathf.Clamp(endIndex + EXPAND_RENDERER_CELL_COUNT, 0, _groups.Count - 1); //更新渲染元素 UpdateCells(scrollPosition, startIndex, endIndex); } protected override List RebuildCellPositionInfo() { var result = new List(); _groups = new List(); 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 = IsHorizontal ? (float)Padding.top : Padding.left; 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); } } return result; } protected override float RebuildContentLength() { if (DataCount <= 0) { return 0; } var end = _groups.Last(); var tail = IsHorizontal ? Padding.right : Padding.bottom; return end.ContentPositionRange.y + tail; } //滚动范围 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 pos = ClampScrollPosition(scrollPosition); //二分法先查组,再查元素 var startIndex = 0; var endIndex = _groups.Count - 1; while ((endIndex - startIndex) > 1) { //二分法查找最近的组 var midIndex = (int)Mathf.Lerp(startIndex, endIndex, 0.5f); if (IsContainsGroupContentPositionRange(pos, startIndex, midIndex)) { endIndex = midIndex; } else { startIndex = midIndex; } } return startIndex; } //限制滚动坐标在有效范围内 public float ClampScrollPosition(float scrollPos) { return Mathf.Clamp(scrollPos, ScrollRange.x, ScrollRange.y); } //判断是否在组 private bool IsContainsGroupContentPositionRange(float contentPos, int groupStartIndex, int groupEndIndex) { var spacing = IsHorizontal ? Spacing.x : Spacing.y; var min = _groups[groupStartIndex].ContentPositionRange.x; var max = _groups[groupEndIndex].ContentPositionRange.y; if (groupStartIndex == 0) { min = float.MinValue; } if (groupEndIndex == _groups.Count - 1) { max = float.MaxValue; } else { max += spacing; } return min <= contentPos && max >= contentPos; } //计算最靠近的元素位置 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 cellDataIndex = CalculateSnapNearestCell(scrollPosition, SnapAlignment, SnapAnchor); snapIndex = cellDataIndex; var posInfo = PositionInfos[cellDataIndex]; var contentPosition = CalculateContentPosition(posInfo, SnapAnchor); snapPos = contentPosition - Scroller.ViewportSize * Mathf.Clamp01(SnapAlignment); dataIndex = cellDataIndex; return new SnapResult(snapPos, snapIndex, dataIndex); } //重建分组(自适应模式) private List RebuildGroupInfosFlexible() { var groups = new List(); //元素大小长度(横向滚动以高作为限制区域,垂直滚动以宽作为限制区域) var cellSizeLength = IsHorizontal ? Scroller.ViewportHeight : Scroller.ViewportWidth; cellSizeLength -= IsHorizontal ? Padding.vertical : Padding.horizontal; var cellSpacing = IsHorizontal ? Spacing.y : Spacing.x; //元素间隔 var groupSpacing = IsHorizontal ? Spacing.x : Spacing.y; //组间隔 var paddingHead = IsHorizontal ? (float)Padding.left : (float)Padding.top; var index = 0; var occupied = 0f; //占用的高度 var counter = 0; //计数器 foreach (var data in CellInfos) { //横向滚动大小取高,垂直滚动大小取宽 var size = IsHorizontal ? data.CellSize.y : data.CellSize.x; occupied += size; if (counter > 0) { occupied += cellSpacing; //补充spacing } if (occupied > cellSizeLength) { var beginIndex = -1; var endIndex = index - 1; //取上一个 var contentStart = paddingHead; 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 paddingHead = IsHorizontal ? (float)Padding.left : (float)Padding.top; 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 = paddingHead; //计算分组 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; } //计算渲染区域 private (float min, float max) CalculateRendererContentRange(float scrollPosition) { var min = scrollPosition; var max = min + Scroller.ViewportSize; return (min, max); } //计算坐标 private Vector3 CalculatePosition(float scrollPosition, int dataIndex) { var posInfo = PositionInfos[dataIndex]; 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); } //更新元素 private void UpdateCells(float scrollPosition, int beginIndex, int endIndex) { var updateCellFlag = false; //计算元素索引范围 var groupBegin = _groups[beginIndex]; var groupEnd = _groups[endIndex]; var dataStartIndex = groupBegin.BeginDataIndex; var dataEndIndex = groupEnd.EndDataIndex; _renderIndex.Clear(); PoolGroup.RecycleInvalid(dataStartIndex, dataEndIndex, _renderIndex); for (int i = dataStartIndex; i <= dataEndIndex; 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 int CalculateSnapNearestGroup(float scrollPosition, float alignment, float anchor) { scrollPosition = ClampScrollPosition(scrollPosition); scrollPosition += Scroller.ViewportSize * Mathf.Clamp01(alignment); var startIndex = CalculateStartIndex(scrollPosition); var beginIndex = Mathf.Clamp(startIndex - 1, 0, _groups.Count - 1); var endIndex = Mathf.Clamp(startIndex + 1, 0, _groups.Count - 1); var result = beginIndex; var distance = Mathf.Abs(scrollPosition - CalculateContentPosition(_groups[beginIndex], anchor)); for (int i = beginIndex + 1; i <= endIndex; i++) { var group = _groups[i]; var checkDis = Mathf.Abs(scrollPosition - CalculateContentPosition(group, anchor)); if (checkDis < distance) { result = i; distance = checkDis; } } return result; } //计算最靠近元素 private int CalculateSnapNearestCell(float scrollPosition, float alignment, float anchor) { scrollPosition = ClampScrollPosition(scrollPosition); var contentPos = scrollPosition + Scroller.ViewportSize * Mathf.Clamp01(alignment); var groupIndex = CalculateSnapNearestGroup(scrollPosition, alignment, anchor); //继续判断相邻组的元素 var beginGroupIndex = Mathf.Clamp(groupIndex - 1, 0, _groups.Count - 1); var endGroupIndex = Mathf.Clamp(groupIndex + 1, 0, _groups.Count - 1); var beginGroup = _groups[beginGroupIndex]; var endGroup = _groups[endGroupIndex]; var beginIndex = beginGroup.BeginDataIndex; var endIndex = endGroup.EndDataIndex; var match = beginIndex; var distance = Mathf.Abs(contentPos - CalculateContentPosition(PositionInfos[beginIndex], anchor)); for (int i = beginIndex + 1; i <= endIndex; i++) { var posInfo = PositionInfos[i]; var dis = Mathf.Abs(contentPos - CalculateContentPosition(posInfo, anchor)); if (dis < distance) { match = i; distance = dis; } } return match; } //计算ContentPosition private float CalculateContentPosition(ScrollCellGroup group, float anchor) { return Mathf.Lerp(group.ContentPositionRange.x, group.ContentPositionRange.y, Mathf.Clamp01(anchor)); } //计算ContentPosition private float CalculateContentPosition(ScrollCellPositionInfo info, float anchor) { return Mathf.Lerp(info.ContentPositionRange.x, info.ContentPositionRange.y, Mathf.Clamp01(anchor)); } //计算指定的索引滚动位置 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 contentPosition = CalculateContentPosition(posInfo, Mathf.Clamp01(anchor)); return contentPosition - Scroller.ViewportSize * Mathf.Clamp01(alignment); } /// /// 滚动到指定位置 /// /// 滚动值 /// 滚动时间(单位秒) /// 缓动曲线 /// 完成回调 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 groupSpacing = IsHorizontal ? Spacing.x : Spacing.y; var groupLength = IsHorizontal ? Scroller.ViewportHeight : Scroller.ViewportWidth; //组长度 groupLength -= (IsHorizontal ? Padding.vertical : Padding.horizontal); //去除留白区域 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 totalContentSize = Scroller.ViewportSize; var contentLength = totalContentSize - (IsHorizontal ? Padding.horizontal : Padding.vertical); 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 = IsHorizontal ? Vector3.right * Padding.left : Vector3.down * Padding.top; //组默认偏移 var cellOffset = groupCellDirection * (IsHorizontal ? Padding.top : Padding.left); for (int i = 0; i < groupNum; i++) { //组偏移位置 var groupStartPoint = (i * 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 = Mathf.Clamp(groupNum - 1, 0, int.MaxValue) * groupSpacing; var contentLength = cellSize * groupNum + totalSpacing; contentLength += IsHorizontal ? Padding.horizontal : Padding.vertical; var min = 0; var max = contentLength - Scroller.ViewportSize; return (min, max); } #endif } }