2025-06-07 17:43:34 +08:00

789 lines
28 KiB
C#
Raw Permalink 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.

// Author: 文若
// CreateDate: 2022/10/26
// ## 生成滑动列表必须以下步骤:
// 1. 持有RecycleView对象rvrv.InitData(callBackFunc)
// 2. 刷新整个列表(首次调用和数量变化时调用): ShowList(int count)
// 3. 回调: Func(GameObject cell, int index)
// ----------
// 功能接口看代码案例详见RecycleViewTest.cs
// 刷新单个项: UpdateCell(int index)
// 刷新列表数据(无数量变化时调用): UpdateList()
// 定位到索引所在当前列表的位置 GoToCellPos(int index)
using System;
using System.Collections.Generic;
using UnityEngine.EventSystems;
namespace UnityEngine.UI
{
public enum E_Direction
{
Horizontal,
Vertical,
HorizontalRTL // 水平从右到左
}
public class RecycleView : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IDragHandler
{
public GameObject firstArrow;
public GameObject endArrow;
public E_Direction dir = E_Direction.Vertical;
public bool isShowArrow = false;
public int lines = 1; // 默认显示1行
public float squareSpacing = 5f; // 方阵间距
public GameObject cell; //指定的cell
public Vector2 Spacing = Vector2.zero;
public float row = 0f; // 行间距
public float col = 0f; // 列间距
public float paddingTop = 0f; // 顶部空隙
public float paddingLeft = 0f; // 左侧空隙
public float paddingRight = 0f; // 右侧空隙用于RTL模式
protected Action<GameObject, int> FuncCallBackFunc_Last;
protected Action<GameObject, int> FuncCallBackFunc;
protected Action<GameObject, int> FuncOnClickCallBack;
protected Action<int, bool, GameObject, bool> FuncOnButtonClickCallBack;
protected Action<GameObject, int> FuncCallBackFunc_OutRange;
protected float planeW;
protected float planeH;
protected float contentW;
protected float contentH;
protected float cellW;
protected float cellH;
private bool isInit = false;
protected GameObject content;
protected ScrollRect scrollRect;
protected RectTransform rectTrans;
protected RectTransform contentRectTrans;
protected int maxCount = -1; //列表数量
protected int minIndex = -1;
protected int maxIndex = -1;
//记录 物体的坐标 和 物体
protected struct CellInfo
{
public Vector3 pos;
public GameObject obj;
};
protected CellInfo[] cellInfos;
protected bool isClearList = false; //是否清空列表
// 对象池
protected Stack<GameObject> Pool = new Stack<GameObject>();
protected bool isInited = false;
public bool GetIsInit()
{
return isInited;
}
public virtual void Init(Action<GameObject, int> callBack)
{
Init(callBack, null);
}
public virtual void Init2(Action<GameObject, int> callBack, Action<GameObject, int> callBack_last)
{
Init2(callBack, callBack_last, null);
}
public virtual void Init(Action<GameObject, int> callBack, Action<GameObject, int> onClickCallBack,
Action<int, bool, GameObject, bool> onButtonClickCallBack)
{
if (onButtonClickCallBack != null)
{
FuncOnButtonClickCallBack = onButtonClickCallBack;
}
Init(callBack, onClickCallBack);
}
public virtual void Init2(Action<GameObject, int> callBack, Action<GameObject, int> callBack_last, Action<GameObject, int> onClickCallBack)
{
this.FuncCallBackFunc_Last = callBack_last;
Init(callBack, onClickCallBack);
}
public virtual void Init3(Action<GameObject, int> callBack, Action<GameObject, int> outRangeCallBack)
{
this.FuncCallBackFunc_OutRange = outRangeCallBack;
Init(callBack, null);
}
public virtual void Init(Action<GameObject, int> callBack, Action<GameObject, int> onClickCallBack)
{
DisposeAll();
FuncCallBackFunc = callBack;
if (onClickCallBack != null)
{
FuncOnClickCallBack = onClickCallBack;
}
if (isInit) return;
content = this.GetComponent<ScrollRect>().content.gameObject;
bool tmpCellInList = false;
if (cell == null)
{
cell = content.transform.GetChild(0).gameObject;
}
if (cell.transform.IsChildOf(content.transform))
tmpCellInList = true;
// ////////////////////** Cell 处理 **////////////////////
// m_CellGameObject.transform.SetParent(m_Content.transform.parent, false);
cell.gameObject.SetActive(tmpCellInList);
if (tmpCellInList == true)
SetPoolsObj(cell);
RectTransform cellRectTrans = cell.GetComponent<RectTransform>();
cellRectTrans.pivot = new Vector2(0f, 1f);
CheckAnchor(cellRectTrans);
cellRectTrans.anchoredPosition = Vector2.zero;
// 记录 Cell 信息
cellH = cellRectTrans.rect.height;
cellW = cellRectTrans.rect.width;
// 记录 Plane 信息
rectTrans = GetComponent<RectTransform>();
Rect planeRect = rectTrans.rect;
planeH = planeRect.height;
planeW = planeRect.width;
// 记录 Content 信息
contentRectTrans = content.GetComponent<RectTransform>();
Rect contentRect = contentRectTrans.rect;
contentH = contentRect.height;
contentW = contentRect.width;
// 记录间距信息 如果存在行列设置就引用,没有使用方阵间距
row = Spacing.x;
col = Spacing.y;
if (row == 0 && col == 0) row = col = squareSpacing;
else squareSpacing = 0;
contentRectTrans.pivot = new Vector2(0f, 1f);
//m_ContentRectTrans.sizeDelta = new Vector2 (planeRect.width, planeRect.height);
//m_ContentRectTrans.anchoredPosition = Vector2.zero;
CheckAnchor(contentRectTrans);
scrollRect = this.GetComponent<ScrollRect>();
scrollRect.onValueChanged.RemoveAllListeners();
//添加滑动事件
scrollRect.onValueChanged.AddListener(delegate (Vector2 value) { ScrollRectListener(value); });
if (firstArrow != null || endArrow != null)
{
scrollRect.onValueChanged.AddListener(delegate (Vector2 value) { OnDragListener(value); });
OnDragListener(Vector2.zero);
}
//InitScrollBarGameObject(); // 废弃
isInit = true;
}
// 检查 Anchor 是否正确
private void CheckAnchor(RectTransform rectTrans)
{
if (dir == E_Direction.Vertical)
{
if (!((rectTrans.anchorMin == new Vector2(0, 1) && rectTrans.anchorMax == new Vector2(0, 1)) ||
(rectTrans.anchorMin == new Vector2(0, 1) && rectTrans.anchorMax == new Vector2(1, 1))))
{
rectTrans.anchorMin = new Vector2(0, 1);
rectTrans.anchorMax = new Vector2(1, 1);
}
}
else // 水平方向 (包括从左到右和从右到左)
{
if (!((rectTrans.anchorMin == new Vector2(0, 1) && rectTrans.anchorMax == new Vector2(0, 1)) ||
(rectTrans.anchorMin == new Vector2(0, 0) && rectTrans.anchorMax == new Vector2(0, 1))))
{
rectTrans.anchorMin = new Vector2(0, 0);
rectTrans.anchorMax = new Vector2(0, 1);
}
}
}
// 实时刷新列表时用
public virtual void UpdateList()
{
for (int i = 0, length = cellInfos.Length; i < length; i++)
{
CellInfo cellInfo = cellInfos[i];
if (cellInfo.obj != null)
{
float rangePos = dir == E_Direction.Vertical ? cellInfo.pos.y : cellInfo.pos.x;
if (!IsOutRange(rangePos))
{
Func(FuncCallBackFunc, cellInfo.obj, true);
}
}
}
}
/// <summary>
/// 刷新某一项 index 从1开始
/// </summary>
/// <param name="index"></param>
public void UpdateCell(int index)
{
CellInfo cellInfo = cellInfos[index - 1];
if (cellInfo.obj != null)
{
float rangePos = dir == E_Direction.Vertical ? cellInfo.pos.y : cellInfo.pos.x;
if (!IsOutRange(rangePos))
{
Func(FuncCallBackFunc, cellInfo.obj);
}
}
}
public virtual void ShowList(string numStr)
{
}
public virtual void ShowList(int num)
{
minIndex = -1;
maxIndex = -1;
//-> 计算 Content 尺寸
if (dir == E_Direction.Vertical)
{
float contentSize = (col + cellH) * Mathf.CeilToInt((float)num / lines) + paddingTop;
contentH = contentSize;
contentW = contentRectTrans.sizeDelta.x + paddingLeft;
contentSize = contentSize < rectTrans.rect.height ? rectTrans.rect.height : contentSize;
contentRectTrans.sizeDelta = new Vector2(contentW, contentSize);
if (num != maxCount)
{
contentRectTrans.anchoredPosition = new Vector2(contentRectTrans.anchoredPosition.x, 0);
}
}
else // 水平方向 (包括从左到右和从右到左)
{
float contentSize = (row + cellW) * Mathf.CeilToInt((float)num / lines) + (dir == E_Direction.HorizontalRTL ? paddingRight : paddingLeft);
contentW = contentSize;
contentH = contentRectTrans.sizeDelta.x + paddingLeft;
contentSize = contentSize < rectTrans.rect.width ? rectTrans.rect.width : contentSize;
contentRectTrans.sizeDelta = new Vector2(contentSize, contentH);
if (num != maxCount)
{
contentRectTrans.anchoredPosition = new Vector2(0, contentRectTrans.anchoredPosition.y);
}
}
//-> 计算 开始索引
int lastEndIndex = 0;
//-> 过多的物体 扔到对象池 ( 首次调 ShowList函数时 则无效 )
if (isInited)
{
lastEndIndex = num - maxCount > 0 ? maxCount : num;
lastEndIndex = isClearList ? 0 : lastEndIndex;
int count = isClearList ? cellInfos.Length : maxCount;
for (int i = lastEndIndex; i < count; i++)
{
if (cellInfos[i].obj != null)
{
Func(FuncCallBackFunc_OutRange, cellInfos[i].obj);
SetPoolsObj(cellInfos[i].obj);
cellInfos[i].obj = null;
}
}
}
//-> 以下四行代码 在for循环所用
CellInfo[] tempCellInfos = cellInfos;
cellInfos = new CellInfo[num];
//-> 1: 计算 每个Cell坐标并存储 2: 显示范围内的 Cell
for (int i = 0; i < num; i++)
{
// * -> 存储 已有的数据 ( 首次调 ShowList函数时 则无效 )
if (maxCount != -1 && i < lastEndIndex)
{
CellInfo tempCellInfo = tempCellInfos[i];
//-> 计算是否超出范围
float rPos = dir == E_Direction.Vertical ? tempCellInfo.pos.y : tempCellInfo.pos.x;
if (!IsOutRange(rPos))
{
//-> 记录显示范围中的 首位index 和 末尾index
minIndex = minIndex == -1 ? i : minIndex; //首位index
maxIndex = i; // 末尾index
if (tempCellInfo.obj == null)
{
tempCellInfo.obj = GetPoolsObj();
}
tempCellInfo.obj.name = i.ToString();
Func(FuncCallBackFunc_Last, tempCellInfo.obj);
// 记录bug这里应该使用localPosition避免z轴丢失导致刷新列表的时候z轴异常 为什么z轴不为0 未知
// tempCellInfo.obj.transform.GetComponent<RectTransform>().anchoredPosition = tempCellInfo.pos;
tempCellInfo.obj.transform.GetComponent<RectTransform>().localPosition = tempCellInfo.pos;
tempCellInfo.obj.SetActive(true);
Func(FuncCallBackFunc, tempCellInfo.obj);
}
else
{
Func(FuncCallBackFunc_OutRange, tempCellInfo.obj);
SetPoolsObj(tempCellInfo.obj);
tempCellInfo.obj = null;
}
cellInfos[i] = tempCellInfo;
continue;
}
CellInfo cellInfo = new CellInfo();
float pos = 0; //坐标( isVertical ? 记录Y : 记录X )
float rowPos = 0; //计算每排里面的cell 坐标
// * -> 计算每个Cell坐标
if (dir == E_Direction.Vertical)
{
pos = cellH * Mathf.FloorToInt(i / lines) +
col * Mathf.FloorToInt(i / lines);
rowPos = cellW * (i % lines) + row * (i % lines);
// 为每个cell假如留白边距
cellInfo.pos = new Vector3(rowPos + paddingLeft, -pos - paddingTop, 0);
}
else if (dir == E_Direction.Horizontal)
{
pos = cellW * Mathf.FloorToInt(i / lines) + row * Mathf.FloorToInt(i / lines);
rowPos = cellH * (i % lines) + col * (i % lines);
cellInfo.pos = new Vector3(pos + paddingLeft, -rowPos - paddingTop, 0);
}
else if (dir == E_Direction.HorizontalRTL)
{
// RTL模式从右向左布局
int columnCount = Mathf.CeilToInt((float)num / lines);
int currentColumn = Mathf.FloorToInt(i / lines);
// 位置计算为:总宽度 - 当前列位置 - 单元格宽度
pos = (columnCount - currentColumn - 1) * (cellW + row);
rowPos = cellH * (i % lines) + col * (i % lines);
cellInfo.pos = new Vector3(pos + paddingRight, -rowPos - paddingTop, 0);
}
//-> 计算是否超出范围
float cellPos = dir == E_Direction.Vertical ? cellInfo.pos.y : cellInfo.pos.x;
if (IsOutRange(cellPos))
{
cellInfo.obj = null;
cellInfos[i] = cellInfo;
continue;
}
//-> 记录显示范围中的 首位index 和 末尾index
minIndex = minIndex == -1 ? i : minIndex; //首位index
maxIndex = i; // 末尾index
//-> 取或创建 Cell
GameObject cell = GetPoolsObj();
cell.gameObject.name = i.ToString();
Func(FuncCallBackFunc_Last, cell);
// 记录bug这里应该使用localPosition避免z轴丢失导致刷新列表的时候z轴异常 为什么z轴不为0 未知
// cell.transform.GetComponent<RectTransform>().anchoredPosition = cellInfo.pos;
cell.transform.GetComponent<RectTransform>().localPosition = cellInfo.pos;
//-> 存数据
cellInfo.obj = cell;
cellInfos[i] = cellInfo;
//-> 回调 函数
Func(FuncCallBackFunc, cell);
}
maxCount = num;
isInited = true;
OnDragListener(Vector2.zero);
}
#region
/// 定位到第一行,也是还原到初始位置
public void GoToOneLine()
{
GoToCellPos(0);
}
/// <summary>
/// 通过index定位到某一单元格的坐标位置
/// </summary>
/// <param name="index">索引ID</param>
public void GoToCellPos(int index, int index2 = 1)
{
// 如果cellInfo不存在坐标说明没有被初始化过当前没有数据直接return
if (cellInfos.Length == 0) return;
// 当前索引所在行的第一个索引
int theFirstIndex = index - index % lines;
Vector2 newPos = cellInfos[theFirstIndex].pos;
if (dir == E_Direction.Vertical)
{
// 纵滑时定位到某一点,需要进行布局上的显示判断
// 如果index是第0行即index<=lines, 回到的位置应该是第一行坐标y+顶部空隙 (x,y+top)
// index>lines,显示的index的布局应该 (x,y+col)
var posY = index <= lines ? -newPos.y - paddingTop : -newPos.y - col;
contentRectTrans.anchoredPosition = new Vector2(contentRectTrans.anchoredPosition.x, posY);
}
else if (dir == E_Direction.Horizontal)
{
// 横向滑动时
// 如果index是第0行即index<=lines, 回到的位置 (x+left,y)
// index>lines,位置应该为 (x+row,y)
var posX = index <= lines ? -newPos.x + paddingLeft : -newPos.x + row;
posX = posX + cellW * (index2 - 1) / 3;
contentRectTrans.anchoredPosition = new Vector2(posX, contentRectTrans.anchoredPosition.y);
}
else if (dir == E_Direction.HorizontalRTL)
{
// RTL模式下的定位逻辑
var posX = index <= lines ? -newPos.x + paddingRight : -newPos.x + row;
posX = posX + cellW * (index2 - 1) / 3;
contentRectTrans.anchoredPosition = new Vector2(posX, contentRectTrans.anchoredPosition.y);
}
UpdateCheck();
}
public void GoToCellPos2(int index, int index2, float[] floats)
{
if (dir == E_Direction.Vertical)
return;
// 如果cellInfo不存在坐标说明没有被初始化过当前没有数据直接return
if (cellInfos.Length == 0) return;
// 当前索引所在行的第一个索引
int theFirstIndex = index - index % lines;
// 假设在第一行最大索引
var tmpIndex = theFirstIndex + maxIndex;
int theLastIndex = tmpIndex > maxCount - 1 ? maxCount - 1 : tmpIndex;
// 如果最大索引就是边界的话,边界的
if (theLastIndex == maxCount - 1 && index2 == floats.Length - 1)
{
// 余数不为0的情况下第一个索引位置需要考虑最大数到最后显示位置的边距
var shortOfNum = maxCount % lines == 0 ? 0 : lines - maxCount % lines;
theFirstIndex = theLastIndex - maxIndex + shortOfNum;
}
Vector2 newPos = cellInfos[theFirstIndex].pos;
var posX = .0f;
if (index < lines)
{
if (index2 == 0)
posX = -newPos.x + paddingLeft;
else
posX = -newPos.x + paddingLeft - floats[index2];
}
else
{
posX = -newPos.x + row - floats[index2];
}
contentRectTrans.anchoredPosition = new Vector2(posX, contentRectTrans.anchoredPosition.y);
UpdateCheck();
}
/// <summary>
/// 获取某一单元格
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public GameObject GetItem(int index)
{
if (cellInfos.Length > index)
return cellInfos[index].obj;
else
return null;
}
#endregion
#if UNITY_EDITOR
public void LogRecycleView()
{
// 拿到容器基础信息
print("----------------------------------------------------------------------------");
print("Direction: " + dir);
print("Lines: " + lines);
print(string.Format("minIndex: {0} , maxIndex: {1}", minIndex, maxIndex));
print("Capacity: " + (maxIndex - minIndex + 1));
print("----------------------------------------------------------------------------");
}
#endif
// 更新滚动区域的大小
public void UpdateSize()
{
Rect rect = GetComponent<RectTransform>().rect;
planeH = rect.height;
planeW = rect.width;
}
// 滑动事件
protected virtual void ScrollRectListener(Vector2 value)
{
UpdateCheck();
}
private void UpdateCheck()
{
if (cellInfos == null) return;
// 检查超出范围
minIndex = -1;
for (int i = 0, length = cellInfos.Length; i < length; i++)
{
CellInfo cellInfo = cellInfos[i];
GameObject obj = cellInfo.obj;
Vector3 pos = cellInfo.pos;
float rangePos = dir == E_Direction.Vertical ? pos.y : pos.x;
// 判断是否超出显示范围
if (IsOutRange(rangePos))
{
// 把超出范围的cell 扔进 poolsObj里
if (obj != null)
{
Func(FuncCallBackFunc_OutRange, obj);
SetPoolsObj(obj);
cellInfos[i].obj = null;
}
}
else
{
if (obj == null)
{
// 优先从 poolsObj中 取出 poolsObj为空则返回 实例化的cell
GameObject cell = GetPoolsObj();
cell.gameObject.name = i.ToString();
Func(FuncCallBackFunc_Last, cell);
cell.transform.localPosition = pos;
cellInfos[i].obj = cell;
Func(FuncCallBackFunc, cell);
}
minIndex = minIndex == -1 ? i : minIndex;
maxIndex = i;
}
}
}
// 判断是否超出显示范围
protected bool IsOutRange(float pos)
{
Vector3 listP = contentRectTrans.anchoredPosition;
if (dir == E_Direction.Vertical)
{
if (pos + listP.y > cellH || pos + listP.y < -rectTrans.rect.height)
{
return true;
}
}
else // 水平方向 (包括从左到右和从右到左)
{
if (pos + listP.x < -cellW || pos + listP.x > rectTrans.rect.width)
{
return true;
}
}
return false;
}
//取出 cell
protected virtual GameObject GetPoolsObj()
{
GameObject cell = null;
if (Pool.Count > 0) cell = Pool.Pop();
if (cell == null)
{
cell = Instantiate(this.cell) as GameObject;
cell.transform.localPosition = Vector3.zero;
}
cell.transform.SetParent(content.transform);
cell.transform.localPosition = Vector3.zero;
cell.transform.localScale = Vector3.one;
SetActive(cell, true);
return cell;
}
//存入 cell
protected virtual void SetPoolsObj(GameObject cell)
{
if (cell != null)
{
Pool.Push(cell);
SetActive(cell, false);
}
}
//回调
protected void Func(Action<GameObject, int> func, GameObject selectObject, bool isUpdate = false)
{
if (selectObject != null)
{
int index = int.Parse(selectObject.name);
if (func != null)
{
func(selectObject, index);
}
}
}
public void DisposeAll()
{
if (FuncCallBackFunc != null) FuncCallBackFunc = null;
if (FuncOnClickCallBack != null) FuncOnClickCallBack = null;
if (FuncOnButtonClickCallBack != null) FuncOnButtonClickCallBack = null;
}
protected void OnDestroy()
{
DisposeAll();
}
public virtual void OnClickCell(GameObject cell)
{
}
//-> ExpandCircularScrollView 函数
public virtual void OnClickExpand(int index)
{
}
//-> FlipCircularScrollView 函数
public virtual void SetToPageIndex(int index)
{
}
public virtual void OnBeginDrag(PointerEventData eventData)
{
}
public void OnDrag(PointerEventData eventData)
{
}
public virtual void OnEndDrag(PointerEventData eventData)
{
}
protected void OnDragListener(Vector2 value)
{
float normalizedPos = dir == E_Direction.Vertical
? scrollRect.verticalNormalizedPosition
: scrollRect.horizontalNormalizedPosition;
if (dir == E_Direction.Vertical)
{
if (contentH - rectTrans.rect.height < 10)
{
SetActive(firstArrow, false);
SetActive(endArrow, false);
return;
}
}
else
{
if (contentW - rectTrans.rect.width < 10)
{
SetActive(firstArrow, false);
SetActive(endArrow, false);
return;
}
}
if (normalizedPos >= 0.9)
{
SetActive(firstArrow, false);
SetActive(endArrow, true);
}
else if (normalizedPos <= 0.1)
{
SetActive(firstArrow, true);
SetActive(endArrow, false);
}
else
{
SetActive(firstArrow, true);
SetActive(endArrow, true);
}
}
public GameObject GetCellGameObject(int index)
{
// 为了保证拿到正确数据根据index应该-1拿到正确数据
return cellInfos[--index].obj;
}
public List<GameObject> GetItems()
{
var list = new List<GameObject>();
foreach (var cellInfo in cellInfos)
{
if (cellInfo.obj == null)
continue;
list.Add(cellInfo.obj);
}
return list;
}
public int GetCellIndex(GameObject obj)
{
// 第0号是模板所以列表中索引应该-1
return Convert.ToInt32(obj.name) - 1;
}
protected void SetActive(GameObject obj, bool isActive)
{
if (obj != null)
{
obj.SetActive(isActive);
}
}
public int GetMinIndex()
{
return minIndex;
}
public int GetMaxIndex()
{
return maxIndex;
}
}
}