首发于 Unity UGUI

[Unity UGUI] 实现可复用Item的ScrollView

er大家好我是锡乔君,目前是Unity开发工程师,做过Java,熟悉Android,略懂C++。相对于知乎上的大神,我只是个菜鸟!下面是我的博客地址,有问题欢迎私信讨论,看到会及时回复。


本文主要介绍一种实现可复用Item ScrollView的方法,系统自带的滚动View有一些缺点,其中不能复用Item会造成内存上的开销。当有上万条数据时,这种开销就很大,所以我们实现一个简单版可复用滚动View。效果如下:
https://www.zhihu.com/video/1396252077492961280

代码还是没什么难度,主要在于计算ID。

采用MVC模式设计,即Model - View - Controller,首先准备制作预制体和图片资源。View只关心显示,Controller负责将Model填充到View里面。

一、计算Item的位置

首先,我们要确定Content视图一次能显示几个Item?当我们上滑时,第一个Item是渐渐消失的,底部也在出现一个Item,如图:

如上图所示,假设我们的Item高度为100,每个Item之间的间隔是10,Content的高度是320,那么显然我们当前视图最高同时只能存放三个Item,然后在1上滑时,4也在慢慢出现,所以我们复用的Item个数需要是4个,不然1来不及复用,因为他正在显示。

reuseItemCount = Mathf.ceilToInt(Content.Height/(item.height+offsetY))+1


第二,让我们来思考一下如何将数据和复用的Item对应起来?

我们的数据使用List存放的,假设有10条数据,从下标0开始取第一条数据,对应第一个Item。当第一个Item完全被上滑至不可见时,我们复用他,他应该显示第几条数据呢?

从图上我们可以看出来,此时1应该显示第五条数据。

这是上滑的逻辑,我们还要考虑下滑的逻辑,如果此时我们下滑,1重新出现怎么处理呢?

定义startId和endId,startId从0开始,endId = startId+ItemCount-1(注意这里对应数据下标),当滑动时,我们可以动态修改startId和endId的值,并始终维护当前Item显示第几条数据。


二、监听滑动事件

在ScrollView中有一个默认的监听OnValueChange()方法,我们可以给每一个Item添加该方法的响应,当滑动时,每个Item都会判断如何修改和维护自己的ID,就比如Item 1上滑至不可见,那么此时他将自己的ID改为4显示第五条数据。这里我们使用委托的方式来完成。最后比较重要的就是动态的修改Item的位置,如何修改呢?

假设我们以O点为(0,0)点,那么A,B,C点的坐标都很好计算。


最后上代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class LoopList : MonoBehaviour
    public float offsetY;
    private float itemHeight;
    private float contentHeight;
    private List<LoopListItem> _items;
    private List<LoopListModel> _models;
    private RectTransform _content;
    void Start()
        _items = new List<LoopListItem>();
        _models = new List<LoopListModel>();
        GetModels();
        _content = transform.Find("Viewport/Content").GetComponent<RectTransform>();
        GameObject go = LoadPrefab("Prefab/Item");
        int itemCount = GetShowItemCount(offsetY,itemHeight);
        //生成count+1个预制体
        SpawnItems(go,itemCount);
        SetContentSize(itemCount,offsetY,itemHeight);
        GetComponent<ScrollRect>().onValueChanged.AddListener(ChangeValue);
    public void ChangeValue(Vector2 data)
        foreach (var item in _items)
            item.OnValueChange();
    private void SpawnItems(GameObject go,int itemCount)
        GameObject itemPrefab = null;
        LoopListItem item = null;
        for (int i = 0; i < itemCount; i++)
          itemPrefab = Instantiate(go, _content.transform);
          item = itemPrefab.AddComponent<LoopListItem>();
          item.AddListener(GetData);
          item.Init(i,offsetY, itemCount);
          _items.Add(item);
    public LoopListModel GetData(int index)
        if (index < 0 || index > _models.Count-1)
            return default(LoopListModel);
            return _models[index];
    private void GetModels()
        Sprite[] sprites = Resources.LoadAll<Sprite>("Texture/Dog");
        LoopListModel model = null;
        foreach (var sprite in sprites)
            model = new LoopListModel(sprite, sprite.name);
            _models.Add(model);
    private GameObject LoadPrefab(string path)
        GameObject go = Resources.Load<GameObject>(path);
        itemHeight = go.GetComponent<RectTransform>().rect.height;
        return go;
    private int GetShowItemCount(float offsetY,float itemHeight)
        float height = GetComponent<RectTransform>().rect.height;
        return Mathf.CeilToInt(height / (offsetY + itemHeight))+1;
    private void SetContentSize(int itemCount, float offsetY, float itemHeight)
        float y = _models.Count * itemHeight + offsetY * (_models.Count - 1);
        _content.sizeDelta = new Vector2(_content.sizeDelta.x, y);


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LoopListModel 
    public Sprite sprite;
    public string name;
    public LoopListModel(Sprite sprite, string name)
        this.sprite = sprite;
        this.name = name;


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class LoopListItem : MonoBehaviour
    private int _id = -1;
    private float _offsetY;
    private int _itemCount;
    private Func<int, LoopListModel> _getData;
    private LoopListModel model;
    private RectTransform _content;
    private RectTransform content
            if (_content == null)
                _content = transform.parent.GetComponent<RectTransform>();
            return _content;
    private RectTransform _rect;
    private RectTransform rect
            if (_rect == null)
                _rect = GetComponent<RectTransform>();
            return _rect;
    private Image _image;
    private Image image
            if (_image == null)
                _image= GetComponentInChildren<Image>();
            return _image;
    private Text _text;
    private Text text
            if (_text == null)
                _text = GetComponentInChildren<Text>();
            return _text;
    public void Init(int i,float offsetY,int itemCount)
        _offsetY = offsetY;
        _itemCount = itemCount;
        ChangeID(i);
    public void AddListener(Func<int, LoopListModel> getData)
        _getData = getData;
    public void OnValueChange()
        int _startId, _endId;
        UpdateIdRange(out _startId,out _endId);
        JudgeSelfId(_startId,_endId);
    private void JudgeSelfId(int _startId,int _endId)
        if (_id < _startId)
            ChangeID(_endId);
        }else if (_id > _endId)
            ChangeID(_startId);
    private void UpdateIdRange(out int _startId,out int _endId)
        _startId = Mathf.FloorToInt(content.anchoredPosition.y / (rect.rect.height + _offsetY));
        if (_startId < 0)
            _startId = 0;
        _endId = _startId + _itemCount - 1;
    private void ChangeID(int Id)
        if (_id != Id && IsVaild(Id))
            _id = Id;
            model = _getData(_id);
            image.sprite = model.sprite;
            text.text = model.name;
            SetPos();
    public void SetPos()
        rect.anchoredPosition = new Vector2(0,-_id*(_offsetY+rect.rect.height));
    private bool IsVaild(int i)