无限滚动 ScrollRect

在这里插入图片描述

无限滚动的ScrollRect
2种布局方式,水平或者垂直
支持跳转索引
支持设置内容的pivot和anchor,从上面开始或者下面开始都可以

原理

创建与数据总数对应的矩形
每帧遍历矩形是否与遮罩矩形相交
如果相交添加到索引列表
遍历格子索引
如果索引存在索引列表里面,列表移除该索引
如果不在索引列表里面,把格子索引设置为-1
遍历索引列表
找一个没有绘制的格子对索引绘制

布局

水平布局:
在这里插入图片描述
格子坐标:

cellPos = new Vector2(index / row,index % row);

垂直布局:
在这里插入图片描述
格子坐标:

cellPos = new Vector2(index % column,index / column);

从公式可以看出水平和垂直只是2个坐标轴调换了一下
已知 Vector2 可以使用this索引访问数据
所以只需要添加一个轴变量即可转换布局

        var pos = Vector2.zero;
        pos[asix] = index / layoutValue;
        pos[(asix + 1) % 2] = index % layoutValue;

显示

首先创建最大可显示数量的格子
只需要 遮罩大小 / 格子大小 +1 就可获得在遮罩内可显示格子数量
200/50 = 4,但是当你拖动列表的时候,就会需要5个格子显示

        var xCount = (Mathf.CeilToInt(m_ViewRect.width / m_CellSize.x) + 1);
        var yCount = (Mathf.CeilToInt(m_ViewRect.height / m_CellSize.y) + 1);
        xCount = !isHorizontal ? Math.Min(xCount, layoutValue) : xCount;
        yCount = isHorizontal ? Math.Min(yCount, layoutValue) : yCount;
        m_RenderCount = xCount * yCount;
渲染

更新遮罩区域坐标,通过计算当前内容的坐标,描点,中心轴,得到遮罩的坐标

        m_ViewRect.x = m_ContentPivotOffset.x - m_ContentRect.anchoredPosition.x - m_ContentAnchorOffset.x;
        m_ViewRect.y = m_ContentPivotOffset.y - m_ViewRect.height - (m_ContentRect.anchoredPosition.y) + m_CellSize.y + m_ContentAnchorOffset.y;

获取与遮罩区域相交的格子

        var count = itemRects.Count;
        for (var i = 0; i < count; i++)
        {
            if (m_ViewRect.Overlaps(itemRects[i]))
            {
                indexs.Add(i);
            }
        }

移除indexs列表中正在显示的格子

        foreach (var item in items)
        {
            if (!indexs.Contains(item.index))
            {
                item.index = -1;
                item.transform.gameObject.SetActive(false);
            }
            else
            {
                indexs.Remove(item.index);
            }
        }

设置格子坐标并渲染

        foreach (var index in indexs)
        {
            var renderRect = GetNullRenderRect();
            RenderItem(renderRect, index);
            renderRect.transform.gameObject.SetActive(true);
            renderRect.index = index;
        }

    void RenderItem(RenderRect rect,int index)
    {
        rect.transform.anchoredPosition = GetRenderItemPos(index);
        onRenderItem?.Invoke(index, rect.transform.gameObject);
    }

在格子上按下

使用OnPointerDown事件接口获取当前点击的对象
向上访问父亲链,如果与格子相等就是点击的索引

    void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
    {
        if (eventData.pointerEnter == null)
            return;

        var pointer = eventData.pointerEnter.transform as RectTransform;

        if (pointer == m_ScrollRect.viewport || pointer == m_ScrollRect.transform)
            return;

        while (pointer != m_ScrollRect.transform)
        {
            foreach (var item in items)
            {
                if (item.transform == pointer)
                {
                    onClickItem?.Invoke(item.index);
                    return;
                }
            }
            pointer = pointer.parent as RectTransform;
        }
    }

跳转索引

