[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)