Unity UGUI 无限滚动列表,自动分页,自动网络数据请求
1.实现功能
1.1 横向和竖向自动滚动,滚动Item重复利用。
1.2 当滚到应该翻页时,自动调用回调,处理翻页需求。一般在游戏开发过程中,此时需要重新请求下一页数据。
1.3 可以快速滑动,如果有多页,一次性滑到底也可。
2.代码实现
2.1 代码实现思路:
a.在原生ScrollView的基础上,添加扩展脚本实现。
b.content宽或者高,在设置数据总条数时,一次性设置。如总条数1000条,每个显示Item高度为100,则content高度为1000*100。由于事先设置了高度,可以自由快速滑动翻页。
c.垂直滑动时:向上滑动时,当最上面Item完全滑出可视区域时,将其重新放在最下边。向下滑动时,当下面Item完全滑出可视区域时,将其重新放在最下边。水平滑动同理。
d.滑出可视区域判定:通过Item的RectTransform的四个角全局坐标和可视区域RectTransform四角全局坐标判断。
2.2 实现代码:
a.垂直滑动代码实现
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public enum ScrollDirOfVer
{
Top, //向上滑动
Bottom, //向下滑动
Stoped //停止
}
public class ScrollRectExtOfVer : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
[Header("必须在Editor中初始化的变量", order = 1)]
[Header("只支持垂直滑动,代码会强制设置", order = 2)]
public ScrollRect scrollRect = null;
[Header("滚动列表Item", order = 3)]
public RectTransform scrollItemTemplate = null;
[Header("Item列数,必须大于0", order = 5)]
public int row = 1;
[Header("行列间距", order = 6)]
public Vector2 spacing = Vector2.zero;
[Header(" ", order = 7)]
[Header(" ", order = 8)]
[Header("******************************************************", order = 9)]
[Header("运行时计算中间变量", order = 10)]
[Header("每页显示Item个数", order = 11)]
public int itemCountPerPage = 0;
[Header("每个Item大小", order = 16)]
Vector2 cellSize = Vector2.one * 100;
[Header("自动滑动时,停止滑动灵敏度", order = 12)]
public float stopSpeedPerFre = 0;
[Header("是否有点击", order = 13)]
public bool isClickedDown = false;
[Header("数据总条数", order = 14)]
public int maxDataCount = 50;
[Header("滑动方向", order = 15)]
public ScrollDirOfVer scrollDirection = ScrollDirOfVer.Stoped;
//更新Item Transform:待更新的Item int:更新Item对应的数据索引,从0开始
public UnityAction<RectTransform, int> onUpdateItemAction = null;
//获取下一页数据 int:下一页数据页码,从0开始
public UnityAction<int> onGetNextPageDataAction = null;
//获取上一页数据 int:上一页数据页码,从0开始
public UnityAction<int> onGetLastPageDataAction = null;
RectTransform viewRect = null;
Vector3[] viewRectCorners = new Vector3[4];
List<RectTransform> allItems = new List<RectTransform>();
RectTransform content = null;
private void Awake()
{
this.scrollItemTemplate.gameObject.SetActive(false);
if (row <= 0)
{
row = 1;
}
this.viewRect = this.scrollRect.viewport.GetComponent<RectTransform>();
this.content = this.scrollRect.content.GetComponent<RectTransform>();
this.cellSize = new Vector2(this.scrollItemTemplate.rect.width, this.scrollItemTemplate.rect.height);
//总条数计算
this.itemCountPerPage = ((int)(this.viewRect.rect.height / (this.cellSize.y + this.spacing.y)) + 3) * row;
int childCount = this.content.childCount;
for (int i = 0; i < childCount; i++)
{
DestroyImmediate(this.content.GetChild(i).gameObject);
}
this.scrollRect.elasticity = 0.05f;
this.scrollRect.horizontal = false;
this.scrollRect.vertical = true;
this.scrollRect.movementType = ScrollRect.MovementType.Clamped;
RectTransform rect = null;
Vector2 pivot = new Vector2(0, 1);
Vector2 anchorMax = new Vector2(0, 1);
Vector2 anchorMin = new Vector2(0, 1);
for (int i = 0; i < itemCountPerPage; i++)
{
rect = GameObject.Instantiate(this.scrollItemTemplate.gameObject, this.content).GetComponent<RectTransform>();
rect.gameObject.SetActive(true);
rect.pivot = pivot;
rect.anchorMax = anchorMax;
rect.anchorMin = anchorMin;
allItems.Add(rect);
}
this.scrollDirection = ScrollDirOfVer.Stoped;
}
public void Init()
{
this.scrollRect.StopMovement();
this.InitItems();
this.SetMaxDataCount(0);
}
public void InitItems()
{
for (int i = 0; i < this.itemCountPerPage; i++)
{
var item = this.scrollRect.content.GetChild(i).GetComponent<RectTransform>();
this.UpdateItem(item, i, false);
var pos = this.scrollRect.content.anchoredPosition;
pos = new Vector2(pos.x, 0);
this.scrollRect.content.anchoredPosition = pos;
}
}
Vector2 tempV2 = Vector2.zero;
int UpdateItem(RectTransform item, int idx = -1, bool isSetSibling = true)
{
if (idx == -1)
{
if (this.scrollDirection == ScrollDirOfVer.Top)
{
idx = int.Parse(this.scrollRect.content.GetChild(this.itemCountPerPage - 1).gameObject.name) + 1;
}
else if (this.scrollDirection == ScrollDirOfVer.Bottom)
{
idx = int.Parse(this.scrollRect.content.GetChild(0).gameObject.name) - 1;
}
}
// Debug.Log("更新Item:" + item.name + " " + this.scrollDirection + " " + idx + " " + this.maxDataCount);
if (idx >= 0)
{
if (isSetSibling)
{
if (this.scrollDirection == ScrollDirOfVer.Top)
{
item.SetAsLastSibling();
}
else if (this.scrollDirection == ScrollDirOfVer.Bottom)
{
item.SetAsFirstSibling();
}
}
item.gameObject.name = idx.ToString();
var x = idx % row;
var y = idx / row * -1;
tempV2.x = (this.cellSize.x + this.spacing.x) * x;
tempV2.y = (this.cellSize.y + this.spacing.y) * y;
item.anchoredPosition = tempV2;
if (idx >= 0 && idx < this.maxDataCount)
{
item.gameObject.SetActive(true);
if (this.onUpdateItemAction != null)
{
this.onUpdateItemAction(item, idx);
}
return idx;
}
else
{
item.gameObject.SetActive(false);
}
}
return -1;
}
//更新所有数据
public void UpdateAllItems()
{
var idx = 0;
foreach (var item in allItems)
{
if (int.TryParse(item.gameObject.name, out idx))
{
this.UpdateItem(item, idx, false);
}
}
}
void OnGetNextPageData(int page)
{
if (onGetNextPageDataAction != null)
{
onGetNextPageDataAction(page);
}
}
void OnGetLastPageData(int page)
{
if (onGetLastPageDataAction != null)
{
onGetLastPageDataAction(page);
}
}
//由于只支持上下滑动,所有只判断y值即可判断item是否在可视区域
Vector3[] itemCorners = new Vector3[4];
bool IsItemInViewRect(RectTransform item)
{
this.viewRect.GetWorldCorners(this.viewRectCorners);
item.GetWorldCorners(itemCorners);
for (int i = 0; i < 4; i++)
{
if (this.IsViewRectContainPoint(itemCorners[i]))
{
return true;
}
}
return false;
}
//只是上下滚动,所以只判断y值
bool IsViewRectContainPoint(Vector3 v3)
{
bool isContain = false;
if (v3.y >= this.viewRectCorners[0].y && v3.y <= this.viewRectCorners[2].y)
{
isContain = true;
}
else
{
isContain = false;
}
return isContain;
}
public void SetMaxDataCount(int count)
{
Debug.Log("设置总数据条数:" + name + " " + count);
this.maxDataCount = count;
var line = Mathf.CeilToInt(count * 1.0f / this.row);
this.scrollRect.content.sizeDelta = new Vector2(this.content.rect.width, line * (this.cellSize.y + this.spacing.y));
}
float lastY = -99999999;
float minus = 0;
RectTransform tempItem = null;
void Update()
{
if (this.scrollRect == null) return;
var v2 = this.scrollRect.content.anchoredPosition;
if (lastY < -1000000)
{
lastY = v2.y;
this.scrollDirection = ScrollDirOfVer.Stoped;
return;
}
if (isClickedDown == false && Mathf.Abs(lastY - v2.y) < stopSpeedPerFre)
{
this.scrollRect.StopMovement();
return;
}
if (lastY > -1000000)
{
if (lastY < v2.y)
{
this.scrollDirection = ScrollDirOfVer.Top;
if (Mathf.Abs(lastY - v2.y) > 0.005)
{
this.OnMoveToTop();
}
}
else
{
this.scrollDirection = ScrollDirOfVer.Bottom;
if (Mathf.Abs(lastY - v2.y) > 0.0001)
{
this.OnMoveToBottom();
}
}
lastY = v2.y;
}
}
//待更新的所有Items
List<RectTransform> updateItems = new List<RectTransform>();
void OnMoveToTop()
{
updateItems.Clear();
for (int i = 0; i < this.itemCountPerPage; i++)
{
tempItem = this.scrollRect.content.GetChild(i).GetComponent<RectTransform>();
if (!this.IsItemInViewRect(tempItem))
{
updateItems.Add(tempItem);
}
else
{
break;
}
}
var updateIdx = -1;
for (int i = 0; i < updateItems.Count; i++)
{
tempItem = updateItems[i];
updateIdx = this.UpdateItem(tempItem);
if (updateIdx >= 0)
{
int idx = 0;
for (int j = 0; j < 1000; j++)
{
idx = this.itemCountPerPage * j;
if (idx > this.maxDataCount)
{
break;
}
if (updateIdx == idx)
{
//Debug.Log("获取下一页数据:" + updateIdx / this.itemCount + " updateIdx:" + updateIdx + " maxDataCount:" + this.maxDataCount);
this.OnGetNextPageData(updateIdx / this.itemCountPerPage);
break;
}
}
}
}
}
void OnMoveToBottom()
{
updateItems.Clear();
for (int i = this.itemCountPerPage - 1; i >= 0; i--)
{
tempItem = this.scrollRect.content.GetChild(i).GetComponent<RectTransform>();
if (!this.IsItemInViewRect(tempItem))
{
//先缓存再更新:更新里面有设置tempItem的sibling值,这会导致上面GetChild不准确
updateItems.Add(tempItem);
}
else
{
break;
}
}
var updateIdx = -1;
for (int i = 0; i < updateItems.Count; i++)
{
tempItem = updateItems[i];
updateIdx = this.UpdateItem(tempItem);
if (updateIdx >= 0)
{
int idx = 0;
for (int j = 0; j < 1000; j++)
{
idx = j * this.itemCountPerPage;
if (idx > this.maxDataCount)
{
break;
}
if (updateIdx == idx)
{
this.OnGetLastPageData(updateIdx / this.itemCountPerPage);
break;
}
}
}
}
}
public void OnPointerDown(PointerEventData eventData)
{
lastY = -99999999;
this.isClickedDown = true;
}
public void OnPointerUp(PointerEventData eventData)
{
lastY = -99999999;
this.scrollDirection = ScrollDirOfVer.Stoped;
this.isClickedDown = false;
}
}
b.水平滑动代码实现
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public enum ScrollDirOfHor
{
Left, //向左滑动
Right, //向右滑动
Stoped //停止
}
public class ScrollRectExtOfHor : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
{
[Header("必须在Editor中初始化的变量", order = 1)]
[Header("只支持水平滑动,代码会强制设置", order = 2)]
public ScrollRect scrollRect = null;
[Header("滚动列表Item", order = 3)]
public RectTransform scrollItemTemplate = null;
[Header("Item行数,必须大于0", order = 5)]
public int line = 1;
[Header("行列间距", order = 6)]
public Vector2 spacing = Vector2.zero;
[Header(" ", order = 7)]
[Header(" ", order = 8)]
[Header("******************************************************", order = 9)]
[Header("运行时计算中间变量", order = 10)]
[Header("每页显示Item个数", order = 11)]
public int itemCountPerPage = 0;
[Header("每个Item大小", order = 16)]
Vector2 cellSize = Vector2.one * 100;
[Header("自动滑动时,停止滑动灵敏度", order = 12)]
public float stopSpeedPerFre = 0;
[Header("是否有点击", order = 13)]
public bool isClickedDown = false;
[Header("数据总条数", order = 14)]
public int maxDataCount = 50;
[Header("滑动方向", order = 15)]
public ScrollDirOfHor scrollDirection = ScrollDirOfHor.Stoped;
RectTransform viewRect = null;
Vector3[] viewRectCorners = new Vector3[4];
//更新Item Transform:待更新的Item int:更新Item对应的数据索引,从0开始
public UnityAction<RectTransform, int> onUpdateItemAction = null;
//获取下一页数据 int:下一页数据页码,从0开始
public UnityAction<int> onGetNextPageDataAction = null;
//获取上一页数据 int:上一页数据页码,从0开始
public UnityAction<int> onGetLastPageDataAction = null;
List<RectTransform> allItems = new List<RectTransform>();
RectTransform content = null;
private void Awake()
{
this.scrollItemTemplate.gameObject.SetActive(false);
if (line <= 0)
{
line = 1;
}
this.viewRect = this.scrollRect.viewport.GetComponent<RectTransform>();
this.content = this.scrollRect.content.GetComponent<RectTransform>();
this.cellSize = new Vector2(this.scrollItemTemplate.rect.width, this.scrollItemTemplate.rect.height);
//总条数计算
this.itemCountPerPage = ((int)(this.viewRect.rect.width / (this.cellSize.x + this.spacing.x)) + 3) * line;
int childCount = this.content.childCount;
for (int i = 0; i < childCount; i++)
{
DestroyImmediate(this.content.GetChild(i).gameObject);
}
this.scrollRect.elasticity = 0.05f;
this.scrollRect.horizontal = true;
this.scrollRect.vertical = false;
this.scrollRect.movementType = ScrollRect.MovementType.Clamped;
RectTransform rect = null;
Vector2 pivot = new Vector2(0, 1);
Vector2 anchorMax = new Vector2(0, 1);
Vector2 anchorMin = new Vector2(0, 1);
for (int i = 0; i < itemCountPerPage; i++)
{
rect = GameObject.Instantiate(this.scrollItemTemplate.gameObject, this.content).GetComponent<RectTransform>();
rect.gameObject.SetActive(true);
rect.pivot = pivot;
rect.anchorMax = anchorMax;
rect.anchorMin = anchorMin;
allItems.Add(rect);
}
this.scrollDirection = ScrollDirOfHor.Stoped;
}
public void Init()
{
this.scrollRect.StopMovement();
this.InitItems();
this.SetMaxDataCount(0);
}
void InitItems()
{
Vector2 v2 = Vector2.zero;
for (int i = 0; i < this.itemCountPerPage; i++)
{
var item = this.content.GetChild(i).GetComponent<RectTransform>();
this.UpdateItem(item, i, false);
var pos = this.content.anchoredPosition;
pos.x = 0;
this.content.anchoredPosition = pos;
}
}
Vector2 tempV2 = Vector2.zero;
int UpdateItem(RectTransform item, int idx = -1, bool isSetSibling = true)
{
if (idx == -1)
{
if (this.scrollDirection == ScrollDirOfHor.Left)
{
idx = int.Parse(this.content.GetChild(this.itemCountPerPage - 1).gameObject.name) + 1;
}
else if (this.scrollDirection == ScrollDirOfHor.Right)
{
idx = int.Parse(this.content.GetChild(0).gameObject.name) - 1;
}
}
if (idx >= 0)
{
if (isSetSibling)
{
if (this.scrollDirection == ScrollDirOfHor.Left)
{
item.SetAsLastSibling();
}
else if (this.scrollDirection == ScrollDirOfHor.Right)
{
item.SetAsFirstSibling();
}
}
item.gameObject.name = idx.ToString();
var x = idx / line;
var y = idx % line;
tempV2.x = (this.cellSize.x + this.spacing.x) * x;
tempV2.y = (this.cellSize.y + this.spacing.y) * -y;
item.anchoredPosition = tempV2;
if (idx >= 0 && idx < this.maxDataCount)
{
item.gameObject.SetActive(true);
if (this.onUpdateItemAction != null)
{
this.onUpdateItemAction(item, idx);
}
return idx;
}
else
{
item.gameObject.SetActive(false);
}
}
return -1;
}
//更新所有数据
public void UpdateAllItems()
{
var idx = 0;
foreach (var item in allItems)
{
if (int.TryParse(item.gameObject.name, out idx))
{
this.UpdateItem(item, idx, false);
}
}
}
//获取下一页网络数据
void OnGetNextPageData(int page)
{
if (onGetNextPageDataAction != null)
{
onGetNextPageDataAction(page);
}
}
//获取上一页网络数据
void OnGetLastPageData(int page)
{
if (onGetLastPageDataAction != null)
{
onGetLastPageDataAction(page);
}
}
//由于只支持左右滑动,所有只判断x值即可判断item是否在可视区域
Vector3[] itemCorners = new Vector3[4];
bool IsItemInViewRect(RectTransform item)
{
this.viewRect.GetWorldCorners(this.viewRectCorners);
item.GetWorldCorners(itemCorners);
for (int i = 0; i < 4; i++)
{
if (this.IsViewRectContainPoint(itemCorners[i]))
{
return true;
}
}
return false;
}
//只是左右滚动,所以只判断x值
bool IsViewRectContainPoint(Vector3 v3)
{
bool isContain = false;
if (v3.x >= this.viewRectCorners[0].x && v3.x <= this.viewRectCorners[3].x)
{
isContain = true;
}
else
{
isContain = false;
}
return isContain;
}
public void SetMaxDataCount(int count)
{
this.maxDataCount = count;
var row = Mathf.CeilToInt(count * 1.0f / this.line);
this.content.sizeDelta = new Vector2(row * (this.cellSize.x + this.spacing.x), this.content.rect.height);
}
float lastX = -99999999;
RectTransform tempItem = null;
void Update()
{
if (this.scrollRect == null) return;
var v2 = this.content.anchoredPosition;
if (lastX < -1000000)
{
lastX = v2.x;
this.scrollDirection = ScrollDirOfHor.Stoped;
return;
}
if ((isClickedDown == false && Mathf.Abs(lastX - v2.x) < stopSpeedPerFre))
{
this.scrollRect.StopMovement();
return;
}
if (lastX > -1000000)
{
if (lastX < v2.x)
{
this.scrollDirection = ScrollDirOfHor.Right;
if (Mathf.Abs(lastX - v2.x) > 0.005)
{
this.OnMoveToRight();
}
}
else
{
this.scrollDirection = ScrollDirOfHor.Left;
if (Mathf.Abs(lastX - v2.x) > 0.0001)
{
this.OnMoveToLeft();
}
}
lastX = v2.x;
}
}
//待更新的所有Items
List<RectTransform> updateItems = new List<RectTransform>();
//content向左移动:左边不可见元素向右补齐
void OnMoveToLeft()
{
updateItems.Clear();
for (int i = 0; i < this.itemCountPerPage; i++)
{
tempItem = this.content.GetChild(i).GetComponent<RectTransform>();
if (!this.IsItemInViewRect(tempItem))
{
updateItems.Add(tempItem);
}
else
{
break;
}
}
var updateIdx = -1;
for (int i = 0; i < updateItems.Count; i++)
{
tempItem = updateItems[i];
updateIdx = this.UpdateItem(tempItem);
if (updateIdx >= 0)
{
int idx = 0;
for (int j = 0; j < 1000; j++)
{
idx = this.itemCountPerPage * j;
if (idx > this.maxDataCount)
{
break;
}
if (updateIdx == idx)
{
//Debug.Log("获取下一页数据:" + updateIdx / this.itemCount + " updateIdx:" + updateIdx + " maxDataCount:" + this.maxDataCount);
this.OnGetNextPageData(updateIdx / this.itemCountPerPage);
break;
}
}
}
}
}
//content向右移动:右边不可见元素向左补齐
void OnMoveToRight()
{
updateItems.Clear();
for (int i = this.itemCountPerPage - 1; i >= 0; i--)
{
tempItem = this.content.GetChild(i).GetComponent<RectTransform>();
if (!this.IsItemInViewRect(tempItem))
{
//先缓存再更新:更新里面有设置tempItem的sibling值,这会导致上面GetChild不准确
updateItems.Add(tempItem);
}
else
{
break;
}
}
var updateIdx = -1;
for (int i = 0; i < updateItems.Count; i++)
{
tempItem = updateItems[i];
updateIdx = this.UpdateItem(tempItem);
if (updateIdx >= 0)
{
int idx = 0;
for (int j = 0; j < 1000; j++)
{
idx = j * this.itemCountPerPage;
if (idx > this.maxDataCount)
{
break;
}
if (updateIdx == idx)
{
this.OnGetLastPageData(updateIdx / this.itemCountPerPage);
break;
}
}
}
}
}
public void OnPointerDown(PointerEventData eventData)
{
lastX = -99999999;
this.isClickedDown = true;
}
public void OnPointerUp(PointerEventData eventData)
{
lastX = -99999999;
this.scrollDirection = ScrollDirOfHor.Stoped;
this.isClickedDown = false;
}
}
c.测试代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Test : MonoBehaviour {
public ScrollRectExtOfHor extHor;
public ScrollRectExtOfVer extVer;
public Dictionary<int, string> horDataList = new Dictionary<int, string>();
void Start () {
extHor.onUpdateItemAction = this.OnUpdateHorItem;
extHor.onGetLastPageDataAction = this.OnGetLastHorPage;
extHor.onGetNextPageDataAction = this.OnGetNextHorPage;
extHor.Init();
extHor.SetMaxDataCount(500);
//初始化时获取第一页数据
this.OnGetLastHorPage(0);
extVer.onUpdateItemAction = this.OnUpdateVerItem;
extVer.onGetLastPageDataAction = this.OnGetLastVerPage;
extVer.onGetNextPageDataAction = this.OnGetNextVerPage;
extVer.Init();
extVer.SetMaxDataCount(300);
StartCoroutine(TestReinit(15));
}
//实际项目中,最好不在里面查找,以免降低性能,应该先查找缓存
void OnUpdateHorItem(RectTransform rect, int idx)
{
var text = rect.Find("Text").GetComponent<Text>();
if (horDataList.ContainsKey(idx))
{
text.text = horDataList[idx];
}
else
{
text.text = "数据加载中...";
}
}
//模拟从服务器获取页码数据
void OnGetLastHorPage(int idx)
{
Debug.Log("==>C2S:获取上页" + idx);
StartCoroutine(OnTestServerData(idx));
}
//模拟从服务器获取页码数据
void OnGetNextHorPage(int idx)
{
Debug.Log("==>获取下页" + idx);
StartCoroutine(OnTestServerData(idx));
}
//模拟服务器返回数据
IEnumerator OnTestServerData(int pageIdx)
{
//模拟网络延时
yield return new WaitForSeconds(UnityEngine.Random.Range(0.05f, 2));
Debug.Log("==>Server Data: pageIdx->" + pageIdx + " count per page->" + extHor.itemCountPerPage);
int idx = 0;
//虚拟网络数据
for (int i = 0; i < extHor.itemCountPerPage; i++)
{
idx = pageIdx * extHor.itemCountPerPage + i;
this.horDataList[idx] = "数据\n" + idx.ToString();
}
extHor.UpdateAllItems();
}
void OnUpdateVerItem(RectTransform rect, int idx)
{
rect.Find("Text").GetComponent<Text>().text = idx.ToString();
}
void OnGetLastVerPage(int idx)
{
Debug.Log("获取上页" + idx);
}
void OnGetNextVerPage(int idx)
{
Debug.Log("获取下页" + idx);
}
IEnumerator TestReinit(float f)
{
yield return new WaitForSeconds(f);
Debug.Log("重新初始化");
extVer.Init();
extVer.UpdateAllItems();
}
}
d.测试场景搭建
e.测试结果
3.测试工程下载地址:https://download.csdn.net/download/lcl20093466/12497807
转载来源:https://blog.csdn.net/lcl20093466/article/details/106574701