根据索引获取跳转坐标,在LateUpdate插值坐标
坐标应设置边界,例如滑动最大值为100,格子坐标为150
当设置为150的时候永远也无法到达,这是不允许的
如果内容比遮罩还小,没必要跳转
假如开启了插值跳转,并且正在跳转中,你滑动了列表,此时应停止跳转

    public void MoveToIndex(int index)
    {
        if (index < 0 || index >= itemRects.Count)
            return;

        m_TargetPos = itemRects[index].position * new Vector2(-1, -1) ;
        m_TargetPos += m_ContentAnchorOffset * new Vector2(-1, 1);
        m_TargetPos += m_ContentPivotOffset;
        
        m_TargetPos.x = Mathf.Max(m_TargetPos.x, -m_ContentRect.sizeDelta.x + m_ViewRect.width - m_ContentAnchorOffset.x + m_ContentPivotOffset.x);
        m_TargetPos.y = Math.Min(m_TargetPos.y, m_ContentRect.sizeDelta.y - m_ViewRect.height + m_ContentAnchorOffset.y + m_ContentPivotOffset.y);

        m_TargetPos.x = m_ContentRect.sizeDelta.x - m_ViewRect.width < 0 ? 0 : m_TargetPos.x;
        m_TargetPos.y = m_ContentRect.sizeDelta.y - m_ViewRect.height < 0 ? 0 : m_TargetPos.y;


        if (m_ScrollRect.inertia)
        {
            m_IsMoveToTarget = true;
        }
        else
        {
            m_ContentRect.anchoredPosition = m_TargetPos;
        }
    }
    
    private void LateUpdate()
    {
        if (!m_IsMoveToTarget)
            return;

        m_ContentRect.anchoredPosition = Vector2.Lerp(m_ContentRect.anchoredPosition, m_TargetPos, m_ScrollRect.decelerationRate);

        if ((m_ContentRect.anchoredPosition - m_TargetPos).sqrMagnitude < 0.05f)
        {
            m_ContentRect.anchoredPosition = m_TargetPos;
            m_IsMoveToTarget = false;
        }
    }

    void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
    {
        m_IsMoveToTarget = false;
    }

demo

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Test : MonoBehaviour
{
    public Button addData;
    public Button gotoIndex;
    public Button changedLayout;
    public ListView listView;
    private void Awake()
    {
        listView.onRenderItem = OnRenderItem;
        listView.onClickItem = OnClickItem;

        addData.onClick.AddListener(OnAddData);
        gotoIndex.onClick.AddListener(OnMoveToIndex);

        bool isHorizontal = true;
        changedLayout.onClick.AddListener(()=> {
            listView.UpdateLayout(isHorizontal);
            isHorizontal = !isHorizontal;
        });
    }

    void OnRenderItem(int index,GameObject item)
    {
        item.GetComponentInChildren<Text>().text = index.ToString();
    }

    void OnClickItem(int index)
    {
        Debug.Log("click "+index);
    }

    void OnAddData()
    {
        listView.Count = Random.Range(0,100);
        Debug.Log($"添加数据:"+listView.Count);
    }

    void OnMoveToIndex()
    {
        var index = Random.Range(0, listView.Count);
        listView.MoveToIndex(index);
        Debug.Log($"moveTo {index}");
    }
}

