// Copyright (c) 2025 EFramework Innovation. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; using UnityEngine.UI; namespace EFramework.Unity.UIPlugin { /// /// UIWrapContent 拓展至 ScrollRect 视图组件,提供了高效的大数据列表和网格显示能力,通过元素复用降低内存占用。 /// /// /// /// 功能特性 /// - 多种布局模式:支持垂直列表、水平列表、垂直网格、水平网格四种布局 /// - 元素复用机制:支持元素复用,只创建可见区域所需的 UI 单元格元素 /// - 适配器支持:提供灵活的适配器接口,便于自定义数据绑定逻辑 /// /// 使用手册 /// 1. 适配器实现 /// /// 1. 简单适配器: /// /// // 创建简单适配器 /// wrapContent.Adapter = new UIWrapContent.ISimpleAdapter( /// wrapContent, /// () => dataList.Count, // 获取数据总数 /// (cell, index) => { /// // 设置单元格数据 /// cell.GetComponent<Text>().text = dataList[index].ToString(); /// } /// ); /// /// 2. 自定义适配器: /// /// // 创建自定义适配器类 /// public class MyAdapter : UIWrapContent.ICustomAdapter /// { /// private List<ItemData> dataList; /// /// public MyAdapter(List<ItemData> dataList) { /// this.dataList = dataList; /// } /// /// public bool SingleZygote => false; // 多种单元格类型 /// /// public int GetCellType(int index) { /// return dataList[index].type; // 根据数据类型返回 /// } /// /// public RectTransform GetCellZygote(int type) => Object.FindAnyObjectByType<UIWrapContent>().content.GetChild(0).GetComponent<RectTransform>(); /// /// public int GetCellCount() { /// return dataList.Count; /// } /// /// public void SetCellData(RectTransform cell, int index) { /// var data = dataList[index]; /// // 根据数据更新UI元素 /// if (GetCellType(index) == 0) { /// // 设置类型A单元格的数据 /// var nameText = cell.Find("Name").GetComponent<Text>(); /// nameText.text = data.name; /// } else { /// // 设置类型B单元格的数据 /// var valueText = cell.Find("Value").GetComponent<Text>(); /// valueText.text = data.value.ToString(); /// } /// } /// } /// /// // 使用自定义适配器 /// wrapContent.Adapter = new MyAdapter(dataList); /// /// 2. 运行时控制 /// /// 1. 滚动控制: /// /// // 滚动到指定位置(0-1范围) /// wrapContent.ScrollTo(0.5f); // 滚动到中间位置 /// wrapContent.ScrollTo(0f); // 滚动到开头 /// wrapContent.ScrollTo(1f); // 滚动到末尾 /// /// 2. 数据更新: /// /// // 重新加载列表 /// wrapContent.Reload(); /// /// 3. 遍历可见项: /// /// // 遍历当前可见的所有单元格 /// wrapContent.EachItem((index, item) => { /// Debug.Log($"可见项: 索引={index}, 名称={item.name}"); /// }); /// /// 常见问题 /// 1. UIWrapContent 组件与 ScrollRect 组件有什么区别? /// /// ScrollRect 会为每个数据项创建一个 UI 单元格元素,当数据量大时会导致性能问题和内存占用过高。 /// UIWrapContent 采用元素复用机制,只创建可见区域所需的 UI 单元格元素,特别适合大数据列表。 /// /// 2. 如何优化 UIWrapContent 组件的性能? /// /// 以下是一些优化建议: /// 1. 减少单元格预制体的复杂度,尽量使用简单的UI结构 /// 2. 适当设置缓冲区大小,避免频繁创建和销毁单元格 /// 3. 在SetCellData方法中避免执行昂贵的操作 /// /// 更多信息请参考模块文档。 /// [AddComponentMenu("UI/Wrap Content")] public class UIWrapContent : ScrollRect { #region data structure /// /// ICustomAdapter 是复杂视图的适配器(支持多种单元格原型)。 /// public interface ICustomAdapter { /// /// SingleZygote 表示是否为单原型模式。 /// bool SingleZygote { get; } /// /// GetCellType 是单元格的类型。 /// 开启时,请返回固定值 /// /// 单元格索引 /// 单元格类型ID int GetCellType(int index); /// /// GetCellZygote 获取单元格的原型。 /// /// 单元格类型 ID /// 对应类型的原型 RectTransform RectTransform GetCellZygote(int type); /// /// GetCellCount 获取单元格的数量。 /// int GetCellCount(); /// /// SetCellData 设置单元格。 /// /// 单元格的 RectTransform 组件 /// 单元格索引 void SetCellData(RectTransform cell, int index); } /// /// ISimpleAdapter 是简单视图的适配器(获取 Content 的第一个子物体作为单元格原型)。 /// public class ISimpleAdapter : ICustomAdapter { private readonly UIWrapContent wrapper; private readonly Func onGetCellCount; private readonly Action onSetCellData; /// /// SingleZygote 表示是否为单原型模式。 /// public bool SingleZygote { get => true; } /// /// GetCellType 获取单元格的类型。 /// 开启时,请返回固定值 /// /// 单元格索引 /// 单元格类型ID,始终为0 public int GetCellType(int index) { return 0; } /// /// GetCellZygote 获取单元格的原型。 /// /// 单元格类型ID /// Content的第一个子物体作为原型 public RectTransform GetCellZygote(int type) { return wrapper.content.GetChild(0).GetComponent(); } /// /// GetCellCount 获取单元格的数量。 /// public int GetCellCount() { return onGetCellCount.Invoke(); } /// /// SetCellData 设置单元格。 /// /// 单元格的 RectTransform 组件 /// 单元格索引 public void SetCellData(RectTransform cell, int index) { onSetCellData.Invoke(cell, index); } /// /// ISimpleAdapter 创建简单视图适配器实例。 /// /// UIWrapContent 实例 /// 获取单元格总数的委托 /// 设置单元格数据的委托 public ISimpleAdapter(UIWrapContent wrapper, Func onGetCellCount, Action onSetCellData) { this.wrapper = wrapper; this.onGetCellCount = onGetCellCount; this.onSetCellData = onSetCellData; } } /// /// CellItem 是单元格扩展。 /// public readonly struct CellItem { /// /// Default 是默认的单元格项,用作返回值的初始状态。 /// public static readonly CellItem Default = new(0, 0, CellInfo.Default); /// /// Index 表示单元格在列表中的索引。 /// public readonly int Index; /// /// Offset 表示单元格的位置偏移。 /// public readonly float Offset; /// /// Cell 是单元格的信息。 /// public readonly CellInfo Cell; /// /// CellItem 创建单元格项实例。 /// /// 单元格索引 /// 位置偏移 /// 单元格信息 public CellItem(int index, float offset, CellInfo cell) { Index = index; Offset = offset; Cell = cell; } /// /// CalcHeight 计算高度。 /// /// 高度 [MethodImpl(MethodImplOptions.AggressiveInlining)] public float CalcHeight() { return Cell.Transform.rect.height; } /// /// CalcWidth 计算宽度。 /// /// 宽度 [MethodImpl(MethodImplOptions.AggressiveInlining)] public float CalcWidth() { return Cell.Transform.rect.width; } } /// /// CellInfo 是单元格的信息。 /// public readonly struct CellInfo { /// /// Default 是默认的单元格信息,用作返回值的初始状态。 /// public static readonly CellInfo Default = new(null, 0); /// /// Transform 是单元格的 RectTransform 组件。 /// public readonly RectTransform Transform; /// /// Type 是单元格的类型 ID。 /// public readonly int Type; /// /// CellInfo 创建单元格信息实例。 /// /// 单元格的 RectTransform 组件 /// 单元格类型ID public CellInfo(RectTransform transform, int type) { Transform = transform; Type = type; } /// /// ToString 将单元格信息字符串化。 /// /// public override string ToString() { return $"{nameof(Transform)}: {Transform}, {nameof(Type)}: {Type}"; } } /// /// CellZygote 是单元格的原型。 /// public readonly struct CellZygote { /// /// Type 是单元格类型 ID。 /// public readonly int Type; /// /// Width 是单元格宽度。 /// public readonly float Width; /// /// Height 是单元格高度。 /// public readonly float Height; /// /// AnchorMin 是锚点最小值。 /// public readonly Vector2 AnchorMin; /// /// AnchorMax 是锚点最大值。 /// public readonly Vector2 AnchorMax; /// /// Pivot 是中心点。 /// public readonly Vector2 Pivot; /// /// Zygote 是单元格原型的 RectTransform 组件。 /// public readonly RectTransform Zygote; /// /// CellZygote 创建单元格原型实例。 /// /// 单元格类型ID /// 单元格宽度 /// 单元格高度 /// 锚点最小值 /// 锚点最大值 /// 中心点 /// 单元格原型的 RectTransform 组件 public CellZygote(int type, float width, float height, Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, RectTransform zygote) { Type = type; Width = width; Height = height; AnchorMin = anchorMin; AnchorMax = anchorMax; Pivot = pivot; Zygote = zygote; } } /// /// LayoutMode 是布局模式。 /// public enum LayoutMode { /// /// VerticalList 为垂直列表。 /// VerticalList, /// /// HorizontalList 为水平列表。 /// HorizontalList, /// /// VerticalGrid 为垂直网格。 /// VerticalGrid, /// /// HorizontalGrid 为水平网格。 /// HorizontalGrid } /// /// LayoutBase 是布局基类。 /// public abstract class LayoutBase { /// /// Recyle 获取或设置循环模块。 /// public virtual RecyleModule Recyle { get; set; } /// /// Vertical 表示是否为垂直滚动。 /// public abstract bool Vertical { get; } /// /// MinCoverage 是最小覆盖率:viewPort * MinCoverage,控制预加载区域的大小,值越大预加载的元素越多。 /// public virtual float MinCoverage => 1.64f; /// /// CalcContentLengthOfSlidingDir 计算内容在滑动方向的长度,此接口开销较大,注意:这里的唯一为正方向距离(总是大于等于0)。 /// /// 内容在滑动方向上的总长度 public abstract float CalcContentLengthOfSlidingDir(); /// /// CalcCellOffset 计算单元格位置偏移,此接口的开销不定,不建议频繁请求。 /// /// 单元格 RectTransform /// 单元格索引 /// 计算出的位置偏移 public abstract Vector2 CalcCellOffset(RectTransform cell, int index); /// /// GetCellZygote 获取单元格原型。 /// /// 单元格类型 ID /// 单元格原型数据 public abstract CellZygote GetCellZygote(int type); /// /// Setup 初始化布局并创建初始可见单元格。 /// public abstract void Setup(); /// /// OnValueChanged 处理滚动值变化事件。 /// /// UIWrapContent 实例 /// 新的滚动值 public abstract void OnValueChanged(UIWrapContent wrapper, Vector2 v); /// /// OnAdapterChanged 处理适配器变化事件。 /// /// UIWrapContent 实例 public abstract void OnAdapterChanged(UIWrapContent wrapper); /// /// ScrollTo 滚动到指定的归一化位置。 /// /// UIWrapContent 实例 /// 归一化位置值(0-1) /// 是否应用归一化值到滚动条,默认为true public abstract void ScrollTo(UIWrapContent wrapper, float normalized, bool applyNormalized = true); } /// /// LayoutHorizontalGrid 是水平网格布局。 /// public class LayoutHorizontalGrid : LayoutBase { /// /// LayoutHorizontalGrid 创建水平网格布局实例。 /// /// 网格的行数 /// 水平间距 /// 垂直间距 public LayoutHorizontalGrid(int row, float horizontalSpace, float verticalSpace) { if (row < 1) row = 1; Row = row; HorizontalSpace = horizontalSpace; VerticalSpace = verticalSpace; } /// /// Row 是网格的行数。 /// public int Row { get; } /// /// HorizontalSpace 是水平间距。 /// public float HorizontalSpace { get; } /// /// VerticalSpace 是垂直间距。 /// public float VerticalSpace { get; } /// /// MinCoverage 为最小覆盖率,用于控制预加载区域大小。 /// public override float MinCoverage => 1.5f; #region 抽象实现 /// /// Vertical 表示是否为垂直滚动,水平网格返回 false。 /// public override bool Vertical => false; /// /// CalcContentLengthOfSlidingDir 计算内容在水平方向的总长度。 /// /// 内容总长度 /// 当适配器不是单一原型模式时抛出异常 public override float CalcContentLengthOfSlidingDir() { var ds = Recyle.Adapter; var count = ds.GetCellCount(); if (count < 1) return 0f; if (!ds.SingleZygote) throw new ArgumentException("LayoutHorizontalGrid is unsupported in multiple zygote mode."); var zygote = Recyle.GetCellZygote(0); var column = (count - 1) / Row + 1; return zygote.Width * column + (column - 1) * HorizontalSpace; } /// /// CalcCellOffset 计算指定索引单元格的位置偏移。 /// /// 单元格 RectTransform /// 单元格索引 /// 计算出的位置偏移 /// 当适配器不是单一原型模式时抛出异常 public override Vector2 CalcCellOffset(RectTransform cell, int index) { if (index < 1) return Vector2.zero; if (!Recyle.Adapter.SingleZygote) throw new ArgumentException("LayoutHorizontalGrid is unsupported in multiple zygote mode."); var zygote = Recyle.GetCellZygote(0); return CalcCellOffset(index, zygote.Width, zygote.Height); } /// /// GetCellZygote 获取单元格原型数据。 /// /// 单元格类型 ID /// 单元格原型数据 public override CellZygote GetCellZygote(int type) { var zygote = Recyle.Adapter.GetCellZygote(type); var obj = zygote.gameObject; obj.SetActive(true); // 等比缩放 var sizeDelta = zygote.sizeDelta; var cellHeight = (Recyle.Content.rect.height - VerticalSpace * (Row + 1)) / Row; var cellWidth = sizeDelta.x / sizeDelta.y * cellHeight; if (obj.scene.IsValid()) obj.SetActive(false); var value = new Vector2(0f, 1); return new CellZygote(type, cellWidth, cellHeight, value, value, value, zygote); } /// /// Setup 初始化布局并创建初始可见单元格。 /// public override void Setup() { var rect = Recyle.Viewport.rect; edge = rect.width * (MinCoverage - 1) * 0.5f; var ds = Recyle.Adapter; if (ds.GetCellCount() < 1) return; var maxX = rect.width * MinCoverage; var count = ds.GetCellCount(); var zygote = Recyle.GetCellZygote(0); var cellHeight = zygote.Height; var cellWidth = zygote.Width; var offset = 0f; var i = 0; while (i < count && (offset < maxX || i < minCellCount)) { var cell = PopCachedPoolPoolAndPushToUsedPool(i, cellWidth, cellHeight); ds.SetCellData(cell.Cell.Transform, i); i++; offset = cell.Offset; if (i % Row == 0) offset += cellWidth; } } /// /// OnValueChanged 处理滚动值变化事件。 /// /// UIWrapContent 实例 /// 新的滚动值 public override void OnValueChanged(UIWrapContent wrapper, Vector2 v) { var normalized = wrapper.horizontalNormalizedPosition; var contentWidth = Recyle.Content.rect.width; var viewportWidth = Recyle.Viewport.rect.width; if (contentWidth <= viewportWidth) return; normalized = Mathf.Clamp(normalized, 0f, 1f); var posX = normalized * (contentWidth - viewportWidth); var dir = posX - prevOffset; if (Mathf.Abs(dir) < minMovingDistance) return; ScrollTo(wrapper, normalized, false); } /// /// OnAdapterChanged 处理适配器变化事件。 /// /// UIWrapContent 实例 public override void OnAdapterChanged(UIWrapContent wrapper) { ScrollTo(wrapper, wrapper.horizontalNormalizedPosition); } /// /// ScrollTo 滚动到指定的归一化位置。 /// /// UIWrapContent 实例 /// 归一化位置值(0-1) /// 是否应用归一化值到滚动条,默认为true public override void ScrollTo(UIWrapContent wrapper, float normalized, bool applyNormalized = true) { var adapter = Recyle.Adapter; var contentWidth = Recyle.Content.rect.width; var viewportWidth = Recyle.Viewport.rect.width; var posX = 0f; if (contentWidth > viewportWidth) { normalized = Mathf.Clamp(normalized, 0f, 1f); posX = normalized * (contentWidth - viewportWidth); } prevOffset = posX; var min = posX - edge; var max = posX + edge + viewportWidth; var zygote = Recyle.GetCellZygote(0); var cellHeight = zygote.Height; var cellWidth = zygote.Width; foreach (var item in Recyle.UsedPool) { var tf = item.Cell.Transform; var edge = tf.anchoredPosition.x + tf.rect.width + HorizontalSpace; if (min <= edge) break; recyclingPool.Add(item); } RecyclingCellFromList(); for (var i = Recyle.UsedPool.Count - 1; i >= 0; i--) { var item = Recyle.UsedPool[i]; var tf = item.Cell.Transform; var edge = tf.anchoredPosition.x; if (max >= edge) break; recyclingPool.Add(item); } RecyclingCellFromList(); var existMinIndex = int.MaxValue; var existMaxIndex = int.MinValue; if (Recyle.UsedPool.Count > 0) { existMinIndex = Recyle.LittleIndexFromUsedPool().Index; existMaxIndex = Recyle.BigIndexFromUsedPool().Index; } var minCol = Mathf.Max((int)(min / (cellWidth + HorizontalSpace)), 0); var offset = cellWidth * minCol; var index = minCol * Row; var cellCount = adapter.GetCellCount(); while (index < cellCount && max >= offset + cellWidth + HorizontalSpace) { // 填充行 var col = index / Row; while (col == index / Row && index < cellCount) { if (existMinIndex <= index && existMaxIndex >= index) { index++; var curCol = index / Row; offset = curCol * (cellWidth + HorizontalSpace); continue; } var cur = PopCachedPoolPoolAndPushToUsedPool(index, cellWidth, cellHeight); adapter.SetCellData(cur.Cell.Transform, index); offset = Mathf.Abs(cur.Offset); index++; } } if (applyNormalized) wrapper.horizontalNormalizedPosition = normalized; } #endregion #region 私有方法 /// /// PopCachedPoolPoolAndPushToUsedPool 从缓存池获取单元格并添加到已使用池。 /// /// 单元格索引 /// 单元格宽度 /// 单元格高度 /// 创建的单元格项 [MethodImpl(MethodImplOptions.AggressiveInlining)] private CellItem PopCachedPoolPoolAndPushToUsedPool(int index, float cellWidth, float cellHeight) { var cell = Recyle.PopFromCachedPool(index); var tf = cell.Transform; var ap = CalcCellOffset(index, cellWidth, cellHeight); tf.anchoredPosition = ap; var cur = new CellItem(index, ap.x, cell); Recyle.PushToUsedPool(cur); return cur; } /// /// RecyclingCellFromList 回收标记为待回收的单元格。 /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RecyclingCellFromList() { if (recyclingPool.Count < 1) return; foreach (var cellExt in recyclingPool) { Recyle.MoveToCachedPool(cellExt); } recyclingPool.Clear(); } /// /// CalcCellOffset 计算单元格在网格中的位置偏移。 /// /// 单元格索引 /// 单元格宽度 /// 单元格高度 /// 计算出的位置偏移 [MethodImpl(MethodImplOptions.AggressiveInlining)] private Vector2 CalcCellOffset(int index, float cellWidth, float cellHeight) { var col = index / Row; return new Vector2( (cellWidth + HorizontalSpace) * col, -(index % Row * (cellHeight + VerticalSpace) + VerticalSpace)); } #endregion #region 私有属性 /// /// minMovingDistance 是最小移动距离,小于此距离的滚动变化将被忽略。 /// private const float minMovingDistance = 1f; /// /// minCellCount 是最小单元格数量,即使未达到视口覆盖区域,也至少创建这么多单元格。 /// private const int minCellCount = 6; /// /// recyclingPool 为待回收单元格列表。 /// private readonly List recyclingPool = new(); /// /// prevOffset 是一次的水平偏移位置。 /// private float prevOffset; /// /// edge 是预加载区域的边缘值。 /// private float edge; #endregion } /// /// LayoutVerticalGrid 是垂直网格布局。 /// public class LayoutVerticalGrid : LayoutBase { /// /// LayoutVerticalGrid 创建垂直网格布局实例。 /// /// 网格的列数 /// 水平间距 /// 垂直间距 public LayoutVerticalGrid(int column, float horizontalSpace, float verticalSpace) { if (column < 1) column = 1; Column = column; HorizontalSpace = horizontalSpace; VerticalSpace = verticalSpace; } /// /// Column 是网格的列数。 /// public int Column { get; } /// /// HorizontalSpace 是水平间距。 /// public float HorizontalSpace { get; } /// /// VerticalSpace 是垂直间距。 /// public float VerticalSpace { get; } /// /// MinCoverage 是最小覆盖率,用于控制预加载区域大小。 /// public override float MinCoverage => 1.5f; #region 抽象实现 /// /// Vertical 表示是否为垂直滚动,垂直网格返回 true。 /// public override bool Vertical => true; /// /// CalcContentLengthOfSlidingDir 计算内容在垂直方向的总长度。 /// /// 内容总长度 /// 当适配器不是单一原型模式时抛出异常 public override float CalcContentLengthOfSlidingDir() { var ds = Recyle.Adapter; var count = ds.GetCellCount(); if (count < 1) return 0f; if (!ds.SingleZygote) throw new ArgumentException("LayoutVerticalGrid is unsupported in multiple zygote mode."); var zygote = Recyle.GetCellZygote(0); var rows = (count - 1) / Column + 1; return zygote.Height * rows + (rows - 1) * VerticalSpace; // [20230111]: 上下不留间隔 } /// /// CalcCellOffset 计算指定索引单元格的位置偏移。 /// /// 单元格RectTransform /// 单元格索引 /// 计算出的位置偏移 /// 当适配器不是单一原型模式时抛出异常 public override Vector2 CalcCellOffset(RectTransform cell, int index) { if (index < 1) return Vector2.zero; if (!Recyle.Adapter.SingleZygote) throw new ArgumentException("LayoutVerticalGrid is unsupported in multiple zygote mode."); var zygote = Recyle.GetCellZygote(0); return CalcCellOffset(index, zygote.Width, zygote.Height); } /// /// GetCellZygote 获取单元格原型数据。 /// /// 单元格类型 ID /// 单元格原型数据 public override CellZygote GetCellZygote(int type) { var zygote = Recyle.Adapter.GetCellZygote(type); var obj = zygote.gameObject; obj.SetActive(true); // 等比缩放 var sizeDelta = zygote.sizeDelta; var cellWidth = (Recyle.Content.rect.width - HorizontalSpace * (Column + 1)) / Column; var cellHeight = sizeDelta.y / sizeDelta.x * cellWidth; if (obj.scene.IsValid()) obj.SetActive(false); var value = new Vector2(0f, 1); return new CellZygote(type, cellWidth, cellHeight, value, value, value, zygote); } /// /// Setup 初始化布局并创建初始可见单元格。 /// public override void Setup() { var rect = Recyle.Viewport.rect; edge = rect.height * (MinCoverage - 1) * 0.5f; var ds = Recyle.Adapter; if (ds.GetCellCount() < 1) return; var maxY = rect.height * MinCoverage; var count = ds.GetCellCount(); var zygote = Recyle.GetCellZygote(0); var cellHeight = zygote.Height; var cellWidth = zygote.Width; var offset = 0f; var i = 0; while (i < count && (offset < maxY || i < minCellCount)) { var cell = PopCachedPoolPoolAndPushToUsedPool(i, cellWidth, cellHeight); ds.SetCellData(cell.Cell.Transform, i); i++; offset = Mathf.Abs(cell.Offset); if (i % Column == 0) offset += cellHeight; } } /// /// OnValueChanged 处理滚动值变化事件。 /// /// UIWrapContent 实例 /// 新的滚动值 public override void OnValueChanged(UIWrapContent wrapper, Vector2 v) { var normalized = wrapper.verticalNormalizedPosition; var contentHeight = Recyle.Content.rect.height; var viewportHeight = Recyle.Viewport.rect.height; if (contentHeight <= viewportHeight) return; normalized = Mathf.Clamp(normalized, 0f, 1f); normalized = 1f - normalized; var posY = normalized * (contentHeight - viewportHeight); var dir = posY - prevOffset; if (Mathf.Abs(dir) < minMovingDistance) return; ScrollTo(wrapper, normalized, false); } /// /// OnAdapterChanged 处理适配器变化事件。 /// /// UIWrapContent 实例 public override void OnAdapterChanged(UIWrapContent wrapper) { ScrollTo(wrapper, 1 - wrapper.verticalNormalizedPosition); } /// /// ScrollTo 滚动到指定的归一化位置。 /// /// UIWrapContent 实例 /// 归一化位置值(0-1) /// 是否应用归一化值到滚动条,默认为 true public override void ScrollTo(UIWrapContent wrapper, float normalized, bool applyNormalized = true) { var adapter = Recyle.Adapter; var contentHeight = Recyle.Content.rect.height; var viewportHeight = Recyle.Viewport.rect.height; var posY = 0f; if (contentHeight > viewportHeight) { normalized = Mathf.Clamp(normalized, 0f, 1f); posY = normalized * (contentHeight - viewportHeight); } prevOffset = posY; var min = posY - edge; var max = posY + edge + viewportHeight; var zygote = Recyle.GetCellZygote(0); var cellHeight = zygote.Height; var cellWidth = zygote.Width; foreach (var item in Recyle.UsedPool) { var tf = item.Cell.Transform; var edge = Mathf.Abs(tf.anchoredPosition.y) + tf.rect.height + VerticalSpace; if (min <= edge) break; recyclingPool.Add(item); } RecyclingCellFromList(); for (var i = Recyle.UsedPool.Count - 1; i >= 0; i--) { var item = Recyle.UsedPool[i]; var tf = item.Cell.Transform; var edge = Mathf.Abs(tf.anchoredPosition.y); if (max >= edge) break; recyclingPool.Add(item); } RecyclingCellFromList(); var existMinIndex = int.MaxValue; var existMaxIndex = int.MinValue; if (Recyle.UsedPool.Count > 0) { existMinIndex = Recyle.LittleIndexFromUsedPool().Index; existMaxIndex = Recyle.BigIndexFromUsedPool().Index; } var minRow = Mathf.Max((int)(min / (cellHeight + VerticalSpace)), 0); var offset = (cellHeight + VerticalSpace) * minRow; var index = minRow * Column; var cellCount = adapter.GetCellCount(); while (index < cellCount && max >= offset + cellHeight + VerticalSpace) { // 填充列 var row = index / Column; while (row == index / Column && index < cellCount) { if (existMinIndex <= index && existMaxIndex >= index) { index++; var curRow = index / Column; offset = curRow * (cellHeight + VerticalSpace); continue; } var cur = PopCachedPoolPoolAndPushToUsedPool(index, cellWidth, cellHeight); adapter.SetCellData(cur.Cell.Transform, index); offset = Mathf.Abs(cur.Offset); index++; } } if (applyNormalized) wrapper.verticalNormalizedPosition = 1f - normalized; } #endregion #region 私有方法 /// /// PopCachedPoolPoolAndPushToUsedPool 从缓存池获取单元格并添加到已使用池。 /// /// 单元格索引 /// 单元格宽度 /// 单元格高度 /// 创建的单元格项 [MethodImpl(MethodImplOptions.AggressiveInlining)] private CellItem PopCachedPoolPoolAndPushToUsedPool(int index, float cellWidth, float cellHeight) { var cell = Recyle.PopFromCachedPool(index); var tf = cell.Transform; var ap = CalcCellOffset(index, cellWidth, cellHeight); tf.anchoredPosition = ap; var cur = new CellItem(index, ap.y, cell); Recyle.PushToUsedPool(cur); return cur; } /// /// RecyclingCellFromList 回收标记为待回收的单元格。 /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RecyclingCellFromList() { if (recyclingPool.Count < 1) return; foreach (var cellExt in recyclingPool) { Recyle.MoveToCachedPool(cellExt); } recyclingPool.Clear(); } /// /// CalcCellOffset 计算单元格在网格中的位置偏移。 /// /// 单元格索引 /// 单元格宽度 /// 单元格高度 /// 计算出的位置偏移 [MethodImpl(MethodImplOptions.AggressiveInlining)] private Vector2 CalcCellOffset(int index, float cellWidth, float cellHeight) { var row = index / Column; return new Vector2( index % Column * (cellWidth + HorizontalSpace) + HorizontalSpace, -(cellHeight + VerticalSpace) * row); } #endregion #region 私有属性 /// /// minMovingDistance 是最小移动距离,小于此距离的滚动变化将被忽略。 /// private const float minMovingDistance = 1f; /// /// minCellCount 是最小单元格数量,即使未达到视口覆盖区域,也至少创建这么多单元格。 /// private const int minCellCount = 6; /// /// recyclingPool 是待回收单元格列表。 /// private readonly List recyclingPool = new(); /// /// prevOffset 是上一次的垂直偏移位置。 /// private float prevOffset; /// /// edge 是预加载区域的边缘值。 /// private float edge; #endregion } /// /// LayoutHorizontalList 是水平列表布局。 /// public class LayoutHorizontalList : LayoutBase { /// /// LayoutHorizontalList 创建水平列表布局实例。 /// /// 单元格间距 /// 左侧内边距 /// 右侧内边距 public LayoutHorizontalList(float space, float leftPadding, float rightPadding) { Space = space; LeftPadding = leftPadding; RightPadding = rightPadding; } /// /// Space 是单元格间距。 /// public float Space { get; } /// /// LeftPadding 是左侧内边距。 /// public float LeftPadding { get; } /// /// RightPadding 是右侧内边距。 /// public float RightPadding { get; } #region 抽象实现 /// /// Vertical 表示是否为垂直滚动,水平列表返回 false。 /// public override bool Vertical => false; /// /// CalcContentLengthOfSlidingDir 计算内容在水平方向的总长度。 /// /// 内容总长度 public override float CalcContentLengthOfSlidingDir() { var ds = Recyle.Adapter; var count = ds.GetCellCount(); if (count < 1) return 0f; if (ds.SingleZygote) { var zygote = Recyle.GetCellZygote(0); return zygote.Width * count + Space * (count - 1) + LeftPadding + RightPadding; } var offset = 0f; for (var i = 0; i < count; i++) { offset += Recyle.GetCellZygote(i).Width + (i > 0 ? Space : 0); } return offset + LeftPadding + RightPadding; } /// /// CalcCellOffset 计算指定索引单元格的位置偏移。 /// /// 单元格 RectTransform /// 单元格索引 /// 计算出的位置偏移 public override Vector2 CalcCellOffset(RectTransform cell, int index) { if (index < 1) return Vector2.zero; var ds = Recyle.Adapter; if (ds.SingleZygote) { var zygote = Recyle.GetCellZygote(0); return new Vector2((zygote.Width + Space) * index, 0); } var offset = LeftPadding; for (var i = 0; i < index; i++) { offset += Recyle.GetCellZygote(i).Width + Space; } return new Vector2(offset, 0); } /// /// GetCellZygote 获取单元格原型数据。 /// /// 单元格类型 ID /// 单元格原型数据 public override CellZygote GetCellZygote(int type) { var zygote = Recyle.Adapter.GetCellZygote(type); var obj = zygote.gameObject; obj.SetActive(true); // 等比缩放 var sizeDelta = zygote.sizeDelta; var cellHeight = Recyle.Content.rect.height; var cellWidth = sizeDelta.x / sizeDelta.y * cellHeight; if (obj.scene.IsValid()) obj.SetActive(false); var value = new Vector2(0, 0.5f); return new CellZygote(type, cellWidth, cellHeight, value, value, value, zygote); } /// /// Setup 初始化布局并创建初始可见单元格。 /// public override void Setup() { var rect = Recyle.Viewport.rect; edge = rect.width * (MinCoverage - 1) * 0.5f; var ds = Recyle.Adapter; if (ds.GetCellCount() < 1) return; var maxX = rect.width * MinCoverage; var offset = LeftPadding; var i = 0; var count = ds.GetCellCount(); while (i < count && (offset < maxX || i < minCellCount)) { var cell = Recyle.PopFromCachedPool(i); var ap = cell.Transform.anchoredPosition; ap.x = offset; cell.Transform.anchoredPosition = ap; var item = new CellItem(i, ap.x, cell); Recyle.PushToUsedPool(item); ds.SetCellData(cell.Transform, i); offset += cell.Transform.rect.width + Space; i++; } } /// /// OnValueChanged 处理滚动值变化事件。 /// /// UIWrapContent 实例。 /// 新的滚动值。 public override void OnValueChanged(UIWrapContent wrapper, Vector2 v) { var normalized = wrapper.horizontalNormalizedPosition; var contentWidth = Recyle.Content.rect.width; var viewportWidth = Recyle.Viewport.rect.width; if (contentWidth <= viewportWidth) return; normalized = Mathf.Clamp(normalized, 0f, 1f); var posX = normalized * (contentWidth - viewportWidth); var dir = posX - prevOffset; if (Mathf.Abs(dir) < minMovingDistance) return; ScrollTo(wrapper, normalized, false); } /// /// OnAdapterChanged 处理适配器变化事件。 /// /// UIWrapContent 实例。 public override void OnAdapterChanged(UIWrapContent wrapper) { ScrollTo(wrapper, wrapper.horizontalNormalizedPosition); } /// /// ScrollTo 滚动到指定的归一化位置。 /// /// UIWrapContent 实例。 /// 归一化位置值(0-1)。 /// 是否应用归一化值到滚动条,默认为 true。 public override void ScrollTo(UIWrapContent wrapper, float normalized, bool applyNormalized = true) { var contentWidth = Recyle.Content.rect.width; var viewportWidth = Recyle.Viewport.rect.width; var posX = 0f; if (contentWidth > viewportWidth) { normalized = Mathf.Clamp(normalized, 0f, 1f); posX = normalized * (contentWidth - viewportWidth); } prevOffset = posX; var min = posX - edge; var max = posX + edge + viewportWidth; foreach (var item in Recyle.UsedPool) { var tf = item.Cell.Transform; var edge = Mathf.Abs(tf.anchoredPosition.y) + tf.rect.width + Space; if (min <= edge) break; recyclingPool.Add(item); } RecyclingCellFromList(); for (var i = Recyle.UsedPool.Count - 1; i >= 0; i--) { var item = Recyle.UsedPool[i]; var tf = item.Cell.Transform; var edge = Mathf.Abs(tf.anchoredPosition.y); if (max >= edge) break; recyclingPool.Add(item); } RecyclingCellFromList(); var existMinIndex = int.MaxValue; var existMaxIndex = int.MinValue; if (Recyle.UsedPool.Count > 0) { existMinIndex = Recyle.LittleIndexFromUsedPool().Index; existMaxIndex = Recyle.BigIndexFromUsedPool().Index; } var count = Recyle.Adapter.GetCellCount(); float offset = LeftPadding, width; for (var i = 0; i < count; i++, offset += width, offset += Space) { width = Recyle.GetCellZygote(i).Width; if (min > offset + width + Space) continue; if (max < offset) break; if (existMinIndex <= i && existMaxIndex >= i) continue; var cell = Recyle.PopFromCachedPool(i); var tf = cell.Transform; var ap = tf.anchoredPosition; ap.x = offset; tf.anchoredPosition = ap; var cur = new CellItem(i, ap.x, cell); Recyle.PushToUsedPool(cur); Recyle.Adapter.SetCellData(cell.Transform, i); } if (applyNormalized) wrapper.horizontalNormalizedPosition = normalized; } #endregion #region 私有方法 /// /// BindCell 在指定方向绑定单元格。 /// /// 起始单元格项。 /// 绑定距离。 /// 是否向小索引方向绑定。 private void BindCell(CellItem item, float distance, bool littleIndex) { var adapter = Recyle.Adapter; var o = 0f; var prev = item; while (o <= distance && (littleIndex ? prev.Index > 0 : prev.Index + 1 < adapter.GetCellCount())) { float width; int index; Vector2 ap; CellInfo cell; RectTransform tf; if (littleIndex) { index = prev.Index - 1; cell = Recyle.PopFromCachedPool(index); tf = cell.Transform; width = tf.rect.width; ap = tf.anchoredPosition; ap.x = prev.Offset - width - Space; } else { index = prev.Index + 1; cell = Recyle.PopFromCachedPool(index); tf = cell.Transform; width = prev.Cell.Transform.rect.width; ap = tf.anchoredPosition; ap.x = prev.Offset + width + Space; } tf.anchoredPosition = ap; var cur = new CellItem(index, ap.x, cell); Recyle.PushToUsedPool(cur); adapter.SetCellData(cell.Transform, index); prev = cur; o += width + Space; } } /// /// RecyclingCellFromList 回收标记为待回收的单元格。 /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RecyclingCellFromList() { if (recyclingPool.Count < 1) return; foreach (var cellExt in recyclingPool) { Recyle.MoveToCachedPool(cellExt); } recyclingPool.Clear(); } #endregion #region 私有属性 /// /// minMovingDistance 是最小移动距离,小于此距离的滚动变化将被忽略。 /// private const float minMovingDistance = 1f; /// /// minCellCount 是最小单元格数量,即使未达到视口覆盖区域,也至少创建这么多单元格。 /// private const int minCellCount = 3; /// /// recyclingPool 是待回收单元格列表。 /// private readonly List recyclingPool = new(); /// /// prevOffset 是上一次的水平偏移位置。 /// private float prevOffset; /// /// edge 是预加载区域的边缘值。 /// private float edge; #endregion } /// /// LayoutVerticalList 是垂直列表布局。 /// public class LayoutVerticalList : LayoutBase { /// /// LayoutVerticalList 创建垂直列表布局实例。 /// /// 单元格间距。 /// 顶部内边距。 /// 底部内边距。 public LayoutVerticalList(float space, float topPadding, float bottomPadding) { Space = space; TopPadding = topPadding; BottomPadding = bottomPadding; } /// /// Space 是单元格间距。 /// public float Space { get; } /// /// TopPadding 是顶部内边距。 /// public float TopPadding { get; } /// /// BottomPadding 是底部内边距。 /// public float BottomPadding { get; } #region 抽象实现 /// /// Vertical 表示是否为垂直滚动,垂直列表返回 true。 /// public override bool Vertical => true; /// /// CalcContentLengthOfSlidingDir 计算内容在垂直方向的总长度。 /// /// 内容总长度。 public override float CalcContentLengthOfSlidingDir() { var ds = Recyle.Adapter; var count = ds.GetCellCount(); if (count < 1) return 0f; if (ds.SingleZygote) { var zygote = Recyle.GetCellZygote(0); return zygote.Height * count + Space * (count - 1) + TopPadding + BottomPadding; } var offset = 0f; for (var i = 0; i < count; i++) { offset += Recyle.GetCellZygote(i).Height + (i > 0 ? Space : 0); } return offset + TopPadding + BottomPadding; } /// /// CalcCellOffset 计算指定索引单元格的位置偏移。 /// /// 单元格 RectTransform。 /// 单元格索引。 /// 计算出的位置偏移。 public override Vector2 CalcCellOffset(RectTransform cell, int index) { if (index < 1) return Vector2.zero; var ds = Recyle.Adapter; if (ds.SingleZygote) { var zygote = Recyle.GetCellZygote(0); return new Vector2(0, (zygote.Height + Space) * index); } var offset = TopPadding; for (var i = 0; i < index; i++) { offset += Recyle.GetCellZygote(i).Height + Space; } return new Vector2(0, offset); } /// /// GetCellZygote 获取单元格原型数据。 /// /// 单元格类型 ID。 /// 单元格原型数据。 public override CellZygote GetCellZygote(int type) { var zygote = Recyle.Adapter.GetCellZygote(type); var obj = zygote.gameObject; obj.SetActive(true); // 等比缩放 var sizeDelta = zygote.sizeDelta; var cellWidth = Recyle.Content.rect.width; var cellHeight = sizeDelta.y / sizeDelta.x * cellWidth; if (obj.scene.IsValid()) obj.SetActive(false); var value = new Vector2(0.5f, 1); return new CellZygote(type, cellWidth, cellHeight, value, value, value, zygote); } /// /// Setup 初始化布局并创建初始可见单元格。 /// public override void Setup() { var rect = Recyle.Viewport.rect; edge = rect.height * (MinCoverage - 1) * 0.5f; var ds = Recyle.Adapter; if (ds.GetCellCount() < 1) return; var maxY = rect.height * MinCoverage; var offset = TopPadding; var i = 0; var count = ds.GetCellCount(); while (i < count && (offset < maxY || i < minCellCount)) { var cell = Recyle.PopFromCachedPool(i); var ap = cell.Transform.anchoredPosition; ap.y = -offset; cell.Transform.anchoredPosition = ap; var item = new CellItem(i, ap.y, cell); Recyle.PushToUsedPool(item); ds.SetCellData(cell.Transform, i); offset += cell.Transform.rect.height + Space; i++; } } /// /// OnValueChanged 处理滚动值变化事件。 /// /// UIWrapContent 实例。 /// 新的滚动值。 public override void OnValueChanged(UIWrapContent wrapper, Vector2 v) { var normalized = wrapper.verticalNormalizedPosition; var contentHeight = Recyle.Content.rect.height; var viewportHeight = Recyle.Viewport.rect.height; if (contentHeight <= viewportHeight) return; normalized = Mathf.Clamp(normalized, 0f, 1f); normalized = 1f - normalized; var posY = normalized * (contentHeight - viewportHeight); var dir = posY - prevOffset; if (Mathf.Abs(dir) < minMovingDistance) return; ScrollTo(wrapper, normalized, false); } /// /// OnAdapterChanged 处理适配器变化事件。 /// /// UIWrapContent 实例。 public override void OnAdapterChanged(UIWrapContent wrapper) { ScrollTo(wrapper, 1 - wrapper.verticalNormalizedPosition); } /// /// ScrollTo 滚动到指定的归一化位置。 /// /// UIWrapContent实例。 /// 归一化位置值(0-1)。 /// 是否应用归一化值到滚动条,默认为 true。 public override void ScrollTo(UIWrapContent wrapper, float normalized, bool applyNormalized = true) { var contentHeight = Recyle.Content.rect.height; var viewportHeight = Recyle.Viewport.rect.height; var posY = 0f; if (contentHeight > viewportHeight) { normalized = Mathf.Clamp(normalized, 0f, 1f); posY = normalized * (contentHeight - viewportHeight); } prevOffset = posY; var min = posY - edge; var max = posY + edge + viewportHeight; foreach (var item in Recyle.UsedPool) { var tf = item.Cell.Transform; var edge = Mathf.Abs(tf.anchoredPosition.y) + tf.rect.height + Space; if (min <= edge) break; recyclingPool.Add(item); } RecyclingCellFromList(); for (var i = Recyle.UsedPool.Count - 1; i >= 0; i--) { var item = Recyle.UsedPool[i]; var tf = item.Cell.Transform; var edge = Mathf.Abs(tf.anchoredPosition.y); if (max >= edge) break; recyclingPool.Add(item); } RecyclingCellFromList(); var existMinIndex = int.MaxValue; var existMaxIndex = int.MinValue; if (Recyle.UsedPool.Count > 0) { existMinIndex = Recyle.LittleIndexFromUsedPool().Index; existMaxIndex = Recyle.BigIndexFromUsedPool().Index; } var count = Recyle.Adapter.GetCellCount(); float offset = TopPadding, height; for (var i = 0; i < count; i++, offset += height, offset += Space) { height = Recyle.GetCellZygote(i).Height; if (min > offset + height + Space) continue; if (max < offset) break; if (existMinIndex <= i && existMaxIndex >= i) continue; var cell = Recyle.PopFromCachedPool(i); var tf = cell.Transform; var ap = tf.anchoredPosition; ap.y = -offset; tf.anchoredPosition = ap; var cur = new CellItem(i, ap.y, cell); Recyle.PushToUsedPool(cur); Recyle.Adapter.SetCellData(cell.Transform, i); } if (applyNormalized) wrapper.verticalNormalizedPosition = 1f - normalized; } #endregion #region 私有方法 /// /// BindCell 在指定方向绑定单元格。 /// /// 起始单元格项。 /// 绑定距离。 /// 是否向小索引方向绑定。 private void BindCell(CellItem item, float distance, bool littleIndex) { var adapter = Recyle.Adapter; var o = 0f; var prev = item; while (o <= distance && (littleIndex ? prev.Index > 0 : prev.Index + 1 < adapter.GetCellCount())) { float height; int index; Vector2 ap; CellInfo cell; RectTransform tf; if (littleIndex) { index = prev.Index - 1; cell = Recyle.PopFromCachedPool(index); tf = cell.Transform; height = tf.rect.height; ap = tf.anchoredPosition; ap.y = prev.Offset + height + Space; } else { index = prev.Index + 1; cell = Recyle.PopFromCachedPool(index); tf = cell.Transform; height = prev.Cell.Transform.rect.height; ap = tf.anchoredPosition; ap.y = prev.Offset - height - Space; } tf.anchoredPosition = ap; var cur = new CellItem(index, ap.y, cell); Recyle.PushToUsedPool(cur); adapter.SetCellData(cell.Transform, index); prev = cur; o += height + Space; } } /// /// RecyclingCellFromList 回收标记为待回收的单元格。 /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RecyclingCellFromList() { if (recyclingPool.Count < 1) return; foreach (var cellExt in recyclingPool) { Recyle.MoveToCachedPool(cellExt); } recyclingPool.Clear(); } #endregion #region 私有属性 /// /// minMovingDistance 是最小移动距离,小于此距离的滚动变化将被忽略。 /// private const float minMovingDistance = 1f; /// /// minCellCount 是最小单元格数量,即使未达到视口覆盖区域,也至少创建这么多单元格。 /// private const int minCellCount = 3; /// /// recyclingPool 是待回收单元格列表。 /// private readonly List recyclingPool = new(); /// /// prevOffset 是上一次的垂直偏移位置。 /// private float prevOffset; /// /// edge 是预加载区域的边缘值。 /// private float edge; #endregion } /// /// RecyleModule 是循环组件。 /// public class RecyleModule : IComparer { #region 公开成员、属性 /// /// Adapter 获取或设置数据适配器。 /// public ICustomAdapter Adapter { get => adapter; protected set => adapter = value; } protected ICustomAdapter adapter; /// /// Viewport 获取或设置视口RectTransform。 /// public RectTransform Viewport { get => viewport; protected set => viewport = value; } protected RectTransform viewport; /// /// Content 获取或设置内容RectTransform。 /// public RectTransform Content { get => content; protected set => content = value; } protected RectTransform content; /// /// Layout 获取或设置布局控制器。 /// public LayoutBase Layout { get => layout; protected set => layout = value; } protected LayoutBase layout; #endregion #region 公开方法 /// /// RecyleModule 创建循环组件实例。 /// /// 布局控制器。 /// 数据适配器。 /// 视口RectTransform。 /// 内容RectTransform。 public RecyleModule(LayoutBase layout, ICustomAdapter adapter, RectTransform viewport, RectTransform content) { layout.Recyle = this; Layout = layout; Adapter = adapter; Viewport = viewport; Content = content; } /// /// Setup 初始化循环组件。 /// /// 初始化完成回调 /// 协程迭代器。 public IEnumerator Setup(Action onInitialized) { Inited = false; Cleanup(); yield return null; // 等待一帧,保证获取到宽高。 if (Layout.Vertical) { // Content.AnchorToTop(); var rect = Content.rect; float width = rect.width, height = rect.height; Content.anchorMin = new Vector2(0.5f, 1); Content.anchorMax = new Vector2(0.5f, 1); Content.pivot = new Vector2(0.5f, 1); Content.sizeDelta = new Vector2(width, height); } else { // Content.AnchorToLeft(); var rect = Content.rect; float width = rect.width, height = rect.height; Content.anchorMin = new Vector2(0, 0.5f); Content.anchorMax = new Vector2(0, 0.5f); Content.pivot = new Vector2(0, 0.5f); Content.sizeDelta = new Vector2(width, height); } Content.anchoredPosition = Vector3.zero; SetContentSize(); Layout.Setup(); Inited = true; onInitialized?.Invoke(); } /// /// OnAdapterChanged 处理适配器变化事件。 /// /// UIWrapContent实例。 public void OnAdapterChanged(UIWrapContent wrapper) { if (!Inited || Layout == null) return; Cleanup(); Adapter = wrapper.Adapter; SetContentSize(); Layout.OnAdapterChanged(wrapper); } /// /// ScrollTo 滚动到指定位置。 /// /// UIWrapContent实例。 /// 归一化位置值(0-1)。 public void ScrollTo(UIWrapContent wrapper, float normalized) { if (!Inited || Layout == null) return; Layout.ScrollTo(wrapper, normalized); } /// /// SetContentSize 设置内容尺寸,根据布局计算内容在滑动方向上的长度。 /// protected void SetContentSize() { var contentSize = Layout.CalcContentLengthOfSlidingDir(); var sizeDelta = Content.sizeDelta; sizeDelta = Layout.Vertical ? new Vector2(sizeDelta.x, contentSize) : new Vector2(contentSize, sizeDelta.y); Content.sizeDelta = sizeDelta; } /// /// Cleanup 清理循环组件,销毁所有已创建的单元格。 /// protected void Cleanup() { foreach (var item in UsedPool) { Destroy(item.Cell.Transform.gameObject); } UsedPool.Clear(); foreach (var ext in CachedPool) { Destroy(ext.Transform.gameObject); } CachedPool.Clear(); Zygotes.Clear(); } /// /// GetCellZygote 获取单元格原型。 /// /// 单元格索引。 /// 对应类型的单元格原型。 public CellZygote GetCellZygote(int index) { var type = Adapter.GetCellType(index); if (!Zygotes.TryGetValue(type, out var value)) { value = Layout.GetCellZygote(type); Zygotes[type] = value; } return value; } /// /// PopFromCachedPool 从缓存池获取单元格。 /// /// 单元格索引。 /// 单元格信息。 public CellInfo PopFromCachedPool(int index) { var cell = CellInfo.Default; var type = Adapter.GetCellType(index); var count = CachedPool.Count; var i = 0; for (; i < count; i++) { cell = CachedPool[i]; if (cell.Type != type) continue; break; } if (count > 0 && i < count) { CachedPool.RemoveAt(i); } else { var zygote = GetCellZygote(index); cell = CreateCell(zygote); } return cell; } /// /// PushToUsedPool 将单元格添加到已使用池。 /// /// 单元格项。 public void PushToUsedPool(CellItem item) { var index = UsedPool.BinarySearch(item, this); if (index < 0) UsedPool.Insert(~index, item); } /// /// MoveToCachedPool 将单元格从已使用池移动到缓存池。 /// /// 单元格项。 public void MoveToCachedPool(CellItem item) { UsedPool.Remove(item); CachedPool.Add(item.Cell); } /// /// Compare 比较两个单元格项的索引。 /// /// 第一个单元格项。 /// 第二个单元格项。 /// 比较结果。 public int Compare(CellItem x, CellItem y) => x.Index.CompareTo(y.Index); /// /// CreateCell 创建单元格实例。 /// /// 单元格原型。 /// 新创建的单元格信息。 private CellInfo CreateCell(CellZygote zygote) { var cellObj = Instantiate(zygote.Zygote.gameObject, Content, false); var cell = cellObj.GetComponent(); cellObj.name = "Cell"; cell.anchoredPosition = Vector2.zero; cell.anchorMin = zygote.AnchorMin; cell.anchorMax = zygote.AnchorMax; cell.pivot = zygote.Pivot; cell.sizeDelta = new Vector2(zygote.Width, zygote.Height); cellObj.SetActive(true); return new CellInfo(cell, zygote.Type); } #endregion #region 保护成员、属性 /// /// Inited 表示模块是否已初始化。 /// protected bool Inited; /// /// UsedPool 记录已使用的单元格,此列表总是以索引从小到大排序。 /// public readonly List UsedPool = new List(); /// /// LittleIndexFromUsedPool 返回索引小端的单元格。 /// /// 索引最小的单元格项。 [MethodImpl(MethodImplOptions.AggressiveInlining)] public CellItem LittleIndexFromUsedPool() => UsedPool.Count > 0 ? UsedPool[0] : CellItem.Default; /// /// BigIndexFromUsedPool 返回索引大端的单元格。 /// /// 索引最大的单元格项。 [MethodImpl(MethodImplOptions.AggressiveInlining)] public CellItem BigIndexFromUsedPool() => UsedPool.Count > 0 ? UsedPool[UsedPool.Count - 1] : CellItem.Default; /// /// CachedPool 是缓存池(未使用的单元格)。 /// internal readonly List CachedPool = new List(); /// /// Zygotes 是单元格原型(预制体)。 /// internal readonly Dictionary Zygotes = new Dictionary(); #endregion } #endregion #region view logic /// /// layout 是当前使用的布局模式。 /// [SerializeField] private LayoutMode layout; /// /// segment 是布局间隔或网格的行/列数。 /// 在列表模式下表示间隔,在网格模式下表示行数或列数。 /// [SerializeField] private int segment; /// /// padding 是内边距。 /// x为左/上内边距,y为右/下内边距。 /// [SerializeField] private Vector2 padding; /// /// scrollTo 是滚动目标位置。 /// 用于初始化完成前的滚动请求暂存。 /// private float scrollTo = -1; /// /// EachItemHandler 是遍历每个显示项的委托。 /// /// 项的索引 /// 项的RectTransform组件 public delegate void EachItemHandler(int index, RectTransform item); /// /// Layout 获取或设置布局模式。 /// public LayoutMode Layout { get => layout; set => layout = value; } /// /// Recyle 获取或设置循环组件。 /// public RecyleModule Recyle { get; set; } /// /// Inited 获取组件是否已完成初始化。 /// public bool Inited { get; protected set; } /// /// adapter 是数据适配器实例。 /// private ICustomAdapter adapter; /// /// Adapter 获取或设置数据适配器。 /// 设置新适配器后会自动重新加载列表。 /// public ICustomAdapter Adapter { get => adapter; set { adapter = value; if (Adapter != null) Reload(); } } /// /// Start 组件启动时初始化。 /// protected override void Start() { base.Start(); if (!Application.isPlaying) return; vertical = layout == LayoutMode.VerticalList || layout == LayoutMode.VerticalGrid; horizontal = !vertical; switch (layout) { case LayoutMode.HorizontalList: Recyle = new RecyleModule(new LayoutHorizontalList(segment, padding.x, padding.y), Adapter, viewport, content); break; case LayoutMode.VerticalList: Recyle = new RecyleModule(new LayoutVerticalList(segment, padding.x, padding.y), Adapter, viewport, content); break; case LayoutMode.VerticalGrid: Recyle = new RecyleModule(new LayoutVerticalGrid(Math.Max(1, segment), padding.x, padding.y), Adapter, viewport, content); break; case LayoutMode.HorizontalGrid: Recyle = new RecyleModule(new LayoutHorizontalGrid(Math.Max(1, segment), padding.x, padding.y), Adapter, viewport, content); break; default: throw new ArgumentOutOfRangeException(); } onValueChanged.RemoveListener(OnValueChanged); StartCoroutine(Recyle.Setup(() => { Inited = true; onValueChanged.AddListener(OnValueChanged); if (scrollTo >= 0) { Recyle.ScrollTo(this, scrollTo); scrollTo = -1; } })); } /// /// OnValueChanged 处理滚动值变化事件。 /// /// 新的滚动值 private void OnValueChanged(Vector2 v) { if (Inited) Recyle.Layout.OnValueChanged(this, v); } /// /// Reload 重载列表。 /// public void Reload() { if (!Inited) return; Recyle.OnAdapterChanged(this); } /// /// ScrollTo 跳转至指定位置。 /// /// [0 -> 从上到下/从左到右 -> 1] public void ScrollTo(float normalized) { if (!Inited) scrollTo = normalized; else Recyle.ScrollTo(this, normalized); } /// /// EachItem 遍历显示的子节点。 /// /// 回调函数。 public void EachItem(EachItemHandler handler) { if (!Inited) return; if (handler == null) return; for (int i = 0; i < Recyle.UsedPool.Count; i++) { var item = Recyle.UsedPool[i]; handler.Invoke(item.Index, item.Cell.Transform); } } #endregion } }