361 lines
11 KiB
C#
361 lines
11 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
public class WaterfallScrollView : MonoBehaviour
|
||
{
|
||
[Header("组件引用")]
|
||
[SerializeField] private ScrollRect scrollRect;
|
||
[SerializeField] private RectTransform viewport;
|
||
[SerializeField] private RectTransform content;
|
||
[SerializeField] private RectTransform itemPrefab;
|
||
[SerializeField] private Image itemImageComponent; // 预制体中的Image组件引用
|
||
|
||
[Header("瀑布流设置")]
|
||
[SerializeField] private int columnCount = 3;
|
||
[SerializeField] private float spacingX = 10f;
|
||
[SerializeField] private float spacingY = 10f;
|
||
[SerializeField] private float paddingLeft = 10f;
|
||
[SerializeField] private float paddingRight = 10f;
|
||
[SerializeField] private float paddingTop = 10f;
|
||
[SerializeField] private float paddingBottom = 10f;
|
||
|
||
private List<float> columnHeights;
|
||
private List<RectTransform> activeItems = new List<RectTransform>();
|
||
private Dictionary<int, RectTransform> pooledItems = new Dictionary<int, RectTransform>();
|
||
private float itemWidth;
|
||
private float viewportHeight;
|
||
private float contentHeight;
|
||
private int totalItemCount = 0;
|
||
private int visibleStartIndex = 0;
|
||
private int visibleEndIndex = 0;
|
||
public List<Sprite> sprites = new List<Sprite>(); // 存储所有精灵
|
||
private List<float> itemHeights = new List<float>(); // 存储每项的计算高度
|
||
|
||
private void Start()
|
||
{
|
||
Initialize();
|
||
scrollRect.onValueChanged.AddListener(OnScroll);
|
||
|
||
// 初始检查sprites是否有内容
|
||
if (sprites.Count > 0)
|
||
{
|
||
SetContent(sprites);
|
||
}
|
||
else
|
||
{
|
||
ClearContent();
|
||
}
|
||
}
|
||
|
||
private void Initialize()
|
||
{
|
||
// 计算列宽和间距
|
||
float availableWidth = viewport.rect.width - paddingLeft - paddingRight - (columnCount - 1) * spacingX;
|
||
itemWidth = availableWidth / columnCount;
|
||
|
||
// 初始化列高度数组
|
||
columnHeights = new List<float>(new float[columnCount]);
|
||
|
||
// 记录视口高度用于计算可见项
|
||
viewportHeight = viewport.rect.height;
|
||
}
|
||
|
||
// 使用提供的精灵列表设置内容
|
||
public void SetContent(List<Sprite> sprites)
|
||
{
|
||
this.sprites = sprites ?? new List<Sprite>();
|
||
totalItemCount = this.sprites.Count;
|
||
|
||
// 如果没有精灵,清空内容
|
||
if (totalItemCount == 0)
|
||
{
|
||
ClearContent();
|
||
return;
|
||
}
|
||
|
||
// 计算每项的高度
|
||
CalculateItemHeights();
|
||
|
||
// 计算内容高度
|
||
contentHeight = CalculateContentHeight();
|
||
content.sizeDelta = new Vector2(content.sizeDelta.x, contentHeight);
|
||
|
||
// 更新可见项
|
||
UpdateVisibleItems();
|
||
}
|
||
|
||
// 清空所有内容
|
||
public void ClearContent()
|
||
{
|
||
// 回收所有活动项
|
||
foreach (var item in activeItems)
|
||
{
|
||
item.gameObject.SetActive(false);
|
||
}
|
||
activeItems.Clear();
|
||
|
||
// 重置内容大小
|
||
content.sizeDelta = new Vector2(content.sizeDelta.x, 0);
|
||
totalItemCount = 0;
|
||
itemHeights.Clear();
|
||
}
|
||
|
||
private void CalculateItemHeights()
|
||
{
|
||
itemHeights.Clear();
|
||
|
||
foreach (Sprite sprite in sprites)
|
||
{
|
||
if (sprite != null)
|
||
{
|
||
// 计算保持宽高比的高度
|
||
float aspectRatio = sprite.rect.width / sprite.rect.height;
|
||
float height = itemWidth / aspectRatio;
|
||
itemHeights.Add(height);
|
||
}
|
||
else
|
||
{
|
||
// 如果精灵为空,使用默认高度
|
||
itemHeights.Add(150f);
|
||
}
|
||
}
|
||
}
|
||
|
||
private float CalculateContentHeight()
|
||
{
|
||
// 重置列高度
|
||
for (int i = 0; i < columnCount; i++)
|
||
{
|
||
columnHeights[i] = paddingTop;
|
||
}
|
||
|
||
// 模拟布局计算内容高度
|
||
for (int i = 0; i < totalItemCount; i++)
|
||
{
|
||
float height = itemHeights[i];
|
||
int shortestColumnIndex = GetShortestColumn();
|
||
columnHeights[shortestColumnIndex] += height + spacingY;
|
||
}
|
||
|
||
// 找出最高的列作为内容高度
|
||
float maxHeight = 0;
|
||
foreach (float height in columnHeights)
|
||
{
|
||
if (height > maxHeight)
|
||
{
|
||
maxHeight = height;
|
||
}
|
||
}
|
||
|
||
return maxHeight + paddingBottom - spacingY;
|
||
}
|
||
|
||
private int GetShortestColumn()
|
||
{
|
||
int shortestIndex = 0;
|
||
float minHeight = columnHeights[0];
|
||
|
||
for (int i = 1; i < columnCount; i++)
|
||
{
|
||
if (columnHeights[i] < minHeight)
|
||
{
|
||
minHeight = columnHeights[i];
|
||
shortestIndex = i;
|
||
}
|
||
}
|
||
|
||
return shortestIndex;
|
||
}
|
||
|
||
private void UpdateVisibleItems()
|
||
{
|
||
// 如果没有项目,不执行任何操作
|
||
if (totalItemCount == 0)
|
||
return;
|
||
|
||
// 计算当前可视区域的上下边界
|
||
float viewportTop = -content.anchoredPosition.y;
|
||
float viewportBottom = viewportTop + viewportHeight;
|
||
|
||
// 重置列高度用于布局计算
|
||
for (int i = 0; i < columnCount; i++)
|
||
{
|
||
columnHeights[i] = paddingTop;
|
||
}
|
||
|
||
// 计算可见项的起始和结束索引
|
||
int newVisibleStartIndex = 0;
|
||
int newVisibleEndIndex = totalItemCount - 1;
|
||
float currentYPosition = 0;
|
||
|
||
// 找到第一个可见项的索引
|
||
for (int i = 0; i < totalItemCount; i++)
|
||
{
|
||
float height = itemHeights[i];
|
||
int columnIndex = GetShortestColumn();
|
||
float xPosition = paddingLeft + columnIndex * (itemWidth + spacingX);
|
||
float yPosition = -columnHeights[columnIndex]; // 注意UI坐标是向下为正
|
||
|
||
columnHeights[columnIndex] += height + spacingY;
|
||
|
||
float itemTop = yPosition;
|
||
float itemBottom = yPosition - height;
|
||
|
||
// 如果项在可视区域内或者与可视区域有交集
|
||
if (itemBottom <= viewportBottom && itemTop >= viewportTop - viewportHeight) // 多加载一个屏幕高度的内容
|
||
{
|
||
newVisibleStartIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 重置列高度再次计算
|
||
for (int i = 0; i < columnCount; i++)
|
||
{
|
||
columnHeights[i] = paddingTop;
|
||
}
|
||
|
||
// 找到最后一个可见项的索引
|
||
for (int i = 0; i < totalItemCount; i++)
|
||
{
|
||
float height = itemHeights[i];
|
||
int columnIndex = GetShortestColumn();
|
||
float xPosition = paddingLeft + columnIndex * (itemWidth + spacingX);
|
||
float yPosition = -columnHeights[columnIndex]; // 注意UI坐标是向下为正
|
||
|
||
columnHeights[columnIndex] += height + spacingY;
|
||
|
||
float itemTop = yPosition;
|
||
float itemBottom = yPosition - height;
|
||
|
||
// 如果项在可视区域内或者与可视区域有交集
|
||
if (itemBottom <= viewportBottom + viewportHeight && itemTop >= viewportTop) // 多加载一个屏幕高度的内容
|
||
{
|
||
newVisibleEndIndex = i;
|
||
}
|
||
else if (i > newVisibleStartIndex) // 已经找到起始索引后才开始判断结束
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 如果可见项范围没有变化则不更新
|
||
if (newVisibleStartIndex == visibleStartIndex && newVisibleEndIndex == visibleEndIndex)
|
||
{
|
||
return;
|
||
}
|
||
|
||
visibleStartIndex = newVisibleStartIndex;
|
||
visibleEndIndex = newVisibleEndIndex;
|
||
|
||
// 回收不再可见的项
|
||
RecycleInvisibleItems();
|
||
|
||
// 重置列高度再次计算,这次用于实际布局
|
||
for (int i = 0; i < columnCount; i++)
|
||
{
|
||
columnHeights[i] = paddingTop;
|
||
}
|
||
|
||
// 创建或更新可见项
|
||
for (int i = 0; i < totalItemCount; i++)
|
||
{
|
||
float height = itemHeights[i];
|
||
int columnIndex = GetShortestColumn();
|
||
float xPosition = paddingLeft + columnIndex * (itemWidth + spacingX);
|
||
float yPosition = -columnHeights[columnIndex]; // 注意UI坐标是向下为正
|
||
|
||
columnHeights[columnIndex] += height + spacingY;
|
||
|
||
// 只处理可见范围内的项
|
||
if (i >= visibleStartIndex && i <= visibleEndIndex)
|
||
{
|
||
CreateOrUpdateItem(i, xPosition, yPosition, itemWidth, height);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void CreateOrUpdateItem(int index, float x, float y, float width, float height)
|
||
{
|
||
RectTransform item;
|
||
if (!pooledItems.TryGetValue(index, out item))
|
||
{
|
||
// 创建新项
|
||
item = Instantiate(itemPrefab, content);
|
||
item.name = "Item_" + index;
|
||
pooledItems[index] = item;
|
||
activeItems.Add(item);
|
||
}
|
||
else if (!activeItems.Contains(item))
|
||
{
|
||
// 从池中取出
|
||
item.gameObject.SetActive(true);
|
||
activeItems.Add(item);
|
||
}
|
||
|
||
// 设置位置和大小
|
||
item.anchorMin = new Vector2(0, 1);
|
||
item.anchorMax = new Vector2(0, 1);
|
||
item.pivot = new Vector2(0, 1);
|
||
item.anchoredPosition = new Vector2(x, y);
|
||
item.sizeDelta = new Vector2(width, height);
|
||
|
||
// 更新项内容
|
||
UpdateItemContent(item, index);
|
||
}
|
||
|
||
private void UpdateItemContent(RectTransform item, int index)
|
||
{
|
||
// 获取Image组件
|
||
Image image = item.GetComponentInChildren<Image>();
|
||
if (image == null && itemImageComponent != null)
|
||
{
|
||
// 如果没有找到Image组件,使用预制体中的引用
|
||
image = Instantiate(itemImageComponent, item);
|
||
image.rectTransform.anchorMin = Vector2.zero;
|
||
image.rectTransform.anchorMax = Vector2.one;
|
||
image.rectTransform.sizeDelta = Vector2.zero;
|
||
}
|
||
|
||
if (image && index < sprites.Count && sprites[index] != null)
|
||
{
|
||
// 设置精灵
|
||
image.sprite = sprites[index];
|
||
image.SetNativeSize();
|
||
|
||
// 设置Image组件为保持宽高比
|
||
image.preserveAspect = true;
|
||
}
|
||
}
|
||
|
||
private void RecycleInvisibleItems()
|
||
{
|
||
// 找出需要回收的项
|
||
List<RectTransform> itemsToRecycle = new List<RectTransform>();
|
||
foreach (RectTransform item in activeItems)
|
||
{
|
||
string[] nameParts = item.name.Split('_');
|
||
if (nameParts.Length >= 2 && int.TryParse(nameParts[1], out int itemIndex))
|
||
{
|
||
if (itemIndex < visibleStartIndex || itemIndex > visibleEndIndex)
|
||
{
|
||
itemsToRecycle.Add(item);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 回收项
|
||
foreach (RectTransform item in itemsToRecycle)
|
||
{
|
||
item.gameObject.SetActive(false);
|
||
activeItems.Remove(item);
|
||
}
|
||
}
|
||
|
||
private void OnScroll(Vector2 scrollPosition)
|
||
{
|
||
UpdateVisibleItems();
|
||
}
|
||
} |