VirtualFramework/Assets/Scripts/Tools/WaterfallScrollView.cs
2025-04-27 11:41:11 +08:00

361 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}