uGUI学习篇: Unity复用cell显示列表ui

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/linxinfa/article/details/89281353

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;
	}
}

猜你喜欢

转载自blog.csdn.net/linxinfa/article/details/89281353
今日推荐