是这样,本来想学习学习一下这位大佬的滚动窗口,发现CV过去向下/向右滚动窗口没问题,反之则出现问题了,想着来重写修复一下,然后发现越写越像。。我的Version如下,新增了一个列表元素的点击回调事件:
24.8.3 更新子元素的锚点自定义:
FScrollView.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using System; public class FScrollView : ScrollRect { /// <summary> /// 是否为水平滚动 /// </summary> [HideInInspector] public bool isHorizontal; /// <summary> /// 滚动元素的预制体 /// </summary> [HideInInspector] public GameObject itemPrefab; /// <summary> /// 元素大小 /// </summary> [HideInInspector] public Vector2 itemSize; /// <summary> /// 元素间隔 /// </summary> [HideInInspector] public Vector2 spaceSize; /// <summary> /// 每组每行多少个元素 /// </summary> [HideInInspector] public int row; /// <summary> /// 每组每列多少个元素 /// </summary> [HideInInspector] public int col; /// <summary> /// 一共有多少个元素 /// </summary> [HideInInspector] public int itemCount; /// <summary> /// 缓存在mask外的滚动元素数量 /// </summary> [HideInInspector] public int cacheCount; [HideInInspector] public Vector2 anchorMin; [HideInInspector] public Vector2 anchorMax; [HideInInspector] public Vector2 pivot; /// <summary> /// 滚动逻辑单位 /// </summary> FScrollBase scroll; public void Init(Action<FScrollItem> onRoll, Action<FScrollItem> onClick = null) { if(scroll != null) { scroll.Dispose(); scroll = null; } if (isHorizontal) { scroll = new FScrollHorizontal(this); } else { scroll = new FScrollVertical(this); } scroll.OnInit(onRoll, onClick); } private void Update() { scroll?.OnUpdate(); } protected override void OnDestroy() { base.OnDestroy(); scroll?.Dispose(); } /// <summary> /// 滚动窗口基类 /// </summary> public abstract class FScrollBase { /// <summary> /// 滚动方向 /// </summary> public enum MoveDir { Idle, Left, Right, Up, Down } MoveDir moveDir = MoveDir.Idle; public FScrollBase(FScrollView loopScrollView) { this.isHorizontal = loopScrollView.isHorizontal; this.content = loopScrollView.content; this.contentSize = content.rect.size; this.itemSize = loopScrollView.itemSize; this.spaceSize = loopScrollView.spaceSize; this.viewport = loopScrollView.viewport; this.viewSize = loopScrollView.viewport.rect.size; this.itemCount = loopScrollView.itemCount; this.row = loopScrollView.row; this.col = loopScrollView.col; this.cacheCount = loopScrollView.cacheCount; this.anchorMin = loopScrollView.anchorMin; this.anchorMax = loopScrollView.anchorMax; this.pivot = loopScrollView.pivot; this.itemPrefab = loopScrollView.itemPrefab; } public bool isHorizontal; public RectTransform content; Vector2 contentCurPos; Vector2 contentLastPos; public Vector2 contentSize; public Vector2 itemSize; public Vector2 spaceSize; /// <summary> /// 窗口内可视元素数量 /// </summary> public int visibleCount; public int itemCount; public int row; public int col; public int cacheCount; public int itemListCount; public Vector2 anchorMin; public Vector2 anchorMax; public Vector2 pivot; public List<FScrollItem> itemList; GameObject itemPrefab; RectTransform viewport; public Vector2 viewSize; /// <summary> /// 滚动回调事件 /// </summary> public Action<FScrollItem> OnRoll; /// <summary> /// 元素点击事件 /// </summary> public Action<FScrollItem> OnClick; public void OnInit(Action<FScrollItem> onRoll, Action<FScrollItem> onClick = null) { ClearGrid(); this.OnRoll = onRoll; this.OnClick = onClick; itemList = new List<FScrollItem>(); // 保证可视的元素不会超过元素的总数量 itemListCount = Mathf.Min(visibleCount + cacheCount, itemCount); for (int i = 0; i < itemListCount; i++) { RectTransform item = Instantiate(itemPrefab, content).GetComponent<RectTransform>(); item.sizeDelta = itemSize; SetAnchors(item); FScrollItem scrollItem = new FScrollItem(i, CheckIntactGroup(i), this, item); OnRoll?.Invoke(scrollItem); itemList.Add(scrollItem); } } /// <summary> /// 更新方向 /// </summary> public void OnUpdate() { contentCurPos = content.anchoredPosition; Vector2 dir = contentCurPos - contentLastPos; if (dir.x > 0) { moveDir = MoveDir.Right; } else if (dir.x < 0) { moveDir = MoveDir.Left; } else if (dir.y > 0) { moveDir = MoveDir.Up; } else if (dir.y < 0) { moveDir = MoveDir.Down; } else { moveDir = MoveDir.Idle; } contentLastPos = contentCurPos; if (moveDir != MoveDir.Idle) { OnMoving(moveDir); } } /// <summary> /// 滚动逻辑 /// 这里的滚动都指的是窗口(Content)的滚动,即你往下划,窗口向上滚动 /// </summary> /// <param name="moveDir"></param> protected abstract void OnMoving(MoveDir moveDir); /// <summary> /// 设置UI锚点 /// </summary> /// <param name="rect"></param> protected void SetAnchors(RectTransform rect) { rect.anchorMin = anchorMin; rect.anchorMax = anchorMax; rect.pivot = pivot; } protected abstract void SetContextAnchors(RectTransform rect); /// <summary> /// 检查是不是一个完整组 /// </summary> /// <returns></returns> protected abstract bool CheckIntactGroup(int itemIdx); /// <summary> /// 清空滚动元素 /// </summary> protected void ClearGrid() { if(content == null || content.childCount == 0) { return; } for(int i = 0; i < content.childCount; i++) { Destroy(content.GetChild(i).gameObject); } } /// <summary> /// 移除时操作 /// </summary> public void Dispose() { ClearGrid(); viewport = null; content = null; itemPrefab = null; if(itemList != null && itemListCount > 0) { for(int i = itemListCount - 1; i >= 0; i--) { itemList[i].Dispose(); itemList[i] = null; } itemList.Clear(); } } } /// <summary> /// 垂直滚动窗口 /// </summary> public class FScrollVertical : FScrollBase { public FScrollVertical(FScrollView scroll) : base(scroll) { this.visibleCount = col * (int)Math.Ceiling((viewSize.y + spaceSize.y) / (itemSize.y + spaceSize.y)); this.cacheCount = cacheCount * col; // 设置滚动范围 int groupNum = itemCount % col == 0 ? itemCount / col : itemCount / col + 1; SetContextAnchors(scroll.content); scroll.content.sizeDelta = new Vector2(viewSize.x, itemSize.y * groupNum + spaceSize.y * (groupNum - 1)); scroll.content.anchoredPosition = Vector2.zero; this.contentSize = scroll.content.sizeDelta; } /// <summary> /// 设置滚动元素居中对齐上方 /// </summary> /// <param name="rect"></param> protected override void SetContextAnchors(RectTransform rect) { rect.anchorMin = new Vector2(0.5f, 1); rect.anchorMax = new Vector2(0.5f, 1); rect.pivot = new Vector2(0.5f, 1); } protected override bool CheckIntactGroup(int itemIdx) { int leakItem = itemCount % col; if (leakItem == 0 || itemIdx<itemCount - leakItem) { return true; } return false; } protected override void OnMoving(MoveDir moveDir) { if (moveDir == MoveDir.Up) { if (itemList[itemListCount - 1].index >= itemCount - 1) { return; } MoveToUp(); } else if (moveDir == MoveDir.Down) { if (itemList[0].index == 0) { return; } MoveToDown(); } } void MoveToUp() { FScrollItem firstItem = itemList[0]; float firstItemDistance2Top = content.anchoredPosition.y + firstItem.position.y - itemSize.y; while (firstItemDistance2Top >= 0) { for (int i = 0; i < col; i++) { int targetIndex = itemList[itemListCount - 1].index + 1; if (targetIndex >= itemCount) { // 已经滚动到顶部 return; } firstItem.ChangeIndex(targetIndex, CheckIntactGroup(targetIndex)); OnRoll?.Invoke(firstItem); itemList.RemoveAt(0); itemList.Insert(itemListCount - 1, firstItem); firstItem = itemList[0]; } firstItemDistance2Top = content.anchoredPosition.y + firstItem.position.y - itemSize.y; } } void MoveToDown() { FScrollItem lastItem = itemList[itemListCount - 1]; float lastItemDistance2Buttom = -lastItem.position.y - content.anchoredPosition.y - viewSize.y; while(lastItemDistance2Buttom >= 0) { bool isIntactGroup = lastItem.isIntactGroup; for(int i = 0; i < col; i++) { int targetIndex = itemList[0].index - 1; if(targetIndex < 0) { // 已经滚动到底部 return; } lastItem.ChangeIndex(targetIndex, CheckIntactGroup(targetIndex)); OnRoll?.Invoke(lastItem); itemList.RemoveAt(itemListCount - 1); itemList.Insert(0, lastItem); lastItem = itemList[itemListCount - 1]; if(!isIntactGroup && lastItem.isIntactGroup) { // 当前元素组已经轮换完 break; } } lastItemDistance2Buttom = -lastItem.position.y - content.anchoredPosition.y - viewSize.y; } } } /// <summary> /// 水平滚动窗口 /// </summary> public class FScrollHorizontal : FScrollBase { public FScrollHorizontal(FScrollView scroll) : base(scroll) { this.visibleCount = row * (int)Math.Ceiling((viewSize.x + spaceSize.x) / (itemSize.x + spaceSize.x)); this.cacheCount = cacheCount * row; // 设置滚动范围 int groupNum = itemCount % row == 0 ? itemCount / row : itemCount / row + 1; SetContextAnchors(scroll.content); scroll.content.sizeDelta = new Vector2(itemSize.x * groupNum + spaceSize.x * (groupNum - 1), viewSize.y); scroll.content.anchoredPosition = Vector2.zero; this.contentSize = scroll.content.sizeDelta; } /// <summary> /// 设置滚动元素居中对齐左侧 /// </summary> /// <param name="rect"></param> protected override void SetContextAnchors(RectTransform rect) { rect.anchorMin = new Vector2(0, 0.5f); rect.anchorMax = new Vector2(0, 0.5f); rect.pivot = new Vector2(0, 0.5f); } protected override bool CheckIntactGroup(int itemIdx) { int leakItem = itemCount % row; if (leakItem == 0 || itemIdx < itemCount - leakItem) { return true; } return false; } protected override void OnMoving(MoveDir moveDir) { if (moveDir == MoveDir.Left) { if (itemList[itemListCount - 1].index >= itemCount - 1) { return; } MoveToLeft(); } else if (moveDir == MoveDir.Right) { if (itemList[0].index == 0) { return; } MoveToRight(); } } void MoveToLeft() { FScrollItem firstItem = itemList[0]; // 最左侧元素锚点离左边框的距离 float firstItemDis2Left = content.anchoredPosition.x + firstItem.position.x + itemSize.x; while(firstItemDis2Left <= 0) { for(int i = 0; i < row; i++) { int targetIndex = itemList[itemListCount - 1].index + 1; if(targetIndex >= itemCount) { // 已经滚到最左侧 return; } firstItem.ChangeIndex(targetIndex, CheckIntactGroup(targetIndex)); OnRoll?.Invoke(firstItem); itemList.RemoveAt(0); itemList.Insert(itemListCount - 1, firstItem); firstItem = itemList[0]; } firstItemDis2Left = content.anchoredPosition.x + firstItem.position.x + itemSize.x; } } void MoveToRight() { FScrollItem lastItem = itemList[itemListCount - 1]; // 最右侧元素的锚点离右边框的距离 float lastItemDis2Right = -content.anchoredPosition.x + viewSize.x - lastItem.position.x; while(lastItemDis2Right <= 0) { bool isIntactGroup = lastItem.isIntactGroup; for(int i = 0; i < row; i++) { int targetIndex = itemList[0].index - 1; if(targetIndex < 0) { // 已经滚动到最右侧 return; } lastItem.ChangeIndex(targetIndex, CheckIntactGroup(targetIndex)); OnRoll?.Invoke(lastItem); itemList.RemoveAt(itemListCount - 1); itemList.Insert(0, lastItem); lastItem = itemList[itemListCount - 1]; if(!isIntactGroup && lastItem.isIntactGroup) { // 当前元素组已经轮换完 return; } } lastItemDis2Right = -content.anchoredPosition.x + viewSize.x - lastItem.position.x; } } } /// <summary> /// 滚动元素逻辑单元 /// </summary> public class FScrollItem { public FScrollItem(int index, bool isIntact, FScrollBase scroll, RectTransform rect) { this.index = index; this.isIntactGroup = isIntact; this.isHorizontal = scroll.isHorizontal; this.row = scroll.row; this.col = scroll.col; this.itemSize = scroll.itemSize; this.spaceSize = scroll.spaceSize; position = GetItemPos(); if (rect) { this.rect = rect; this.rect.anchoredPosition = position; this.rect.name = index.ToString(); this.btn = rect.GetComponent<Button>(); if (this.btn) { this.btn.onClick.AddListener(() => { scroll.OnClick?.Invoke(this); }); } } } bool isHorizontal; /// <summary> /// 滚动元素序号 /// </summary> public int index; int row; int col; /// <summary> /// 本元素是否所属完整的一组 /// </summary> public bool isIntactGroup; Vector2 itemSize; Vector2 spaceSize; public Vector2 position; public RectTransform rect; public Button btn; public void ChangeIndex(int index, bool isIntact) { this.index = index; position = GetItemPos(); rect.anchoredPosition = position; rect.name = index.ToString(); isIntactGroup = isIntact; } /// <summary> /// 获取元素行列对应的实际坐标 /// </summary> /// <returns></returns> Vector2 GetItemPos() { Vector2Int gridPos = GetItemGridPos(); if(isHorizontal) { return new Vector2(gridPos.x * (itemSize.x + spaceSize.x), gridPos.y * (itemSize.y + spaceSize.y)); } else { return new Vector2(gridPos.y * (itemSize.x + spaceSize.x), -gridPos.x * (itemSize.y + spaceSize.y)); } } /// <summary> /// 获取元素在(第几行,第几列)的索引坐标 /// </summary> /// <returns></returns> Vector2Int GetItemGridPos() { if(isHorizontal) { return new Vector2Int(index / row, index % row); } else { return new Vector2Int(index / col, index % col); } } public void Dispose() { rect = null; } } }
需要重写一下该类的Editor,不然会根据ScrollRect的界面来显示
FScrollViewEditor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
[CustomEditor(typeof(FScrollView))]
public class FScrollViewEditor : Editor
{
private FScrollView scroll;
private SerializedProperty prefab;
private void Awake()
{
scroll = target as FScrollView;
prefab = this.serializedObject.FindProperty("itemPrefab");
}
public override void OnInspectorGUI()
{
EditorGUILayout.BeginHorizontal();
scroll.isHorizontal = EditorGUILayout.Toggle("是否为横向滑动(isHorizontal)", scroll.isHorizontal);
scroll.horizontal = scroll.isHorizontal;
scroll.vertical = !scroll.horizontal;
if (scroll.isHorizontal)
{
scroll.row = EditorGUILayout.IntField("行数", scroll.row);
}
else
{
scroll.col = EditorGUILayout.IntField("列数", scroll.col);
}
EditorGUILayout.EndHorizontal();
scroll.itemSize = EditorGUILayout.Vector2Field("元素大小", scroll.itemSize);
scroll.spaceSize = EditorGUILayout.Vector2Field("元素间隔", scroll.spaceSize);
scroll.itemCount = EditorGUILayout.IntField("元素数量", scroll.itemCount);
scroll.cacheCount = EditorGUILayout.IntField("缓存数量", scroll.cacheCount);
EditorGUILayout.PropertyField(serializedObject.FindProperty("itemPrefab"), new GUIContent("元素预制体"));
if (GUI.changed)
{
// 保存在预制体下对属性的修改
Undo.RecordObject(scroll, "modify scroll");
EditorUtility.SetDirty(scroll);
serializedObject.ApplyModifiedProperties();
}
base.OnInspectorGUI();
}
}
然后对应的使用方法就是
FScrollView scroll;
scroll.Init(
(item)=>
{
Func1(); // 元素滚动事件(可用来对元素内容进行更新)
},
(item)=>
{
Func2(); // 你的点击事件
});