一、效果展示

unity中无限滚动_unity

二、使用自带组件实现

为了让「Scroll View」组件实现内容框自适应大小,我们可以在「Content」上挂载「Vertical Layout Group」组件和「Content Size Fitter」组件。

unity中无限滚动_滚动视图_02

效果如下

unity中无限滚动_加载数据_03

但这种实现方式有诸多问题。一方面,「Vertical Layout Group」这类排序组件可能会增加性能消耗;另一方面,「Content Size Fitter」必须靠子项才能撑开,当子项数量较多但又只需要展示少数几条时,会造成内存的浪费。因此我们需要自己实现一个优化版的滚动视图。

三、手动实现

3.1 准备工作

首先在场景中创建一个「Scroll View」并命名为「ScrollViewPlus」,在其上挂载一个同名脚本。创建一个子物体脚本命名为「ScrollViewPlusItem」。创建名为「ScrollViewPlusItem」的预制体,用于之后实例化子物体。

3.2 生成子物体

在生成子物体之前,首先要计算出最少需要的子物体数量。子物体的数量通过子物体的高度与间距就可以计算出来,但需要考虑到当前视口高度无法整除子物体高度加间距的情况。这种情况下就需要向上取整。另外还需要在计算出来的数量上加1,以避免在滚动时“露馅”。

unity中无限滚动_unity中无限滚动_04

代码如下:

public class ScrollViewPlus : MonoBehaviour
{
    // 子物体预制体
    public GameObject ItemPrefab;
    // 子物体间距
    public float ItemOffset;
    // 子物体高度
    private float _itemHeight;
    private RectTransform _content;
    private List<ScrollViewPlusItem> _items;
    void Start()
    {
        _items = new List<ScrollViewPlusItem>();
        _content = transform.Find("Viewport/Content").GetComponent<RectTransform>();
        _itemHeight = ItemPrefab.GetComponent<RectTransform>().rect.height;
        int num = GetShowItemNum(_itemHeight, ItemOffset);
        SpawnItem(num, ItemPrefab);
    }
    /// <summary>
    /// 获取展示区域子元素个数
    /// </summary>
    /// <param name="itemHeight"></param>
    /// <param name="itemOffset"></param>
    /// <returns></returns>
    private int GetShowItemNum(float itemHeight, float itemOffset)
    {
        float height = GetComponent<RectTransform>().rect.height;
        return Mathf.CeilToInt(height / (itemHeight + itemOffset)) + 1;
    }
    /// <summary>
    /// 生成子物体
    /// </summary>
    /// <param name="num"></param>
    /// <param name="itemPrefab"></param>
    private void SpawnItem(int num,GameObject itemPrefab)
    {
        for (int i = 0; i < num; i++)
        {
            GameObject item = Instantiate(itemPrefab, _content);
            _items.Add(item.AddComponent<ScrollViewPlusItem>());
        }
    }

3.3 加载数据

在实际的项目中,滚动视图的每个元素肯定是不同的,它们的数据可能是从本地加载或是从远程服务器加载。这里只简单模拟一下加载数据的情况。首先创建一个「ScrollViewPlusItemModel」类用于封装数据

public class ScrollViewPlusItemModel
{
    public Sprite Icon { get; }
    public string Describe { get; }

