UGUI学习笔记(十一)自制优化版滚动视图

逃离我推掉我的手 2023-09-27 09:31 125阅读 0赞

一、效果展示

1c137f09a0c65ea80c9e68c4f6925a13.gif

二、使用自带组件实现

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

效果如下
afdc91d590f8ad8e0aa9b0a858270597.gif

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

三、手动实现

3.1 准备工作

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

3.2 生成子物体

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

代码如下:

  1. public class ScrollViewPlus : MonoBehaviour
  2. {
  3. // 子物体预制体
  4. public GameObject ItemPrefab;
  5. // 子物体间距
  6. public float ItemOffset;
  7. // 子物体高度
  8. private float _itemHeight;
  9. private RectTransform _content;
  10. private List<ScrollViewPlusItem> _items;
  11. void Start()
  12. {
  13. _items = new List<ScrollViewPlusItem>();
  14. _content = transform.Find("Viewport/Content").GetComponent<RectTransform>();
  15. _itemHeight = ItemPrefab.GetComponent<RectTransform>().rect.height;
  16. int num = GetShowItemNum(_itemHeight, ItemOffset);
  17. SpawnItem(num, ItemPrefab);
  18. }
  19. /// <summary>
  20. /// 获取展示区域子元素个数
  21. /// </summary>
  22. /// <param name="itemHeight"></param>
  23. /// <param name="itemOffset"></param>
  24. /// <returns></returns>
  25. private int GetShowItemNum(float itemHeight, float itemOffset)
  26. {
  27. float height = GetComponent<RectTransform>().rect.height;
  28. return Mathf.CeilToInt(height / (itemHeight + itemOffset)) + 1;
  29. }
  30. /// <summary>
  31. /// 生成子物体
  32. /// </summary>
  33. /// <param name="num"></param>
  34. /// <param name="itemPrefab"></param>
  35. private void SpawnItem(int num,GameObject itemPrefab)
  36. {
  37. for (int i = 0; i < num; i++)
  38. {
  39. GameObject item = Instantiate(itemPrefab, _content);
  40. _items.Add(item.AddComponent<ScrollViewPlusItem>());
  41. }
  42. }

3.3 加载数据

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

  1. public class ScrollViewPlusItemModel
  2. {
  3. public Sprite Icon {
  4. get; }
  5. public string Describe {
  6. get; }
  7. public ScrollViewPlusItemModel(Sprite sprite, string describe)
  8. {
  9. Icon = sprite;
  10. Describe = describe;
  11. }
  12. }

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

  1. private List<ScrollViewPlusItemModel> _models;
  2. /// <summary>
  3. /// 加载数据
  4. /// </summary>
  5. private void GetModels()
  6. {
  7. var sprites = Resources.LoadAll<Sprite>("Icon");
  8. foreach (var sprite in sprites)
  9. {
  10. _models.Add(new ScrollViewPlusItemModel(sprite,sprite.name));
  11. }
  12. }

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

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

3.4 实现滚动

传统的滚动视图需要把所有的子元素全部加载到内存中,造成资源浪费。我们的优化版滚动视图只需要实例化有限的几个子元素,在滚动过程中通过将视口之外的子元素挪回视口中,实现虚假的滚动效果。
以从下向上滑动为例,视口中可以容纳5个元素,我们一共生成了6个。当向上滑动时,Item1被划出了视口区间外。因此需要把Item1重新挪动到队尾,并改变Item1上展示的内容。此时继续向上滑动,Item1就会出现在视口的最下方。
0c6c028feab225fd156eea67d6d70761.png

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

  1. public class ScrollViewPlusItem : MonoBehaviour
  2. {
  3. private RectTransform _rect;
  4. private RectTransform Rect
  5. {
  6. get
  7. {
  8. if (_rect == null)
  9. _rect = GetComponent<RectTransform>();
  10. return _rect;
  11. }
  12. }
  13. private Image _img;
  14. private Image Img
  15. {
  16. get
  17. {
  18. if (_img == null)
  19. _img = transform.Find("Icon").GetComponent<Image>();
  20. return _img;
  21. }
  22. }
  23. private TMP_Text _txt;
  24. private TMP_Text Txt
  25. {
  26. get
  27. {
  28. if (_txt == null)
  29. _txt = transform.Find("Text").GetComponent<TMP_Text>();
  30. return _txt;
  31. }
  32. }
  33. public int Index;
  34. /// <summary>
  35. /// 设定展示内容
  36. /// </summary>
  37. /// <param name="model"></param>
  38. /// <param name="itemHeight"></param>
  39. /// <param name="itemOffset"></param>
  40. public void SetData(ScrollViewPlusItemModel model, float itemHeight, float itemOffset)
  41. {
  42. Img.sprite = model.Icon;
  43. Txt.text = model.Describe;
  44. Rect.anchoredPosition = new Vector2(0, -Index * (itemHeight + itemOffset));
  45. }
  46. }

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

  1. private void OnValueChanged()
  2. {
  3. int startId = Mathf.FloorToInt(_content.anchoredPosition.y / (_itemHeight + ItemOffset));
  4. int endId = startId + _items.Count - 1;
  5. if(startId < 0 || endId > _models.Count-1) return;
  6. foreach (var item in _items)
  7. {
  8. if (!IsInRange(startId, endId, item))
  9. {
  10. var offset = 0;
  11. if (item.Index < startId)
  12. {
  13. offset = startId - item.Index - 1;
  14. item.Index = endId-offset;
  15. item.SetData(_models[endId-offset],_itemHeight,ItemOffset);
  16. }else if (item.Index > endId)
  17. {
  18. offset = item.Index - endId - 1;
  19. item.Index = startId+offset;
  20. item.SetData(_models[startId+offset],_itemHeight,ItemOffset);
  21. }
  22. }
  23. }
  24. }
  25. private bool IsInRange(int startId, int endId, ScrollViewPlusItem item)
  26. {
  27. return item.Index >= startId && item.Index <= endId;
  28. }

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

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

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

  1. /// <summary>
  2. /// 生成子物体
  3. /// </summary>
  4. /// <param name="num"></param>
  5. /// <param name="itemPrefab"></param>
  6. private void SpawnItem(int num,GameObject itemPrefab)
  7. {
  8. for (int i = 0; i < num; i++)
  9. {
  10. GameObject item = Instantiate(itemPrefab, _content);
  11. var scrollViewPlusItem = item.AddComponent<ScrollViewPlusItem>();
  12. scrollViewPlusItem.Index = i;
  13. scrollViewPlusItem.SetData(_models[i],_itemHeight,ItemOffset);
  14. _items.Add(scrollViewPlusItem);
  15. }
  16. }

最终效果如下
1c137f09a0c65ea80c9e68c4f6925a13.gif


源码下载

发表评论

表情:
评论列表 (有 0 条评论,125人围观)

还没有评论,来说两句吧...

相关阅读