滑动列表是程序里面最常用的一个组件之一
这次要实现的功能是使用少量的滑动列表项来加载显示大量的数据
即比如可显示部分能够容下5个滑动列表项,则demo只需要六个就可以达到若干个数据的显示
Demo在这里
实现的主要思路就是,
当上滑的时候,如果有子项超出可显示范围,则会对应添加到底部
当下滑的时候,如果有子项超出可显示范围,则会对应添加到顶部
程序主要分为三部分,模拟加载数据,单个子项item和总的list
程序的重要逻辑部分在代码里面有注释
代码如下所示:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 循环列表总控制
/// </summary>
public class LoopList : MonoBehaviour
{
public float itemInterval;
private float _itemHeight;
/// <summary>
/// ScrollRect的Content属性
/// </summary>
private RectTransform scrollRectContent;
private List<LoopListItem> _items;
private List<LoopListItemData> _models;
private void Start()
{
_items = new List<LoopListItem>();
_models = new List<LoopListItemData>();
//模拟数据获取
GetModel();
scrollRectContent = transform.Find("Viewport/Content").GetComponent<RectTransform>();
GameObject itemPrefab = Resources.Load<GameObject>("LoopListItem");
_itemHeight = itemPrefab.GetComponent<RectTransform>().rect.height;
int totalItemNum = GetTotalShowItemNum(_itemHeight, itemInterval);
SpwanItem(totalItemNum, itemPrefab);
SetContentSize();
transform.GetComponent<ScrollRect>().onValueChanged.AddListener(OnScrollRectScroll);
}
private void OnScrollRectScroll(Vector2 data)
{
foreach (LoopListItem item in _items)
{
item.OnScrollRectScroll();
}
}
/// <summary>
/// 获得总共需要的项数 包括多余的一项
/// </summary>
/// <param name="itemHeight"></param>
/// <param name="offset"></param>
/// <returns></returns>
private int GetTotalShowItemNum(float itemHeight, float offset)
{
float height = GetComponent<RectTransform>().rect.height;
//这里多加的1是在显示区域外部 用来准备显示数据的一项
return Mathf.CeilToInt(height / (itemHeight + offset)) + 1;
}
private void SpwanItem(int totalItemNum, GameObject itemPrefab)
{
GameObject temp = null;
LoopListItem itemTemp = null;
for (int idx = 0; idx < totalItemNum; idx++)
{
temp = Instantiate(itemPrefab, scrollRectContent);
itemTemp = temp.AddComponent<LoopListItem>();
itemTemp.AddGetDataListener(GetData);
itemTemp.Init(idx, itemInterval, totalItemNum, scrollRectContent);
_items.Add(itemTemp);
}
}
/// <summary>
/// 通过下标获取对应数据
/// 下标超出范围的返回是new的数据
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
private LoopListItemData GetData(int index)
{
if (index < 0 || index >= _models.Count)
return new LoopListItemData();
return _models[index];
}
private void GetModel()
{
foreach (var sprite in Resources.LoadAll<Sprite>("Icon"))
{
_models.Add(new LoopListItemData(sprite, sprite.name));
}
}
/// <summary>
/// 设置总内容区域的大小为所有数据加载之后的大小
/// 只是超出可显示部分是没有LoopListItem的
/// </summary>
private void SetContentSize()
{
float y = _models.Count * _itemHeight + (_models.Count - 1) * itemInterval;
scrollRectContent.sizeDelta = new Vector2(scrollRectContent.sizeDelta.x, y);
}
}
using System;
using UnityEngine;
using UnityEngine.UI;
public class LoopListItem : MonoBehaviour
{
/// <summary>
/// 自身id
/// 其实也是每个LoopListItem在LoopList上对应的数据的下标
/// 每个LoopListItem也根据这个id计算自身所处的位置
/// </summary>
private int selfId = -1;
private RectTransform _rect;
public RectTransform Rect
{
get
{
if (_rect == null)
_rect = GetComponent<RectTransform>();
return _rect;
}
}
private Image _icon;
public Image Icon
{
get
{
if (_icon == null)
_icon = transform.Find("Image").GetComponent<Image>();
return _icon;
}
}
private Text _des;
public Text Des
{
get
{
if (_des == null)
_des = transform.Find("Text").GetComponent<Text>();
return _des;
}
}
private Func<int, LoopListItemData> _getData;
private RectTransform scrollRectContent;
/// <summary>
/// 每项的空隙大小
/// </summary>
private float itemInterval;
/// <summary>
/// 总共需要的Item项数 包括多余的一项
/// </summary>
private int totalItemNum;
private LoopListItemData data;
public void Init(int id,float itemInterval,int totalItemNum, Transform scrollRectContent)
{
this.scrollRectContent = scrollRectContent.GetComponent<RectTransform>();
this.totalItemNum = totalItemNum;
this.itemInterval = itemInterval;
ChangeSelfDataById(id);
}
/// <summary>
/// 将通过id获取对应数据的工作交给外部即LoopList来做
/// </summary>
/// <param name="getData"></param>
public void AddGetDataListener(Func<int, LoopListItemData> getData)
{
_getData = getData;
}
public void OnScrollRectScroll()
{
int toppestVisibleId, lowestVisibleId = 0;
UpdateIdRange(out toppestVisibleId,out lowestVisibleId);
OverflowRemedy(toppestVisibleId, lowestVisibleId);
}
private void UpdateIdRange(out int toppestVisibleId,out int lowestVisibleId)
{
//scrollRectContent的顶部ScrollRect顶部的时候 anchoredPosition.y = 0
//anchoredPosition.y可以表示scrollRectContent滑出的时候
//它的顶部到ScrollRect顶部的距离
//越往上滑 y值越大 所得到的startId越大
//所以这个值表示 scrollRectContent滑动到当前位置 时候 可见的最顶部的item的id 不是超出的item的id
toppestVisibleId = Mathf.FloorToInt(scrollRectContent.anchoredPosition.y/(Rect.rect.height + itemInterval));
//这个值表示的是可见的最底部的item的id
lowestVisibleId = toppestVisibleId + totalItemNum - 1;
//越往下 item的id越大
}
private void OverflowRemedy(int toppestVisibleId, int lowestVisibleId)
{
int offset = 0;
//offset是为了防止向上或者向下滑动太快的时候
//同时有几个item超出的话 超出的不同位置的item
//要归位到它对应的位置
if (selfId < toppestVisibleId)
{
//上滑的时候 超出两个item,这两个item是不可见的
//则最顶部的item的selfId与toppestVisibleId的差值是2
//offfset是1
//它归为到底部时候的对应id是lowestVisibleId - 1
//第二个item的的selfId与toppestVisibleId的差值是1
//offset是0
//它归为到底部时候的对应id是lowestVisibleId
//顶部更多的超出也是一样的道理
//虽然没有越早超出的越快补上,但是每个item都会根据itemid刷新它对应的位置
//所以看起来没问题
offset = toppestVisibleId - selfId - 1;
ChangeSelfDataById(lowestVisibleId - offset);
}
else if (selfId > lowestVisibleId)
{
//下滑的时候 超出两个item,这两个item是不可见的
//则最底部的item的selfId与lowestVisibleId的差值是2
//offfset是1
//它归为到顶部时候的对应id是toppestVisibleId + 1
//第二个item的selfId与lowestVisibleId的差值是1
//offset是0
//它归为到顶部时候的对应id是toppestVisibleId
//底部更多的超出也是一样的道理
//虽然没有越早超出的越快补上,但是每个item都会根据itemid刷新它对应的位置
//所以看起来没问题
offset = selfId - lowestVisibleId - 1;
ChangeSelfDataById(toppestVisibleId + offset);
}
}
private void ChangeSelfDataById(int id)
{
if (selfId != id && JudgeIdValid(id))
{
selfId = id;
data = _getData(id);
Icon.sprite = data.Icon;
Des.text = data.Describe;
SetPos();
}
}
private void SetPos()
{
//每个LoopListItem所挂物体的Pivot是在自身的左上角
//当content的顶部与viewport顶部对齐的时候
//anchoredPosition为(0,0)的在ScrollView最顶部 对应的selfId为0
//当content的顶部超出viewport顶部对齐的时候
//anchoredPosition为(0,toppestVisibleId)的在ScrollView最顶部
Rect.anchoredPosition = new Vector2(0, - selfId * (Rect.rect.height + itemInterval));
}
private bool JudgeIdValid(int id)
{
//不合法的id返回的是new出来的新的LoopListItemModel
return !_getData(id).Equals(new LoopListItemData());
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 每项的数据
/// </summary>
public struct LoopListItemData
{
public Sprite Icon;
public string Describe;
public LoopListItemData(Sprite icon, string describe)
{
Icon = icon;
Describe = describe;
}
}