    public ScrollViewPlusItemModel(Sprite sprite, string describe)
    {
        Icon = sprite;
        Describe = describe;
    }
}

然后在「ScrollViewPlus」类中定义一个加载数据的方法

private List<ScrollViewPlusItemModel> _models;
/// <summary>  
/// 加载数据  
/// </summary>
private void GetModels()
{
	var sprites = Resources.LoadAll<Sprite>("Icon");
	foreach (var sprite in sprites)
	{
		_models.Add(new ScrollViewPlusItemModel(sprite,sprite.name));
	}
}

数据加载后,还需要根据数据的数量动态调节Content的大小

/// <summary>  
/// 设置内容大小  
/// </summary>
private void SetContentSize()  
{  
    float y = _models.Count * _itemHeight + (_models.Count - 1) * ItemOffset;  
    _content.sizeDelta = new Vector2(_content.sizeDelta.x, y);  
}

3.4 实现滚动

传统的滚动视图需要把所有的子元素全部加载到内存中,造成资源浪费。我们的优化版滚动视图只需要实例化有限的几个子元素,在滚动过程中通过将视口之外的子元素挪回视口中,实现虚假的滚动效果。

以从下向上滑动为例,视口中可以容纳5个元素,我们一共生成了6个。当向上滑动时,Item1被划出了视口区间外。因此需要把Item1重新挪动到队尾,并改变Item1上展示的内容。此时继续向上滑动,Item1就会出现在视口的最下方。

unity中无限滚动_unity中无限滚动_05

接下来将思路转换成代码。首先在「ScrollViewPlusItem」类中定义出需要用到的属性和方法

public class ScrollViewPlusItem : MonoBehaviour
{
    private RectTransform _rect;
    private RectTransform Rect
    {
        get
        {
            if (_rect == null)
                _rect = GetComponent<RectTransform>();
            return _rect;
        }
    }

    private Image _img;
    private Image Img
    {
        get
        {
            if (_img == null)
                _img = transform.Find("Icon").GetComponent<Image>();
            return _img;
        }
    }

    private TMP_Text _txt;
    private TMP_Text Txt
    {
        get
        {
            if (_txt == null)
                _txt = transform.Find("Text").GetComponent<TMP_Text>();
            return _txt;
        }
    }

    public int Index;

    /// <summary>  
	/// 设定展示内容  
	/// </summary>  
	/// <param name="model"></param>  
	/// <param name="itemHeight"></param>  
	/// <param name="itemOffset"></param>
    public void SetData(ScrollViewPlusItemModel model, float itemHeight, float itemOffset)
    {
        Img.sprite = model.Icon;
        Txt.text = model.Describe;
        Rect.anchoredPosition = new Vector2(0, -Index * (itemHeight + itemOffset));
    }
}

在「ScrollRect」组件中,当视口滚动时,会触发onValueChanged中绑定的方法。我们可以通过这种方式实现实时更新子物体的数据。首先我们需要先计算出视口中显示的元素在_models中的起始和终止下标。然后判断每个Item的Index是否在这个区间中。如果不在区间中,就需要更新它的Index和展示的内容。在「ScrollViewPlus」类中添加如下方法

private void OnValueChanged()
{
	int startId = Mathf.FloorToInt(_content.anchoredPosition.y / (_itemHeight + ItemOffset));
	int endId = startId + _items.Count - 1;
	if(startId < 0 || endId > _models.Count-1) return;
	foreach (var item in _items)
	{
		if (!IsInRange(startId, endId, item))
		{
			var offset = 0;
			if (item.Index < startId)
			{
				offset = startId - item.Index - 1;
				item.Index = endId-offset;
				item.SetData(_models[endId-offset],_itemHeight,ItemOffset);
			}else if (item.Index > endId)
			{
				offset = item.Index - endId - 1;
				item.Index = startId+offset;
				item.SetData(_models[startId+offset],_itemHeight,ItemOffset);
			}
		}
	}
	
}
private bool IsInRange(int startId, int endId, ScrollViewPlusItem item)  
{  
    return item.Index >= startId && item.Index <= endId;  
}

这里之所以设置了一个offset偏移量,是因为考虑到快速上下滑动导致可能有多个Item堆积在同一个地方的情况。
然后在Start()方法中添加事件的监听

GetComponent<ScrollRect>().onValueChanged.AddListener(point =>OnValueChanged());

另外,别忘了在SpawnItem()方法中初始化展示内容和下标

/// <summary>
/// 生成子物体
/// </summary>
/// <param name="num"></param>
/// <param name="itemPrefab"></param>
private void SpawnItem(int num,GameObject itemPrefab)
{
	for (int i = 0; i < num; i++)
	{
		GameObject item = Instantiate(itemPrefab, _content);
		var scrollViewPlusItem = item.AddComponent<ScrollViewPlusItem>();
		scrollViewPlusItem.Index = i;
		scrollViewPlusItem.SetData(_models[i],_itemHeight,ItemOffset);
		_items.Add(scrollViewPlusItem);
		
	}
}

最终效果如下

unity中无限滚动_ui_06