先看一下實現的效果:
接下來詳細講解一下具體實現步驟:
一、 創建好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);
}
}