普通滚动列表

新建Scene命名为LoopList,在Scripts文件夹内新建同名文件夹。

一般情况下,新建一个滚动视图,需要添加“UI——Scroll View”,然后再将各种UI的prefab放在“Scroll View——View Port——Content”下面。然后再给Content挂载“Vertical Layout Group”,并勾选“Control Child Size”(如果是垂直列表就勾选width,反之勾选height)关闭“Child Force Expand”,再挂载“Content Size Fitter”,将“Vertical Fit”改为“Preferred Size”。

并不推荐这样做的原因是,Vertical Layout Group很消耗性能,并且使用了这些组件会让Content变得很大(如果下面的子物体item很多的话),消耗许多内存。

LoopList

在LoopList文件夹内新建LoopListLoopListItemLoopListItemModel三个脚本,其中LoopList负责MVC架构中C(控制器)的职责,里面负责Item生成、监听Item改变等逻辑。LoopListItem负责MVC架构中的V(视图)职责,里面只包含了需要显示的内容。IoopListItemModel负责MVC架构中的M(模型)职责,里面包含了需要显示的各种数据。

首先编辑LoopList,随便找些Image作为prefab放在Resource文件夹内,命名为“LoopListItem”,这个脚本挂载在“Scroll View”上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using System.Collections.Generic;
using UnityEngine;

public class LoopList : MonoBehaviour
{
public float m_OffsetY;
private float m_ItemHeight;
private RectTransform m_Content;
private List<LoopListItem> m_Items;
private List<LoopListItemModel> m_Models;
void Start()
{
m_Items = new List<LoopListItem>();
m_Models = new List<LoopListItemModel>();
//模拟数据获取
GetModel();

m_Content = transform.Find("Viewport/Content").GetComponent<RectTransform>();
var itemPrefab = Resources.Load<GameObject>("LoopListItem");
m_ItemHeight = itemPrefab.GetComponent<RectTransform>().rect.height;
int num = GetShowItemNum(m_ItemHeight,m_OffsetY);
SpawnItem(num,itemPrefab);
SetContentSize();

transform.GetComponent<ScrollRect>().onValueChanged.AddListener(ValueChange);
}
private void ValueChange(Vector2 value)
{
foreach (var item in m_Items)
{
item.OnValueChange();
}
}
private int GetShowItemNum(float itemHeight,float offsetY)
{
float viewHeight = GetComponent<RectTransform>().rect.height;
return Mathf.CeilToInt(viewHeight / (itemHeight + offsetY)) + 1;
}
private void SpawnItem(int num,GameObject prefab)
{
GameObject itemGo;
LoopListItem itemTemp;
for (int i = 0; i < num; i++)
{
itemGo = Instantiate(prefab, m_Content);
itemTemp = itemGo.AddComponent<LoopListItem>();
itemTemp.AddGetDataListener(GetData);
itemTemp.Init(i,m_OffsetY,num);
m_Items.Add(itemTemp);
}
}
private LoopListItemModel GetData(int index)
{
if (index < 0 || index >= m_Models.Count)
{
return new LoopListItemModel();
}
return m_Models[index];
}
private void GetModel()
{
Sprite[] sprites = Resources.LoadAll<Sprite>("Icon");
foreach (Sprite sprite in sprites)
{
m_Models.Add(new LoopListItemModel(sprite,sprite.name));

}
}
private void SetContentSize()
{
float y = m_Models.Count * m_ItemHeight + (m_Models.Count - 1) * m_OffsetY;
m_Content.sizeDelta = new Vector2(m_Content.sizeDelta.x, y);
}
}

首先拿到需要生成的Item的高度,以及整个Scroll View的高度,判断需要生成几个Item才不会穿帮。即GetShowItemNum方法

然后根据需要生成Item,即SpawnItem方法,给Item挂载LoopListItem、添加获取数据Model的监听并初始化每个Item的id等,并把生成的Item缓存在List<LoopListItem> m_Items中。

GetData就是每个Item获取数据的方法,这个方法自己过滤掉了非法的Index。

获取Item需要显示的数据(Model),这里仅作演示。首先声明private List<LoopListItemModel> m_Models;然后使用GetModel方法填充数据

扩充Content框的大小,虽然Item的数量优化了,但是Content框还是需要增大的,否则就无法拖动了。使用SetContentSize方法来设置。Content的大小是使用RectTransform.sizeDelta来设置的。

优化原理:

