前言:
我们在做项目的时候往往会遇到很多的数据,并需要将数据展示在Scroll View中,而对于一些初学者来说,Json与Scroll View 的结合使用会有一些困难的地方,那么今天我就分享一下我的项目中的两种方法,希望可以帮助到你
代码段:
首先使我们需要展示的内容信息,将其放在Json中:
[
{
"Information": {
"id": "sddgrewefw",
"Exam_name": "集成电路封装测试",
"Start_time": "2022-02-10 13:34",
"End_time": "2022-02-10 17:30",
"Score": 100,
"D_score": 80,
"Exam_time": 90,
"Exam_content": "集成电路封装测试",
"Exam_Precautions": "注意事项:",
"Number_questions": 8,
"state": 1
}
},
{
"Information": {
"id": "sddgrewefw",
"Exam_name": "集成电路测试",
"Start_time": "2022-02-10 13:45",
"End_time": "2022-02-10 18:00",
"Score": 100,
"D_score": 50,
"Exam_time": 90,
"Exam_content": "集成电路测试",
"Exam_Precautions": "注意事项:",
"Number_questions": 8,
"state": 0
}
},
{
"Information": {
"id": "sddgrewefww",
"Exam_name": "集成电路封装工艺",
"Start_time": "2022-02-10 13:45",
"End_time": "2022-02-10 18:00",
"Score": 60,
"D_score": 50,
"Exam_time": 90,
"Exam_content": "集成电路测试",
"Exam_Precautions": "注意事项:",
"Number_questions": 8,
"state": 0
}
},
{
"Information": {
"id": "sddgreweew",
"Exam_name": "激光打标考试",
"Start_time": "2022-02-10 13:45",
"End_time": "2022-02-10 18:00",
"Score": 60,
"D_score": 50,
"Exam_time": 90,
"Exam_content": "集成电路测试",
"Exam_Precautions": "注意事项:",
"Number_questions": 8,
"state": 0
}
}
]
接下来我们只需要先解析一下这个Json文件,这应该不难的
①,我们需要设置与Json中所有的字段。代码如下:
public class Information
{
//试卷ID
public string id;
//考试名称/试卷名称
public string examName;
//试卷类型
public bool exam;
//开始考试时间
public string beginTime;
//结束考试时间
public string overTime;
//总分数
public int fullScore;
//得分
public int studentScore;
//考试时间
public int examTime;
//考试内容
public string examContent;
//考试注意事项
public string examDesc;
//题目数量
public int questionNumber;
//当前题库状态
public int status;
//班级ID
public string classId;
}
②开始解析代码:
private List<Information> _testcenterses;
public List<Information> testcenterses
{
get
{
if (_workshops == null)
{
_testcenterses= new List<Information>();
string str = Resources.Load<TextAsset>("Data/KaoheStandAlone").text;
List<Information> list = JsonMapper.ToObject<List<Information>>(str);
_testcenterses= list;
}
return _testcenterses;
}
}
这样的话,这个Json文档已经被解析完成了,我们后续只需要读取里面的内容就可以了
接下来才是正题,我们需要把解析出来的文档放入到UGUI中的Scroll View组件上面,以便于我们查看对应的信息,首先是我们的主代码,是为了循环使用列表,备注很详细,大家可以看一下
using System;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 循环复用列表
/// </summary>
[RequireComponent(typeof(ScrollRect))]
public class RecyclingListView : MonoBehaviour
{
[Tooltip("子节点物体")]
public RecyclingListViewItem ChildObj;
[Tooltip("行间隔")]
public float RowPadding = 15f;
[Tooltip("事先预留的最小列表高度")]
public float PreAllocHeight = 0;
public enum ScrollPosType
{
Top,
Center,
Bottom,
}
public float VerticalNormalizedPosition
{
get => scrollRect.verticalNormalizedPosition;
set => scrollRect.verticalNormalizedPosition = value;
}
/// <summary>
/// 列表行数
/// </summary>
protected int rowCount;
/// <summary>
/// 列表行数,赋值时,会执行列表重新计算
/// </summary>
public int RowCount
{
get => rowCount;
set
{
if (rowCount != value)
{
rowCount = value;
Debug.Log("5:"+rowCount);
// 先禁用滚动变化
ignoreScrollChange = true;
// 更新高度
UpdateContentHeight();
// 重新启用滚动变化
ignoreScrollChange = false;
// 重新计算item
ReorganiseContent(true);
}
}
}
/// <summary>
/// item更新回调函数委托
/// </summary>
/// <param name="item">子节点对象</param>
/// <param name="rowIndex">行数</param>
public delegate void ItemDelegate(RecyclingListViewItem item, int rowIndex);
/// <summary>
/// item更新回调函数委托
/// </summary>
public ItemDelegate ItemCallback;
protected ScrollRect scrollRect;
/// <summary>
/// 复用的item数组
/// </summary>
protected RecyclingListViewItem[] childItems;
/// <summary>
/// 循环列表中,第一个item的索引,最开始每个item都有一个原始索引,最顶部的item的原始索引就是childBufferStart
/// 由于列表是循环复用的,所以往下滑动时,childBufferStart会从0开始到n,然后又从0开始,以此往复
/// 如果是往上滑动,则是从0到-n,再从0开始,以此往复
/// </summary>
protected int childBufferStart = 0;
/// <summary>
/// 列表中最顶部的item的真实数据索引,比如有一百条数据,复用10个item,当前最顶部是第60条数据,那么sourceDataRowStart就是59(注意索引从0开始)
/// </summary>
protected int sourceDataRowStart;
protected bool ignoreScrollChange = false;
protected float previousBuildHeight = 0;
protected const int rowsAboveBelow = 1;
protected virtual void Awake()
{
scrollRect = GetComponent<ScrollRect>();
ChildObj.gameObject.SetActive(false);
}
protected virtual void OnEnable()
{
scrollRect.onValueChanged.AddListener(OnScrollChanged);
ignoreScrollChange = false;
}
protected virtual void OnDisable()
{
scrollRect.onValueChanged.RemoveListener(OnScrollChanged);
}
/// <summary>
/// 供外部调用,强制刷新整个列表,比如数据变化了,刷新一下列表
/// </summary>
public virtual void Refresh()
{
ReorganiseContent(true);
}
/// <summary>
/// 供外部调用,强制刷新整个列表的局部item
/// </summary>
/// <param name="rowStart">开始行</param>
/// <param name="count">数量</param>
public virtual void Refresh(int rowStart, int count)
{
int sourceDataLimit = sourceDataRowStart + childItems.Length;
for (int i = 0; i < count; ++i)
{
int row = rowStart + i;
if (row < sourceDataRowStart || row >= sourceDataLimit)
continue;
int bufIdx = WrapChildIndex(childBufferStart + row - sourceDataRowStart);
if (childItems[bufIdx] != null)
{
UpdateChild(childItems[bufIdx], row);
}
}
}
/// <summary>
/// 供外部调用,强制刷新整个列表的某一个item
/// </summary>
public virtual void Refresh(RecyclingListViewItem item)
{
for (int i = 0; i < childItems.Length; ++i)
{
int idx = WrapChildIndex(childBufferStart + i);
if (childItems[idx] != null && childItems[idx] == item)
{
UpdateChild(childItems[i], sourceDataRowStart + i);
break;
}
}
}
/// <summary>
/// 清空列表
/// </summary>
public virtual void Clear()
{
RowCount = 0;
}
/// <summary>
/// 供外部调用,强制滚动列表,使某一行显示在列表中
/// </summary>
/// <param name="row">行号</param>
/// <param name="posType">目标行显示在列表的位置:顶部,中心,底部</param>
public virtual void ScrollToRow(int row, ScrollPosType posType)
{
scrollRect.verticalNormalizedPosition = GetRowScrollPosition(row, posType);
}
/// <summary>
/// 获得归一化的滚动位置,该位置将给定的行在视图中居中
/// </summary>
/// <param name="row">行号</param>
/// <returns></returns>
public float GetRowScrollPosition(int row, ScrollPosType posType)
{
// 视图高
float vpHeight = ViewportHeight();
float rowHeight = RowHeight();
// 将目标行滚动到列表目标位置时,列表顶部的位置
float vpTop = 0;
switch (posType)
{
case ScrollPosType.Top:
{
vpTop = row * rowHeight;
}
break;
case ScrollPosType.Center:
{
// 目标行的中心位置与列表顶部的距离
float rowCentre = (row + 0.5f) * rowHeight;
// 视口中心位置
float halfVpHeight = vpHeight * 0.5f;
vpTop = Mathf.Max(0, rowCentre - halfVpHeight);
}
break;
case ScrollPosType.Bottom:
{
vpTop = (row+1) * rowHeight - vpHeight;
}
break;
}
// 滚动后,列表底部的位置
float vpBottom = vpTop + vpHeight;
// 列表内容总高度
float contentHeight = scrollRect.content.sizeDelta.y;
// 如果滚动后,列表底部的位置已经超过了列表总高度,则调整列表顶部的位置
if (vpBottom > contentHeight)
vpTop = Mathf.Max(0, vpTop - (vpBottom - contentHeight));
// 反插值,计算两个值之间的Lerp参数。也就是value在from和to之间的比例值
return Mathf.InverseLerp(contentHeight - vpHeight, 0, vpTop);
}
/// <summary>
/// 根据行号获取复用的item对象
/// </summary>
/// <param name="row">行号</param>
protected RecyclingListViewItem GetRowItem(int row)
{
if (childItems != null &&
row >= sourceDataRowStart && row < sourceDataRowStart + childItems.Length &&
row < rowCount)
{
// 注意这里要根据行号计算复用的item原始索引
return childItems[WrapChildIndex(childBufferStart + row - sourceDataRowStart)];
}
return null;
}
protected virtual bool CheckChildItems()
{
// 列表视口高度
float vpHeight = ViewportHeight();
float buildHeight = Mathf.Max(vpHeight, PreAllocHeight);
bool rebuild = childItems == null || buildHeight > previousBuildHeight;
if (rebuild)
{
int childCount = Mathf.RoundToInt(0.5f + buildHeight / RowHeight());
childCount += rowsAboveBelow * 2;
if (childItems == null)
childItems = new RecyclingListViewItem[childCount];
else if (childCount > childItems.Length)
Array.Resize(ref childItems, childCount);
// 创建item
for (int i = 0; i < childItems.Length; ++i)
{
if (childItems[i] == null)
{
var item = Instantiate(ChildObj);
childItems[i] = item;
}
childItems[i].RectTransform.SetParent(scrollRect.content, false);
childItems[i].gameObject.SetActive(false);
}
previousBuildHeight = buildHeight;
}
return rebuild;
}
/// <summary>
/// 列表滚动时,会回调此函数
/// </summary>
/// <param name="normalisedPos">归一化的位置</param>
protected virtual void OnScrollChanged(Vector2 normalisedPos)
{
if (!ignoreScrollChange)
{
ReorganiseContent(false);
}
}
/// <summary>
/// 重新计算列表内容
/// </summary>
/// <param name="clearContents">是否要清空列表重新计算</param>
protected virtual void ReorganiseContent(bool clearContents)
{
if (clearContents)
{
scrollRect.StopMovement();
scrollRect.verticalNormalizedPosition = 1;
}
bool childrenChanged = CheckChildItems();
// 是否要更新整个列表
bool populateAll = childrenChanged || clearContents;
float ymin = scrollRect.content.localPosition.y;
// 第一个可见item的索引
int firstVisibleIndex = (int)(ymin / RowHeight());
int newRowStart = firstVisibleIndex - rowsAboveBelow;
// 滚动变化量
int diff = newRowStart - sourceDataRowStart;
if (populateAll || Mathf.Abs(diff) >= childItems.Length)
{
sourceDataRowStart = newRowStart;
childBufferStart = 0;
int rowIdx = newRowStart;
foreach (var item in childItems)
{
UpdateChild(item, rowIdx++);
}
}
else if (diff != 0)
{
int newBufferStart = (childBufferStart + diff) % childItems.Length;
if (diff < 0)
{
// 向前滑动
for (int i = 1; i <= -diff; ++i)
{
// 得到复用item的索引
int wrapIndex = WrapChildIndex(childBufferStart - i);
int rowIdx = sourceDataRowStart - i;
UpdateChild(childItems[wrapIndex], rowIdx);
}
}
else
{
// 向后滑动
int prevLastBufIdx = childBufferStart + childItems.Length - 1;
int prevLastRowIdx = sourceDataRowStart + childItems.Length - 1;
for (int i = 1; i <= diff; ++i)
{
int wrapIndex = WrapChildIndex(prevLastBufIdx + i);
int rowIdx = prevLastRowIdx + i;
UpdateChild(childItems[wrapIndex], rowIdx);
}
}
sourceDataRowStart = newRowStart;
childBufferStart = newBufferStart;
}
}
private int WrapChildIndex(int idx)
{
while (idx < 0)
idx += childItems.Length;
return idx % childItems.Length;
}
/// <summary>
/// 获取一行的高度,注意要加上RowPadding
/// </summary>
private float RowHeight()
{
return RowPadding + ChildObj.RectTransform.rect.height;
}
/// <summary>
/// 获取列表视口的高度
/// </summary>
private float ViewportHeight()
{
return scrollRect.viewport.rect.height;
}
protected virtual void UpdateChild(RecyclingListViewItem child, int rowIdx)
{
if (rowIdx < 0 || rowIdx >= rowCount)
{
child.gameObject.SetActive(false);
}
else
{
if (ItemCallback == null)
{
Debug.Log("RecyclingListView is missing an ItemCallback, cannot function", this);
return;
}
// 移动到正确的位置
var childRect = ChildObj.RectTransform.rect;
Vector2 pivot = ChildObj.RectTransform.pivot;
float ytoppos = RowHeight() * rowIdx;
float ypos = ytoppos + (1f - pivot.y) * childRect.height;
float xpos = 0 + pivot.x * childRect.width;
child.RectTransform.anchoredPosition = new Vector2(xpos, -ypos);
child.NotifyCurrentAssignment(this, rowIdx);
// 更新数据
ItemCallback(child, rowIdx);
child.gameObject.SetActive(true);
}
}
/// <summary>
/// 更新content的高度
/// </summary>
protected virtual void UpdateContentHeight()
{
// 列表高度
float height = ChildObj.RectTransform.rect.height * rowCount + (rowCount - 1) * RowPadding;
// 更新content的高度
var sz = scrollRect.content.sizeDelta;
scrollRect.content.sizeDelta = new Vector2(sz.x, height);
}
protected virtual void DisableAllChildren()
{
if (childItems != null)
{
for (int i = 0; i < childItems.Length; ++i)
{
childItems[i].gameObject.SetActive(false);
}
}
}
}
然后接着就是两个重要的代码:
第一个是我们scroll view中需要放入的内容的字段,我们需要将他写出来
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class MeShiJuan : RecyclingListViewItem
{
/// <summary>
/// 我的试卷
/// </summary>
public TextMeshProUGUI shijuanname;
public TextMeshProUGUI staretime;
public TextMeshProUGUI enttime;
public TextMeshProUGUI Totalscore;
public TextMeshProUGUI score;
//当前试卷状态
public int zhuangtai;
//当前试卷ID
public string id;
public Button _kaoshiBtn;
//考试说明
public GameObject _Kaoshism;
private MeshijuanR meshijuanTR;
public MeshijuanR MeshijuanTR
{
get { return meshijuanTR; }
set
{
meshijuanTR = value;
shijuanname.text = meshijuanTR.name;
staretime.text = meshijuanTR.statime;
enttime.text = meshijuanTR.endtime;
score.text = meshijuanTR.fenshu;
Totalscore.text = meshijuanTR.totalscore;
zhuangtai = meshijuanTR.Zhuangtai;
id = meshijuanTR.ID;
}
}
void Start()
{
_kaoshiBtn = transform.GetChild(0).GetComponentInChildren<Button>();
_kaoshiBtn.onClick.AddListener(() =>
{
JsonJiexi.Instance.shijuan_id = id;
_Kaoshism.SetActive(true);
KaoshiShuoming.Instance.JiazaiX();
});
}
}
public struct MeshijuanR
{
public string name;
public string statime;
public string endtime;
public string fenshu;
public string totalscore;
public int Zhuangtai;
public string ID;
public MeshijuanR(string a, string b, string c, string d, string e, int ztai, string id)
{
name = a;
statime = b;
endtime = c;
fenshu = d;
totalscore = e;
Zhuangtai = ztai;
ID = id;
}
}
那么接下来这个代码就是将表中的信息加载到组件上面显示在界面上
using System.Collections;
using System.Collections.Generic;
using LitJson;
using UnityEngine;
public class MeshiPanel : MonoBehaviour
{
public RecyclingListView scrollList;
/// <summary>
/// 列表数据
/// </summary>
//我的试卷
private List<MeshijuanR> medata = new List<MeshijuanR>();
private void Start()
{
// 列表item更新回调
scrollList.ItemCallback = PopulateItem;
CreateList();
}
/// <summary>
/// item更新回调
/// </summary>
/// <param name="item">复用的item对象</param>
/// <param name="rowIndex">行号</param>
private void PopulateItem(RecyclingListViewItem item, int rowIndex)
{
var meshi = item as MeShiJuan;
Debug.Log(meshi);
meshi.MeshijuanTR = medata[rowIndex];
}
public void CreateList()
{
var rowCnt = JsonJiexi.Instance.testcenterses.data.Count;
medata.Clear();
Debug.Log("2:"+rowCnt);
for (int i = 0; i < rowCnt; ++i)
{
medata.Add(new MeshijuanR(
JsonJiexi.Instance.testcenterses.data[i].examName,
JsonJiexi.Instance.testcenterses.data[i].beginTime,
JsonJiexi.Instance.testcenterses.data[i].overTime,
JsonJiexi.Instance.testcenterses.data[i].studentScore.ToString(),
JsonJiexi.Instance.testcenterses.data[i].fullScore.ToString(),
JsonJiexi.Instance.testcenterses.data[i].status,
JsonJiexi.Instance.testcenterses.data[i].id));
}
// 设置数据,此时列表会执行更新
scrollList.RowCount = medata.Count;
Debug.Log("4:"+scrollList.RowCount);
}
}
那么,这样的话相当于我们的代码部分已经完成了,接下来就是界面的搭建问题
界面信息:
代码MeshiPanel放置的位置以及里面调用的是哪一个ScrollView
接下来是Scroll View组件上挂的代码
接下来是Item上面对应的代码,大家可以认真的看一下,其实很简单的
效果图:
大家有什么不懂的地方可以私聊我,这是最基本的一种加载信息的方法,下一篇会给大家介绍一种更好懂的动态加载实现信息展示