完整源码

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class ListView : MonoBehaviour, IBeginDragHandler, IPointerDownHandler
{
    class RenderRect
    {
        public RectTransform transform;
        public int index = -1;
    }

    /// <summary>
    /// 生成模板
    /// </summary>
    public GameObject itemTemplate;
    /// <summary>
    /// 是否水平布局
    /// </summary>
    [SerializeField] private bool isHorizontal = true;
    /// <summary>
    /// 如果是水平布局,该值为垂直数量,如果是垂直布局,该值为水平数量
    /// </summary>
    public int layoutValue;
    /// <summary>
    /// 间隔
    /// </summary>
    public Vector2 spacing;
    /// <summary>
    /// 渲染回调
    /// </summary>
    public Action<int, GameObject> onRenderItem;
    /// <summary>
    /// 点击回调
    /// </summary>
    public Action<int> onClickItem;

    private bool m_isInitialize = false;
    private bool m_IsMoveToTarget = false;
    private int m_Asix;
    private int m_Count;
    private int m_RenderCount;
    private Vector2 m_SpacingPivot;
    private Vector2 m_CellPivotOffset;
    private Vector2 m_TargetPos;
    private Vector2 m_CellSize;
    private Vector2 m_ContentAnchor;
    private Vector2 m_ItemAnchor;
    private Vector2 m_CellAnchorOffset;
    private Vector2 m_ContentAnchorOffset;
    private Vector2 m_ContentPivotOffset;
    private Rect m_ViewRect;
    private ScrollRect m_ScrollRect;
    private RectTransform m_ContentRect;

    private List<Rect> itemRects = new List<Rect>();
    private List<RenderRect> items = new List<RenderRect>();
    private List<int> indexs = new List<int>();

    /// <summary>
    /// 数据数量
    /// 生成与数据数量相等的格子矩形
    /// </summary>
    public int Count
    {
        get
        {
            return m_Count;
        }
        set
        {
            var v = value - m_Count;
            m_Count = value;
            if (v > 0)
            {
                for (var i = 0; i < v; i++)
                {
                    itemRects.Add(GetLayout(itemRects.Count, m_Asix));
                }
            }
            else
            {
                v = Mathf.Abs(v);
                itemRects.RemoveRange(itemRects.Count - v, v);
            }
            UpdateContentSize(m_Asix);
            RefreshList();
        }
    }

    /// <summary>
    /// 移动至目标索引
    /// </summary>
    /// <param name="index"></param>
    public void MoveToIndex(int index)
    {
        if (index < 0 || index >= itemRects.Count)
            return;

        m_TargetPos = itemRects[index].position * new Vector2(-1, -1) ;
        m_TargetPos += m_ContentAnchorOffset * new Vector2(-1, 1);
        m_TargetPos += m_ContentPivotOffset;
        
        m_TargetPos.x = Mathf.Max(m_TargetPos.x, -m_ContentRect.sizeDelta.x + m_ViewRect.width - m_ContentAnchorOffset.x + m_ContentPivotOffset.x);
        m_TargetPos.y = Math.Min(m_TargetPos.y, m_ContentRect.sizeDelta.y - m_ViewRect.height + m_ContentAnchorOffset.y + m_ContentPivotOffset.y);

        m_TargetPos.x = m_ContentRect.sizeDelta.x - m_ViewRect.width < 0 ? 0 : m_TargetPos.x;
        m_TargetPos.y = m_ContentRect.sizeDelta.y - m_ViewRect.height < 0 ? 0 : m_TargetPos.y;


        if (m_ScrollRect.inertia)
        {
            m_IsMoveToTarget = true;
        }
        else
        {
            m_ContentRect.anchoredPosition = m_TargetPos;
        }
    }
    
    /// <summary>
    /// 初始化
    /// </summary>
    public void Initialize()
    {
        if (m_isInitialize)
            return;
        m_ScrollRect = GetComponent<ScrollRect>();
        m_ViewRect = new Rect(Vector2.zero, (m_ScrollRect.transform as RectTransform).sizeDelta);

        m_ContentRect = m_ScrollRect.content;
        m_ContentAnchor = (m_ContentRect.anchorMax + m_ContentRect.anchorMin) * 0.5f;

        var itemRect = itemTemplate.transform as RectTransform;
        m_CellSize = itemRect.rect.size + spacing;
        m_ItemAnchor = (itemRect.anchorMax + itemRect.anchorMin) * 0.5f;
        m_CellPivotOffset = m_CellSize * (itemRect.pivot - Vector2.up);
         
         m_ContentAnchorOffset = new Vector2(m_ContentAnchor.x, (1 - m_ContentAnchor.y)) * m_ViewRect.size;


        m_SpacingPivot = spacing * new Vector2(-itemRect.pivot.x, 1 - itemRect.pivot.y);

        if (Mathf.Approximately(m_ScrollRect.decelerationRate, 0))
        {
            m_ScrollRect.decelerationRate = 1;
        }

        UpdateLayout(isHorizontal);
        m_isInitialize = true;
    }

    /// <summary>
    /// 更换布局
    /// </summary>
    /// <param name="isHorizontal"></param>
    public void UpdateLayout(bool isHorizontal)
    {
        if(m_isInitialize && this.isHorizontal==isHorizontal)
            return;

        layoutValue = Mathf.Max(layoutValue, 1);
        m_Asix = isHorizontal ? 0 : 1;

        var xCount = (Mathf.CeilToInt(m_ViewRect.width / m_CellSize.x) + 1);
        var yCount = (Mathf.CeilToInt(m_ViewRect.height / m_CellSize.y) + 1);
        xCount = !isHorizontal ? Math.Min(xCount, layoutValue) : xCount;
        yCount = isHorizontal ? Math.Min(yCount, layoutValue) : yCount;
        m_RenderCount = xCount * yCount;

        var v = m_RenderCount - items.Count;
        if (v > 0)
        {
            for (var i = 0; i < v; i++)
            {
                var go = GameObject.Instantiate(itemTemplate);
                go.transform.SetParent(itemTemplate.transform.parent, false);
                items.Add(new RenderRect
                {
                    transform = go.transform as RectTransform
                });
            }
        }

        foreach (var item in items)
        {
            item.index = -1;
        }

        itemRects.Clear();
        var count = m_Count;
        m_Count = 0;
        Count = count;
        this.isHorizontal = isHorizontal;
    }

    /// <summary>
    /// 数据更新后手动刷新列表
    /// </summary>
    public void RefreshList()
    {
        foreach (var item in items)
        {
            if(item.index!=-1)
            {
                RenderItem(item, item.index);
            }
        }
    }

    private void Awake()
    {
        Initialize();
    }

    /// <summary>
    /// 移动到目标
    /// </summary>
    private void LateUpdate()
    {
        if (!m_IsMoveToTarget)
            return;

        m_ContentRect.anchoredPosition = Vector2.Lerp(m_ContentRect.anchoredPosition, m_TargetPos, m_ScrollRect.decelerationRate);

        if ((m_ContentRect.anchoredPosition - m_TargetPos).sqrMagnitude < 0.05f)
        {
            m_ContentRect.anchoredPosition = m_TargetPos;
            m_IsMoveToTarget = false;
        }
    }

    /// <summary>
    /// 渲染格子
    /// </summary>
    private void Update()
    {
        m_ViewRect.x = m_ContentPivotOffset.x - m_ContentRect.anchoredPosition.x - m_ContentAnchorOffset.x;
        m_ViewRect.y = m_ContentPivotOffset.y - m_ViewRect.height - (m_ContentRect.anchoredPosition.y) + m_CellSize.y + m_ContentAnchorOffset.y;

        var count = itemRects.Count;
        for (var i = 0; i < count; i++)
        {
            if (m_ViewRect.Overlaps(itemRects[i]))
            {
                indexs.Add(i);
            }
        }

        foreach (var item in items)
        {
            if (!indexs.Contains(item.index))
            {
                item.index = -1;
                item.transform.gameObject.SetActive(false);
            }
            else
            {
                indexs.Remove(item.index);
            }
        }


        foreach (var index in indexs)
        {
            var renderRect = GetNullRenderRect();
            RenderItem(renderRect, index);
            renderRect.transform.gameObject.SetActive(true);
            renderRect.index = index;
        }
        indexs.Clear();
    }

    void RenderItem(RenderRect rect,int index)
    {
        rect.transform.anchoredPosition = GetRenderItemPos(index);
        onRenderItem?.Invoke(index, rect.transform.gameObject);
    }

    Vector2 GetRenderItemPos(int index)
    {
        return itemRects[index].position + m_CellPivotOffset + m_CellAnchorOffset + m_SpacingPivot;
    }

    /// <summary>
    /// 找一个空闲的格子绘制
    /// </summary>
    /// <returns></returns>
    RenderRect GetNullRenderRect()
    {
        foreach (var item in items)
        {
            if (item.index == -1)
            {
                return item;
            }
        }
        throw new System.Exception("Error");
    }

    /// <summary>
    /// 获取当前布局下,index的矩形
    /// </summary>
    /// <param name="index"></param>
    /// <param name="asix"></param>
    /// <returns></returns>
    Rect GetLayout(int index, int asix)
    {
        var pos = Vector2.zero;
        pos[asix] = index / layoutValue;
        pos[(asix + 1) % 2] = index % layoutValue;

        return new Rect(pos * new Vector2(1, -1) * m_CellSize, m_CellSize);
    }

    /// <summary>
    /// 更新内容大小
    /// </summary>
    /// <param name="asix"></param>
    void UpdateContentSize(int asix)
    {
        var count = Vector2.zero;
        count[asix] = Mathf.CeilToInt((float)Count / layoutValue);
        count[(asix + 1) % 2] = layoutValue;
        m_ContentRect.sizeDelta = count * m_CellSize - spacing;

        m_CellAnchorOffset = m_ContentRect.rect.size * new Vector2(-m_ItemAnchor.x, 1 - m_ItemAnchor.y);
        m_ContentPivotOffset = m_ContentRect.rect.size * new Vector2(m_ContentRect.pivot.x, -(1 - m_ContentRect.pivot.y));
    }

    void IBeginDragHandler.OnBeginDrag(PointerEventData eventData)
    {
        m_IsMoveToTarget = false;
    }

    void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
    {
        if (eventData.pointerEnter == null)
            return;

        var pointer = eventData.pointerEnter.transform as RectTransform;

        if (pointer == m_ScrollRect.viewport || pointer == m_ScrollRect.transform)
            return;

        while (pointer != m_ScrollRect.transform)
        {
            foreach (var item in items)
            {
                if (item.transform == pointer)
                {
                    onClickItem?.Invoke(item.index);
                    return;
                }
            }
            pointer = pointer.parent as RectTransform;
        }
    }
}




评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小鱼游戏开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值