先看一下实现的效果:
接下来详细讲解一下具体实现步骤:
一、 创建好Content以及初始个数的item
- 按照预设体的宽/高创建出Content的总长度。
- 根据ViewPort,也就是绿色背景的宽度来创建初始个数的预设体。也就是ViewPort_Witdh / item_Width 向上取整并 + 1,显示部分是可以被看到的,但是在滑动那过程中,需要有一个临时item来改变位置。如上图未滑动时候第5个item,在滑动过程中,这个临时的item会实时改变。
- 准备要显示的数据,为发生越界刷新数据用。
二、滑动时候判断越界情况
- ScrollView中属性onValueChanged提供了滑动时候的回调,返回的是Vector2的x,y是[0,1]的值,表明当前滑动到当前的相对位置。每帧需要记录当前的值,以便下一帧判断是左滑还是右滑。
- 先看下图:
红色框为创建好的Content,绿色为ViewPort。稍微思考知道:items(假想出所有未创建出来的Item)是跟着Content走的,每个item的位置在Content中是固定不变的,ViewPort是相对于Content实时改变的。由此可得出,判断越界就是判断两端显示的item在Content中的坐标和ViewPort两个边在Content中的关系。判断越界的条件方式可以不同,本博客判断的条件如下图:
左边越界的条件:显示的items列表中,最左边的item的右边(红色的边)超过Viewport的左边位置为越界。
上面的解释为向左滑动时候的越界处理,右边则同理(会有稍许不同)。
三、更新位置、UI
- 监测到越界之后要更新已经越界的item位置,如上图第一个位置设置到最后,此时需要计算出下“最后”在Content中的位置,这里的“第一个”和“最后一个”分别要用两个变量 leftIndex 和 rightIndex 来记录,这两个变量表明了当前显示的items是 leftIndex 到 rightIndex 之间的。
- 由于只有有限个item,但是数据是无限、不固定的,所以在显示之前要将数据准备好,发生越界时候通过 leftIndex 和 rightIndex 来更新数据,也就是显示 leftIndex 到 rightIndex 中的数据。
以上只是本片博客大体思路,实现无限滚动有很多种方式,不同之处大概可分为:
- 预留item的个数
- 坐标的参考、定义
- 越界的处理判断
下面是功能的代码实现,简单写的DEMO,稍有不严谨之处仅供参考:LoopScrollView.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
namespace EamonnLi.LoopScrollView{
public class LoopScrollView : MonoBehaviour {
public RectTransform content;
public ScrollRect scrollRect;
public RectTransform scrollRectRt;
public GameObject loopObject;
RectTransform loopRt;
int count;
public float spacing = 0;
int leftIndex = 0;
int rightIndex = 0;
float leftPosOfContent = 0f;
float rightPosOfContent = 0;
int currentIndex = 0;
List<ItemData> datas = new List<ItemData>();
List<RectTransform> loopObjectsList = new List<RectTransform>();
void Awake(){
loopRt = loopObject.GetComponent<RectTransform>();
}
public void InitLoopScrollView(int count, List<ItemData> dataList){
this.count = count;
datas = dataList;
CreateContent();
CaculateMinCount();
CreateLoopObject();
UpdateContent();
scrollRect.onValueChanged.AddListener(OnScrolled);
}
void UpdateContent(){
for (int i = 0; i < loopObjectsList.Count; i++)
{
ItemData data = datas[i + leftIndex];
loopObjectsList[i].GetComponent<LoopItemController>().UpdateUI(data);
}
for (int i = leftIndex; i < rightIndex; i++)
{
}
}
float contentWidth = 0f;
void CreateContent(){
Vector2 itemSize = loopRt.sizeDelta;
contentWidth = count * itemSize.x + (count - 1) * spacing;
content.sizeDelta = new Vector2(contentWidth, content.sizeDelta.y);
}
int minCount = 0;
void CaculateMinCount(){
float minWidth = scrollRect.GetComponent<RectTransform>().sizeDelta.x;
Debug.Log("minWidth : " + minWidth);
minCount = 1;
minCount = minCount + (int)Mathf.Floor(minWidth / (loopRt.sizeDelta.x + spacing));
minCount++;
}
void CreateLoopObject(){
int createCount = 0;
if (count < minCount - 1){
leftIndex = 0;
rightIndex = count;
leftPosOfContent = 0f;
rightPosOfContent = scrollRectRt.sizeDelta.x;
createCount = count;
}else{
leftIndex = 0;
rightIndex = minCount;
leftPosOfContent = 0f;
rightPosOfContent = scrollRectRt.sizeDelta.x;
createCount = minCount;
}
Debug.Log("minCount : " + minCount);
Debug.Log("createCount : " + createCount);
for (int i = 0; i < createCount; i++)
{
GameObject loop = Instantiate(loopObject);
RectTransform loopRectTransform = loop.GetComponent<RectTransform>();
loopRectTransform.SetParent(content);
loopRectTransform.localScale = Vector3.one;
loopObjectsList.Add(loopRectTransform);
SetPositionOfIndex(loopRectTransform, i);
}
}
void SetPositionOfIndex(RectTransform rt, int index){
float x = (rt.sizeDelta.x + spacing) * index;
Vector3 anchoredPosition = new Vector3(x, -300, 0);
rt.anchoredPosition = anchoredPosition;
}
float lastValuX = 0;
void OnScrolled(Vector2 value){
leftPosOfContent = - content.anchoredPosition.x;
rightPosOfContent = leftPosOfContent + scrollRectRt.sizeDelta.x;
// if (leftIndex <= 0 || rightIndex >= count - 1)
// return;
float currentValueX = value.x;
Debug.Log("count +++ " + count + " ____rightIndex " + rightIndex);
if (currentValueX > lastValuX){
// 手指往左滑动,content往左走,判断 loopObjectsList[0] 的右边是否出去
if (rightIndex >= count)
return;
RectTransform rt = loopObjectsList[0];
float rtRightPos = rt.sizeDelta.x + rt.anchoredPosition.x;
if (rtRightPos < leftPosOfContent){
loopObjectsList.RemoveAt(0);
loopObjectsList.Add(rt);
SetPositionOfIndex(rt, rightIndex);
rightIndex += 1;
leftIndex += 1;
UpdateContent();
}
}
else if (currentValueX < lastValuX){
// 手指往右滑动,content往右走,判断 loopObjectsList[loopObjectsList.Count - 1];] 的左边是否出去
if (leftIndex <= 0)
return;
RectTransform rt = loopObjectsList[loopObjectsList.Count - 1];
float rtLeftPos = rt.anchoredPosition.x;
if (rtLeftPos > rightPosOfContent){
loopObjectsList.RemoveAt(loopObjectsList.Count - 1);
loopObjectsList.Insert(0, rt);
rightIndex -= 1;
leftIndex -= 1;
SetPositionOfIndex(rt, leftIndex);
UpdateContent();
}
}
lastValuX = currentValueX;
}
}
}
LoopItemController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace EamonnLi.LoopScrollView{
public class LoopItemController : MonoBehaviour {
public Image background;
public Image icon;
public Text title;
public void UpdateUI(ItemData data){
icon.gameObject.SetActive(false);
title.gameObject.SetActive(false);
background.sprite = data.bgSprite;
icon.color = new Color(data.index / 100.0f, 1 - data.index / 100.0f, 1, 1);
title.text = data.index.ToString();
}
}
}
TestUseLoopScrollView.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using EamonnLi.LoopScrollView;
public class ItemData{
public int index;
public Sprite iconSprite;
public Sprite bgSprite;
public ItemData(int index, Sprite iconSprite, Sprite bgSprite){
this.index = index;
this.iconSprite = iconSprite;
this.bgSprite = bgSprite;
}
}
public class TestUseLoopScrollView : MonoBehaviour {
public LoopScrollView loopScrollView;
void Awake(){
int count = 100;
List<ItemData> datas = new List<ItemData>();
for (int i = 0; i < count; i++)
{
datas.Add(new ItemData(i, null, null));
}
loopScrollView.InitLoopScrollView(count, datas);
}
}