普通滚动列表
新建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文件夹内新建LoopList
、LoopListItem
、LoopListItemModel
三个脚本,其中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和锚点

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