// Author: 文若 // CreateDate: 2022/10/26 // ## 生成滑动列表必须以下步骤: // 1. 持有RecycleView对象rv,rv.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 FuncCallBackFunc_Last; protected Action FuncCallBackFunc; protected Action FuncOnClickCallBack; protected Action FuncOnButtonClickCallBack; protected Action 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 Pool = new Stack(); protected bool isInited = false; public bool GetIsInit() { return isInited; } public virtual void Init(Action callBack) { Init(callBack, null); } public virtual void Init2(Action callBack, Action callBack_last) { Init2(callBack, callBack_last, null); } public virtual void Init(Action callBack, Action onClickCallBack, Action onButtonClickCallBack) { if (onButtonClickCallBack != null) { FuncOnButtonClickCallBack = onButtonClickCallBack; } Init(callBack, onClickCallBack); } public virtual void Init2(Action callBack, Action callBack_last, Action onClickCallBack) { this.FuncCallBackFunc_Last = callBack_last; Init(callBack, onClickCallBack); } public virtual void Init3(Action callBack, Action outRangeCallBack) { this.FuncCallBackFunc_OutRange = outRangeCallBack; Init(callBack, null); } public virtual void Init(Action callBack, Action onClickCallBack) { DisposeAll(); FuncCallBackFunc = callBack; if (onClickCallBack != null) { FuncOnClickCallBack = onClickCallBack; } if (isInit) return; content = this.GetComponent().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(); cellRectTrans.pivot = new Vector2(0f, 1f); CheckAnchor(cellRectTrans); cellRectTrans.anchoredPosition = Vector2.zero; // 记录 Cell 信息 cellH = cellRectTrans.rect.height; cellW = cellRectTrans.rect.width; // 记录 Plane 信息 rectTrans = GetComponent(); Rect planeRect = rectTrans.rect; planeH = planeRect.height; planeW = planeRect.width; // 记录 Content 信息 contentRectTrans = content.GetComponent(); 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.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); } } } } /// /// 刷新某一项 index 从1开始 /// /// 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().anchoredPosition = tempCellInfo.pos; tempCellInfo.obj.transform.GetComponent().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().anchoredPosition = cellInfo.pos; cell.transform.GetComponent().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); } /// /// 通过index定位到某一单元格的坐标位置 /// /// 索引ID 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(); } /// /// 获取某一单元格 /// /// /// 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().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 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 GetItems() { var list = new List(); 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; } } }