Table View
Table View是列表化显示多个项目的UI,当项目数量众多时,还可以滚动显示。
cell
Table View的各个项被称为cell。
为什么要复用cell
列表显示的项目数量很多时,如果对所有列表项目都创建cell的话,会消耗大量内存,性能会很低,为了控制内存的使用量,只创建画面内可显示的最小限度的cell,滚动时再次利用cell显示内容。
怎么做
1 创建滚动视图
创建空物体,命名为 “Table View”,并附加Scroll Rect组件
假设我要创建的是纵向滚动视图,那么将Scroll Rect组件的Vertical属性设置为ON,Horizontal属性设置为OFF
此外,为了能覆盖滚动区域之外的部分,为Table View对象附加Image组件和Mask组件,Mask组件的Show Mask Graphic属性设置为OFF
2 滚动内容的创建
创建空物体作为Table View的子元素,命名为 “Scroll Content”,将Scroll Content对象的锚点设置在父元素的左上或右上方,令Scroll Content对象的上端也重合在相同位置。此外,枢轴的位置也同样设置在Scroll Content对象的上端。
将Scroll Content对象设置为Table View对象的Scroll Rect组件的Content属性,建立关联
3 创建cell
创建空物体作为Scroll Content的子元素,命名为 “Cell”,将Cell对象的锚点设置在父元素的左上方或右上方,将枢轴的位置设置在Cell对象的上端。
在Cell的子元素中创建你需要的ui,比如图片,名字,价格等等
这个Cell会最为复制源,后面通过脚本来动态生成所必须的cell
4 cell脚本
为了能适用于显示各种数据的Table View,最好创建泛型类,实现通用处理,通过继承来实现别的处理。
// ViewController.cs
using UnityEngine;
[RequireComponent(typeof(RectTransform))] // 需要RectTransform
public class ViewController : MonoBehaviour
{
// 缓存Rect Transform
private RectTransform cachedRectTransform;
public RectTransform CachedRectTransform
{
get {
if(cachedRectTransform == null)
{ cachedRectTransform = GetComponent<RectTransform>(); }
return cachedRectTransform;
}
}
// 获取、设置视图名称的属性
public virtual string Title { get { return ""; } set {} }
}
// TableViewCell.cs
using UnityEngine;
public class TableViewCell<T> : ViewController // 继承ViewController
{
// 更新cell的内容
public virtual void UpdateContent(T itemData)
{
// 实际处理运用继承类来实现
}
// 保持cell对应的列表项目索引
public int DataIndex { get; set; }
// 获取并设置cell高度
public float Height
{
get { return CachedRectTransform.sizeDelta.y; }
set {
Vector2 sizeDelta = CachedRectTransform.sizeDelta;
sizeDelta.y = value;
CachedRectTransform.sizeDelta = sizeDelta;
}
}
// 获取并设置cell上端的位置
public Vector2 Top
{
get {
Vector3[] corners = new Vector3[4];
CachedRectTransform.GetLocalCorners(corners);
return CachedRectTransform.anchoredPosition +
new Vector2(0.0f, corners[1].y);
}
set {
Vector3[] corners = new Vector3[4];
CachedRectTransform.GetLocalCorners(corners);
CachedRectTransform.anchoredPosition =
value - new Vector2(0.0f, corners[1].y);
}
}
// 获取并设置cell下端位置
public Vector2 Bottom
{
get {
Vector3[] corners = new Vector3[4];
CachedRectTransform.GetLocalCorners(corners);
return CachedRectTransform.anchoredPosition +
new Vector2(0.0f, corners[3].y);
}
set {
Vector3[] corners = new Vector3[4];
CachedRectTransform.GetLocalCorners(corners);
CachedRectTransform.anchoredPosition =
value - new Vector2(0.0f, corners[3].y);
}
}
}
// ShopItemTableViewCell.cs
using UnityEngine;
using UnityEngine.UI;
// 定义列表项目的数据类
public class ShopItemData
{
public string iconName; // 图标名
public string name; // 产品名
public int price; // 价格
public string description; // 说明
}
// 继承TableViewCell<T>类
public class ShopItemTableViewCell : TableViewCell<ShopItemData>
{
[SerializeField] private Image iconImage; // 显示图标的图像
[SerializeField] private Text nameLabel; // 显示产品名的文本
[SerializeField] private Text priceLabel; // 显示价格的文本
// 覆盖更新cell内容
public override void UpdateContent(ShopItemData itemData)
{
nameLabel.text = itemData.name;
priceLabel.text = itemData.price.ToString();
}
}
将ShopItemTableViewCell组件附加到Cell对象上,将Cell子元素中的图像、文本等ui对象附加给ShopItemTableViewCell的对应属性。
5 Table View脚本
// TableViewController.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
[RequireComponent(typeof(ScrollRect))]
public class TableViewController<T> : ViewController // 継承ViewController
{
protected List<T> tableData = new List<T>(); // 保持列表项目数据
[SerializeField] private RectOffset padding; // 填充滚动内容
[SerializeField] private float spacingHeight = 4.0f; // 各个cell的间隔
// 缓存Scroll Rect
private ScrollRect cachedScrollRect;
public ScrollRect CachedScrollRect
{
get {
if(cachedScrollRect == null) {
cachedScrollRect = GetComponent<ScrollRect>(); }
return cachedScrollRect;
}
}
protected virtual void Awake()
{
}
// 返回列表项目对应的cell高度
protected virtual float CellHeightAtIndex(int index)
{
// 通过继承类实现返回实际值
return 0.0f;
}
// 更新滚动内容整体高度
protected void UpdateContentSize()
{
// 计算出滚动内容的整体高度
float contentHeight = 0.0f;
for(int i=0; i<tableData.Count; i++)
{
contentHeight += CellHeightAtIndex(i);
if(i > 0) { contentHeight += spacingHeight; }
}
// 设置滚动内容的高度
Vector2 sizeDelta = CachedScrollRect.content.sizeDelta;
sizeDelta.y = padding.top + contentHeight + padding.bottom;
CachedScrollRect.content.sizeDelta = sizeDelta;
}
#region セルを作成するメソッドとセルの内容を更新するメソッドの実装
[SerializeField] private GameObject cellBase; // 源cell,用于克隆
private LinkedList<TableViewCell<T>> cells =
new LinkedList<TableViewCell<T>>(); // 保持cell
protected virtual void Start()
{
cellBase.SetActive(false);
#region セルを再利用する処理の実装
// Scroll RectコンポーネントのOn Value Changedイベントのイベントリスナーを設定する
CachedScrollRect.onValueChanged.AddListener(OnScrollPosChanged);
#endregion
}
// 创建cell
private TableViewCell<T> CreateCellForIndex(int index)
{
// 复制cell源
GameObject obj = Instantiate(cellBase) as GameObject;
obj.SetActive(true);
TableViewCell<T> cell = obj.GetComponent<TableViewCell<T>>();
// 因为如果替换父元素的话,就会失去比例和尺寸,所以需要先保持变量
Vector3 scale = cell.transform.localScale;
Vector2 sizeDelta = cell.CachedRectTransform.sizeDelta;
Vector2 offsetMin = cell.CachedRectTransform.offsetMin;
Vector2 offsetMax = cell.CachedRectTransform.offsetMax;
cell.transform.SetParent(cellBase.transform.parent);
// 设置cell的比例和尺寸
cell.transform.localScale = scale;
cell.CachedRectTransform.sizeDelta = sizeDelta;
cell.CachedRectTransform.offsetMin = offsetMin;
cell.CachedRectTransform.offsetMax = offsetMax;
UpdateCellForIndex(cell, index);
cells.AddLast(cell);
return cell;
}
// 更新cell内容
private void UpdateCellForIndex(TableViewCell<T> cell, int index)
{
// 设置与cell相对应的列表项目索引
cell.DataIndex = index;
if(cell.DataIndex >= 0 && cell.DataIndex <= tableData.Count-1)
{
// 如果有与cell相对应的列表项目,则显示cell,更新内容,设置高度
cell.gameObject.SetActive(true);
cell.UpdateContent(tableData[cell.DataIndex]);
cell.Height = CellHeightAtIndex(cell.DataIndex);
}
else
{
// 如果没有与cell相对应的列表项目,则cell不显示
cell.gameObject.SetActive(false);
}
}
#endregion
#region visibleRectの定義とvisibleRectを更新するメソッドの実装
private Rect visibleRect; // 用矩形表示cell的形式显示列表项目的范围
[SerializeField] private RectOffset visibleRectPadding;
// 更新visibleRect
private void UpdateVisibleRect()
{
// visibleRect的位置时距离滚动内容的基准点的相对位置
visibleRect.x =
CachedScrollRect.content.anchoredPosition.x + visibleRectPadding.left;
visibleRect.y =
-CachedScrollRect.content.anchoredPosition.y + visibleRectPadding.top;
// visibleRect的尺寸时滚动视图的尺寸+填充内容的尺寸
visibleRect.width = CachedRectTransform.rect.width +
visibleRectPadding.left + visibleRectPadding.right;
visibleRect.height = CachedRectTransform.rect.height +
visibleRectPadding.top + visibleRectPadding.bottom;
}
#endregion
#region テーブルビューの表示内容を更新する処理の実装
protected void UpdateContents()
{
UpdateContentSize(); // スクロールさせる内容のサイズを更新する
UpdateVisibleRect(); // visibleRectを更新する
if(cells.Count < 1)
{
// 如果一个cell都没有的话,就搜索最先进入visibleRect范围的列表项目,创建相应的cell
Vector2 cellTop = new Vector2(0.0f, -padding.top);
for(int i=0; i<tableData.Count; i++)
{
float cellHeight = CellHeightAtIndex(i);
Vector2 cellBottom = cellTop + new Vector2(0.0f, -cellHeight);
if((cellTop.y <= visibleRect.y &&
cellTop.y >= visibleRect.y - visibleRect.height) ||
(cellBottom.y <= visibleRect.y &&
cellBottom.y >= visibleRect.y - visibleRect.height))
{
TableViewCell<T> cell = CreateCellForIndex(i);
cell.Top = cellTop;
break;
}
cellTop = cellBottom + new Vector2(0.0f, spacingHeight);
}
// 如果visibleRect的范围为空的话,则创建cell
FillVisibleRectWithCells();
}
else
{
// 如果已经有cell的话,从最开始的cell依次设置对应的列表项目的索引并修改更新位置和内容
LinkedListNode<TableViewCell<T>> node = cells.First;
UpdateCellForIndex(node.Value, node.Value.DataIndex);
node = node.Next;
while(node != null)
{
UpdateCellForIndex(node.Value, node.Previous.Value.DataIndex + 1);
node.Value.Top =
node.Previous.Value.Bottom + new Vector2(0.0f, -spacingHeight);
node = node.Next;
}
// 如果visibleRect的范围为空的话,则创建cell
FillVisibleRectWithCells();
}
}
// 创建visibleRect范围内可显示的数量的cell
private void FillVisibleRectWithCells()
{
if(cells.Count < 1)
{
return;
}
// 如果显示的最后的cell相应的列表项目之后有列表项目
// 并且,该cell进入到visibleRect范围内的话,则创建相应的cell
TableViewCell<T> lastCell = cells.Last.Value;
int nextCellDataIndex = lastCell.DataIndex + 1;
Vector2 nextCellTop = lastCell.Bottom + new Vector2(0.0f, -spacingHeight);
while(nextCellDataIndex < tableData.Count &&
nextCellTop.y >= visibleRect.y - visibleRect.height)
{
TableViewCell<T> cell = CreateCellForIndex(nextCellDataIndex);
cell.Top = nextCellTop;
lastCell = cell;
nextCellDataIndex = lastCell.DataIndex + 1;
nextCellTop = lastCell.Bottom + new Vector2(0.0f, -spacingHeight);
}
}
#endregion
#region セルを再利用する処理の実装
private Vector2 prevScrollPos; // 保持之前的滚动位置
// 当滚动滚动视图时调用
public void OnScrollPosChanged(Vector2 scrollPos)
{
// 更新visibleRect
UpdateVisibleRect();
// 根据滚动的方向,再次利用cell,更新显示
ReuseCells((scrollPos.y < prevScrollPos.y)? 1: -1);
prevScrollPos = scrollPos;
}
// 再次利用cell,更新显示
private void ReuseCells(int scrollDirection)
{
if(cells.Count < 1)
{
return;
}
if(scrollDirection > 0)
{
// 向上滚动时,令超出visibleRect范围之上的cell
// 依次向下移动,更新内容
TableViewCell<T> firstCell = cells.First.Value;
while(firstCell.Bottom.y > visibleRect.y)
{
TableViewCell<T> lastCell = cells.Last.Value;
UpdateCellForIndex(firstCell, lastCell.DataIndex + 1);
firstCell.Top = lastCell.Bottom + new Vector2(0.0f, -spacingHeight);
cells.AddLast(firstCell);
cells.RemoveFirst();
firstCell = cells.First.Value;
}
// 如果visibleRect的范围为空,则创建cell
FillVisibleRectWithCells();
}
else if(scrollDirection < 0)
{
// 向下滚动时,令超出visibleRect范围之下的cell
// 依次向上移动,更新内容
TableViewCell<T> lastCell = cells.Last.Value;
while(lastCell.Top.y < visibleRect.y - visibleRect.height)
{
TableViewCell<T> firstCell = cells.First.Value;
UpdateCellForIndex(lastCell, firstCell.DataIndex - 1);
lastCell.Bottom = firstCell.Top + new Vector2(0.0f, spacingHeight);
cells.AddFirst(lastCell);
cells.RemoveLast();
lastCell = cells.Last.Value;
}
}
}
#endregion
}
接下来,创建ShopItemTableViewController.cs脚本,继承TableViewControler< T >类
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
[RequireComponent(typeof(ScrollRect))]
public class ShopItemTableViewController : TableViewController<ShopItemData>
{
// 读取列表项目数据
private void LoadData()
{
// 这里自己构造一些测试用的数据
tableData = new List<ShopItemData>() {
new ShopItemData { iconName="drink1", name="WATER",
price=100, description="Nothing else, just water." },
new ShopItemData { iconName="drink2", name="SODA",
price=150, description="Sugar free and low calorie." },
new ShopItemData { iconName="drink3", name="COFFEE",
price=200, description="How would you like your coffee?" },
new ShopItemData { iconName="drink4", name="ENERGY DRINK",
price=300, description="It will give you wings." },
new ShopItemData { iconName="drink5", name="BEER",
price=500, description="It's a drink for grown-ups." },
new ShopItemData { iconName="drink6", name="COCKTAIL",
price=1000, description="A cocktail made of tropical fruits." },
new ShopItemData { iconName="fruit1", name="CHERRY",
price=100, description="Do you like cherries?" },
new ShopItemData { iconName="fruit2", name="ORANGE",
price=150, description="It contains much vitamin C." },
new ShopItemData { iconName="fruit3", name="APPLE",
price=300, description="Enjoy the goodness without peeling it." },
new ShopItemData { iconName="fruit4", name="BANANA",
price=400, description="Don't slip on its peel." },
new ShopItemData { iconName="fruit5", name="GRAPE",
price=600, description="It's not a grapefruit." },
new ShopItemData { iconName="fruit6", name="PINEAPPLE",
price=800, description="It's not a hand granade." },
new ShopItemData { iconName="gun1", name="MINI GUN",
price=1000, description="A tiny concealed carry gun." },
new ShopItemData { iconName="gun2", name="CLASSIC GUN",
price=2000, description="The gun that was used by a pirate." },
new ShopItemData { iconName="gun3", name="STANDARD GUN",
price=4000, description="Just a standard weapon." },
new ShopItemData { iconName="gun4", name="REVOLVER",
price=5000, description="It can hold a maximum of 6 bullets." },
new ShopItemData { iconName="gun5", name="AUTO RIFLE",
price=10000, description="It can fire automatically and rapidly." },
new ShopItemData { iconName="gun6", name="SPACE GUN",
price=20000, description="A weapon that comes from the future." },
};
// 更新滚动内容的大小
UpdateContents();
}
// 返回与列表项目相对应的cell高度
protected override float CellHeightAtIndex(int index)
{
if(index >= 0 && index <= tableData.Count-1)
{
if(tableData[index].price >= 1000)
{
// 如果产品价格超过1000,则返回显示cell高度240.0f
return 240.0f;
}
if(tableData[index].price >= 500)
{
// 如果产品价格超过500,则返回显示cell高度160.0f
return 160.0f;
}
}
return 128.0f;
}
protected override void Awake()
{
base.Awake();
// 缓存图标精灵表单中所包含的精灵
SpriteSheetManager.Load("IconAtlas");
}
protected override void Start()
{
base.Start();
// 读取列表项目的数据
LoadData();
#region アイテム一覧画面をナビゲーションビューに対応させる
if(navigationView != null)
{
// ナビゲーションビューの最初のビューとして設定する
navigationView.Push(this);
}
#endregion
}
#region アイテム一覧画面をナビゲーションビューに対応させる
// ナビゲーションビューを保持
[SerializeField] private NavigationViewController navigationView;
// ビューのタイトルを返す
public override string Title { get { return "SHOP"; } }
#endregion
#region アイテム詳細画面に遷移させる処理の実装
// アイテム詳細画面のビューを保持
[SerializeField] private ShopDetailViewController detailView;
// セルが選択されたときに呼ばれるメソッド
public void OnPressCell(ShopItemTableViewCell cell)
{
if(navigationView != null)
{
// 選択されたセルからアイテムのデータを取得して、アイテム詳細画面の内容を更新する
detailView.UpdateContent(tableData[cell.DataIndex]);
// アイテム詳細画面に遷移する
navigationView.Push(detailView);
}
}
#endregion
}
将ShopItemTableViewController 组件附加到Table View对象上,设置Cell Base属性为复制源cell对象。
至此,复用cell的Table View就制作完成了。
附:
通过精灵名来获取对应的精灵
using UnityEngine;
using System.Collections.Generic;
public class SpriteSheetManager
{
private static Dictionary<string, Dictionary<string, Sprite>> spriteSheets =
new Dictionary<string, Dictionary<string, Sprite>>();
// 将精灵列表中包含的精灵读取出来并缓存
public static void Load(string path)
{
if(!spriteSheets.ContainsKey(path))
{
spriteSheets.Add(path, new Dictionary<string, Sprite>());
}
// 读取精灵,缓存名称与路径
Sprite[] sprites = Resources.LoadAll<Sprite>(path);
foreach(Sprite sprite in sprites)
{
if(!spriteSheets[path].ContainsKey(sprite.name))
{
spriteSheets[path].Add(sprite.name, sprite);
}
}
}
// 由精灵名返回精灵
public static Sprite GetSpriteByName(string path, string name)
{
if(spriteSheets.ContainsKey(path) && spriteSheets[path].ContainsKey(name))
{
return spriteSheets[path][name];
}
return null;
}
}