[Unity] ScrollView组件优化-回收机制的滚动窗口

原文链接http://t.csdnimg.cn/D46xT

是这样,本来想学习学习一下这位大佬的滚动窗口,发现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(); // 你的点击事件
});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值