在LoopList中,Item始终只有固定的个数,我们需要判断当前列表显示的Item中的Start Index和End Index,从m_Models中获取对应的数据来更新Item。当我们拖动Scroll View时,Content上面的RectTransform组件中的PosY会发生变化,我们可以根据它的数据和Item的height来判断当前Item的Start Index应该是什么,同时我们已经知道了Item的Height和每个Item之间的偏移(m_OffsetY),就可以设置每个Item更新后的位置在哪里。

ViewRect添加监听方法ValueChange,这样每次拖动时,每一个Item都会调用自己的OnValueChange方法

LoopListItemModel文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;

public class LoopListItemModel
{
public Sprite Icon;
public string Description;
public LoopListItemModel(){}
public LoopListItemModel(Sprite sprite, string description)
{
this.Icon = sprite;
this.Description = description;
}
}

LoopListItem

LoopListItem文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
using System;
using UnityEngine;
using UnityEngine.UI;

public class LoopListItem : MonoBehaviour
{
private int m_Id;
private RectTransform m_RectTransform;
public RectTransform Rect
{
get
{
if(m_RectTransform == null)
m_RectTransform = GetComponent<RectTransform>();
return m_RectTransform;
}
}
private Image m_Icon;
public Image Icon
{
get
{
if( m_Icon == null)
m_Icon = transform.Find("BG/Image").GetComponent<Image>();
return m_Icon;
}
}
private Text m_Text;
public Text Des
{
get
{
if (m_Text == null)
m_Text = transform.Find("BG/Des").GetComponent<Text>();
return m_Text;
}
}
private Func<int, LoopListItemModel> m_GetData;
private LoopListItemModel m_Model;
private RectTransform m_Content;
private float m_OffsetY;
private int m_ItemNum;
public void Init(int id, float offsetY,int showItemNum)
{
m_Content = transform.parent.GetComponent<RectTransform>();
m_OffsetY = offsetY;
m_ItemNum = showItemNum;

ChangeId(id);
}
public void AddGetDataListener(Func<int, LoopListItemModel> function)
{
m_GetData = function;
}
public void OnValueChange()
{
UpdateIdRange(out int startId,out int endId);//这种写法叫做内联变量声明
JudgeSelfId(startId,endId);
}
private void UpdateIdRange(out int startId,out int endId)
{
startId = Mathf.FloorToInt(m_Content.anchoredPosition.y / (Rect.rect.height + m_OffsetY));
endId = startId + m_ItemNum - 1;
}
private void JudgeSelfId(int startId,int endId)
{
if (m_Id < startId)//向上滑动
{
ChangeId(endId);
}
else if(m_Id > endId)//向下滑动
{
ChangeId(startId);
}
}
private void ChangeId(int id)
{
if(m_Id != id && JudgeIdValid(id))
{
m_Id = id;
m_Model = m_GetData.Invoke(id);
Icon.sprite = m_Model.Icon;
Des.text = m_Model.Description;
SetPos();
}
}
private void SetPos()
{
Rect.anchoredPosition = new Vector2(0, -m_Id * (Rect.rect.height + m_OffsetY));
}
private bool JudgeIdValid(int id)
{
return !m_GetData.Invoke(id).Equals(new LoopListItemModel());
}
}

这里使用一个Func来获取每个Item的数据,在LoopList.SpawnItem时添加监听

OnValueChange方法在LoopList中用来监听ScrollRect滚动,每次拖动时都会调用此方法。

UpdateRange方法计算滚动后的StartId和EndId,注意这里StartId和EndId的生成方式,由于只有JudgeSelfId方法用得到这两个参数,所以我们直接使用out关键字,而不是使用全局变量(全局变量越少越好)。

JudgeSelfId方法用来判断当滑动时,此Item的Id是否在画面之外,并更新Item的id

ChangeId方法用来实现当前Item的id改变时,更新数据部分的逻辑

JudgeIdValid方法用来判断Id和合法性,因为Scroll View组件在边界位置也是可以拖动的,此时计算的StartId和EndId会出现超出数组下标的情况。如果下标非法,在LoopList中的监听方法GetData会返回一个有默认构造的LoopListItemModel,只要我们判断一下就能知道是不是非法的id了。注意:这里的非法下标返回的是一个默认构造,而不是null,这是一个很实用的优化技巧

使用SetPos方法来设置每一个Item的位置。注意每个Item设置的Pivot和锚点

LoopListItem

注意每一个Item的对齐和Pivot设置