一、效果展示
二、使用自带组件实现
为了让「Scroll View」组件实现内容框自适应大小,我们可以在「Content」上挂载「Vertical Layout Group」组件和「Content Size Fitter」组件。
效果如下
但这种实现方式有诸多问题。一方面,「Vertical Layout Group」这类排序组件可能会增加性能消耗;另一方面,「Content Size Fitter」必须靠子项才能撑开,当子项数量较多但又只需要展示少数几条时,会造成内存的浪费。因此我们需要自己实现一个优化版的滚动视图。
三、手动实现
3.1 准备工作
首先在场景中创建一个「Scroll View」并命名为「ScrollViewPlus」,在其上挂载一个同名脚本。创建一个子物体脚本命名为「ScrollViewPlusItem」。创建名为「ScrollViewPlusItem」的预制体,用于之后实例化子物体。
3.2 生成子物体
在生成子物体之前,首先要计算出最少需要的子物体数量。子物体的数量通过子物体的高度与间距就可以计算出来,但需要考虑到当前视口高度无法整除子物体高度加间距的情况。这种情况下就需要向上取整。另外还需要在计算出来的数量上加1,以避免在滚动时“露馅”。
代码如下:
public class ScrollViewPlus : MonoBehaviour
{
// 子物体预制体
public GameObject ItemPrefab;
// 子物体间距
public float ItemOffset;
// 子物体高度
private float _itemHeight;
private RectTransform _content;
private List<ScrollViewPlusItem> _items;
void Start()
{
_items = new List<ScrollViewPlusItem>();
_content = transform.Find("Viewport/Content").GetComponent<RectTransform>();
_itemHeight = ItemPrefab.GetComponent<RectTransform>().rect.height;
int num = GetShowItemNum(_itemHeight, ItemOffset);
SpawnItem(num, ItemPrefab);
}
/// <summary>
/// 获取展示区域子元素个数
/// </summary>
/// <param name="itemHeight"></param>
/// <param name="itemOffset"></param>
/// <returns></returns>
private int GetShowItemNum(float itemHeight, float itemOffset)
{
float height = GetComponent<RectTransform>().rect.height;
return Mathf.CeilToInt(height / (itemHeight + itemOffset)) + 1;
}
/// <summary>
/// 生成子物体
/// </summary>
/// <param name="num"></param>
/// <param name="itemPrefab"></param>
private void SpawnItem(int num,GameObject itemPrefab)
{
for (int i = 0; i < num; i++)
{
GameObject item = Instantiate(itemPrefab, _content);
_items.Add(item.AddComponent<ScrollViewPlusItem>());
}
}
3.3 加载数据
在实际的项目中,滚动视图的每个元素肯定是不同的,它们的数据可能是从本地加载或是从远程服务器加载。这里只简单模拟一下加载数据的情况。首先创建一个「ScrollViewPlusItemModel」类用于封装数据
public class ScrollViewPlusItemModel
{
public Sprite Icon { get; }
public string Describe { get; }
public ScrollViewPlusItemModel(Sprite sprite, string describe)
{
Icon = sprite;
Describe = describe;
}
}
然后在「ScrollViewPlus」类中定义一个加载数据的方法
private List<ScrollViewPlusItemModel> _models;
/// <summary>
/// 加载数据
/// </summary>
private void GetModels()
{
var sprites = Resources.LoadAll<Sprite>("Icon");
foreach (var sprite in sprites)
{
_models.Add(new ScrollViewPlusItemModel(sprite,sprite.name));
}
}
数据加载后,还需要根据数据的数量动态调节Content的大小
/// <summary>
/// 设置内容大小
/// </summary>
private void SetContentSize()
{
float y = _models.Count * _itemHeight + (_models.Count - 1) * ItemOffset;
_content.sizeDelta = new Vector2(_content.sizeDelta.x, y);
}
3.4 实现滚动
传统的滚动视图需要把所有的子元素全部加载到内存中,造成资源浪费。我们的优化版滚动视图只需要实例化有限的几个子元素,在滚动过程中通过将视口之外的子元素挪回视口中,实现虚假的滚动效果。
以从下向上滑动为例,视口中可以容纳5个元素,我们一共生成了6个。当向上滑动时,Item1被划出了视口区间外。因此需要把Item1重新挪动到队尾,并改变Item1上展示的内容。此时继续向上滑动,Item1就会出现在视口的最下方。
接下来将思路转换成代码。首先在「ScrollViewPlusItem」类中定义出需要用到的属性和方法
public class ScrollViewPlusItem : MonoBehaviour
{
private RectTransform _rect;
private RectTransform Rect
{
get
{
if (_rect == null)
_rect = GetComponent<RectTransform>();
return _rect;
}
}
private Image _img;
private Image Img
{
get
{
if (_img == null)
_img = transform.Find("Icon").GetComponent<Image>();
return _img;
}
}
private TMP_Text _txt;
private TMP_Text Txt
{
get
{
if (_txt == null)
_txt = transform.Find("Text").GetComponent<TMP_Text>();
return _txt;
}
}
public int Index;
/// <summary>
/// 设定展示内容
/// </summary>
/// <param name="model"></param>
/// <param name="itemHeight"></param>
/// <param name="itemOffset"></param>
public void SetData(ScrollViewPlusItemModel model, float itemHeight, float itemOffset)
{
Img.sprite = model.Icon;
Txt.text = model.Describe;
Rect.anchoredPosition = new Vector2(0, -Index * (itemHeight + itemOffset));
}
}
在「ScrollRect」组件中,当视口滚动时,会触发onValueChanged
中绑定的方法。我们可以通过这种方式实现实时更新子物体的数据。首先我们需要先计算出视口中显示的元素在_models
中的起始和终止下标。然后判断每个Item的Index
是否在这个区间中。如果不在区间中,就需要更新它的Index
和展示的内容。在「ScrollViewPlus」类中添加如下方法
private void OnValueChanged()
{
int startId = Mathf.FloorToInt(_content.anchoredPosition.y / (_itemHeight + ItemOffset));
int endId = startId + _items.Count - 1;
if(startId < 0 || endId > _models.Count-1) return;
foreach (var item in _items)
{
if (!IsInRange(startId, endId, item))
{
var offset = 0;
if (item.Index < startId)
{
offset = startId - item.Index - 1;
item.Index = endId-offset;
item.SetData(_models[endId-offset],_itemHeight,ItemOffset);
}else if (item.Index > endId)
{
offset = item.Index - endId - 1;
item.Index = startId+offset;
item.SetData(_models[startId+offset],_itemHeight,ItemOffset);
}
}
}
}
private bool IsInRange(int startId, int endId, ScrollViewPlusItem item)
{
return item.Index >= startId && item.Index <= endId;
}
这里之所以设置了一个offset
偏移量,是因为考虑到快速上下滑动导致可能有多个Item堆积在同一个地方的情况。
然后在Start()
方法中添加事件的监听
GetComponent<ScrollRect>().onValueChanged.AddListener(point =>OnValueChanged());
另外,别忘了在SpawnItem()
方法中初始化展示内容和下标
/// <summary>
/// 生成子物体
/// </summary>
/// <param name="num"></param>
/// <param name="itemPrefab"></param>
private void SpawnItem(int num,GameObject itemPrefab)
{
for (int i = 0; i < num; i++)
{
GameObject item = Instantiate(itemPrefab, _content);
var scrollViewPlusItem = item.AddComponent<ScrollViewPlusItem>();
scrollViewPlusItem.Index = i;
scrollViewPlusItem.SetData(_models[i],_itemHeight,ItemOffset);
_items.Add(scrollViewPlusItem);
}
}
最终效果如下