在程序开发过程中难免要涉及到列表的生成,而程序总是相似的。虽然列表中每一条及列表的处理内容不相同,然创建列表,显示列表这个过程是可以抽象出来的。也就是说可以在其他业务逻辑的实现的基础上,将这个共用的列表生成器嵌入到程序中,以防止每次都去重复创建对象设置对象坐标这个繁琐的事情。
前一段时间写过一个列表生成脚本,挺实用,应该初始化的时候指定预制体和父级,数据来的时候调用创建就可以实现一个列表显示。一直都是那样用的,但最近需要创建一个比较长的列表,用于日志信息的显示。当然前一雄段时间这个功能实现是通过翻页实现的,但考虑到能像qq这样实现列表滑动多好。于是在原先列表简易创建使用的基础上,实现了无限列表显示的功能,考虑到unity资源商店里面要好几doller。这里开源出我写的这个大家一起学习讨论。
一、最终效果
1.独立于具体程序逻辑
下面是在一个MonoBehaiver脚本中使用这个控制器的例子,可以看到这个控制器只关心列表创建的功能,在创建过程会触发相应的事件,使用时,只需要将列表中每一条都当成view层,在onVisiable事件触发时绑定好相应的事件就可以,毕竟大多数情况下,只有看得到某一条才会对其进行操作,也才有交互。
[SerializeField]
private Button m_insert;
[SerializeField]
private ScrollRect m_scrollRect;
[SerializeField]
private DemoItem m_itemPfb;
private ListCreater<DemoItem> creater;
public Direction direction;
public int datacount;
private void Awake()
{
creater = new ListView.ListCreater<global::DemoItem>(m_scrollRect, m_itemPfb, direction);
creater.onVisiable = OnCreateDemoItem;
creater.CreateItemsAsync(datacount);
m_insert.onClick.AddListener(InsertAnElement);
}
private void InsertAnElement()
{
creater.AddItem();
}
private void OnCreateDemoItem(DemoItem arg0)
{
arg0.onClicked = OnClickDemoItem;
arg0.InitItem();
}
private void OnClickDemoItem(DemoItem arg0)
{
Debug.LogFormat("移除id为 :{0}的条目", arg0.Id);
creater.RemoveItem(arg0);
}
2.数据处理能力大增
只用有限的元素来进行列表创建,在滑动过程中列表其实在快速的更新,如果删除其中一条,其后的id会快速后退一条,但对象其他数据并不会相应后退。一开始是打算一条一条的更新的,但由于滑动右边滑动条会造成数量跳跃大的问题,所以最后综合了一条一条更新跳跃更新的优点,实现了流畅且稳定的滑动效果。见图一。
(图一)
3.横向纵向都可用
列表最常见的就是从上向下拉和从左向右拉本列子也就在实现纵向的基础上实现了横向的列表显示,其中scrollbar的选项有点小问题最后和纵向的取值进行了一点适配。注意水平方向的scrollbar要选择从左到右,垂直方向的scrollbar要选择从下到上,实现效果如图二:
(图一)
二、实现的思路及步骤
1.坐标限定及按ID设置坐标
由于unity3d有自己的列表显示脚本,叫verticallayoutGroup,这样可以方便的排列出列表,但是并不能按我所想的指定的id在指定的区域显示,而且自身的区域也会受到程序的限制无法外部指定,所以只能自行获取指定的区域来设定元素,主要实现了下面三个方法:
/// <summary>
/// 设置显示区域大小
/// </summary>
/// <param name="count"></param>
public void SetContent(int count)
{
this.count = count;
content.SetSizeWithCurrentAnchors(dir == Direction.Vertical ? RectTransform.Axis.Vertical: RectTransform.Axis.Horizontal,
(dir == Direction.Vertical ? SingleHeight :SingleWidth)* count);
}
/// <summary>
/// 按比例获取区域
/// </summary>
/// <param name="ratio"></param>
/// <param name="startID"></param>
/// <param name="endID"></param>
public void CalcuateIndex(float ratio,int maxcount,out int startID,out int endID)
{
//float ratio1 = dir == Direction.Vertical ? 1 - ratio : ratio;
startID = Mathf.FloorToInt((1-ratio) * (count - BestCount + 1));
startID = startID < 0 ? 0 : startID;
endID = BestCount + startID - 1;
endID = endID > maxcount-1 ? maxcount-1 : endID;
startID = endID - BestCount + 1;
}
/// <summary>
/// 设置指定对象的坐标
/// </summary>
/// <param name="item"></param>
public void SetPosition(T item)
{
if (dir == Direction.Vertical)
{
Vector3 startPos = Vector3.down * SingleHeight * 0.5f;
item.transform.localPosition = Vector3.down * item.Id * SingleHeight + startPos;
}
else
{
Vector3 startPos = Vector3.right * SingleWidth * 0.5f;
item.transform.localPosition = Vector3.right * item.Id * SingleWidth + startPos;
}
}
其中SetContent可以按你实际最大的条目来设定滑动区域的大小,这样可以让滑动条工作起来。CalcuateIndex可以按当前的区域及最大显示的条数来计算出对应区域应该显示那几条内容。SetPosition可以按对应的ID来指定相应条目的相对于Content的坐标。
2.添加及删除条目
列表显示与更新实现后,添加和删除是挺关键的,因为会出现删除到不满整个屏幕的情况,也会出现添加时超出屏幕的情况,于是需要两个方法分别实现添加一条和隐藏一条的功能。
/// <summary>
/// 显示出一条,可以后接也可以前置
/// </summary>
/// <param name="head"></param>
/// <returns></returns>
private T ShowAnItem(bool head)
{
if (!head && _endID == totalCount) return null;
if (head && _startID == 0) return null;
Debug.Log("Show:" + (head ? "Head" : "End"));
T scr = _objectPool.GetPoolObject(pfb, parent, false);
_createdItems.Insert(!head ? _createdItems.Count : 0, scr);
scr.Id = !head ? ++_endID : --_startID;
_contentCtrl.SetPosition(scr);
if (onVisiable != null) onVisiable(scr);
return scr;
}
/// <summary>
/// 隐藏掉一条
/// </summary>
/// <param name="head"></param>
private T HideAnItem(bool head)
{
Debug.Log("Hide:" + (head ? "Head" : "End"));
if (_createdItems.Count == 0) return null;
T item = null;
if (head)
{
item = _createdItems[0];
_startID++;
}
else
{
item = _createdItems[_createdItems.Count - 1];
_endID--;
}
_objectPool.SavePoolObject(item, false);
_createdItems.Remove(item);
if (onInViesiable != null) onInViesiable.Invoke(item);
return item;
}
3.跳跃更新和单步更新
这两种情况的区别在于,跳跃更新会有延时感觉,因为涉及到隐藏后再打开。而单步更新则不涉及到隐藏问题,但会因为连续处理很多条时会出现卡顿的现象,于是在程序只使用了以下两个方法,分别在条目少的时候和条目多的时候调用(ps:都实现条目更新)
private void RefeshConnect(bool hidehead, int count)
{
//隐藏同时显示
for (int i = 0; i < count; i++)
{
if (_createdItems.Count == 0) return;
if (hidehead && _endID == totalCount) return;
if (!hidehead && _startID == 0) return;
T itemSwith = null;
if (hidehead)
{
itemSwith = _createdItems[0];
_startID++;
_endID++;
itemSwith.Id = _endID;
}
else
{
itemSwith = _createdItems[_createdItems.Count - 1];
_startID--;
_endID--;
}
if (onInViesiable != null) onInViesiable.Invoke(itemSwith);
_createdItems.Remove(itemSwith);
itemSwith.Id = hidehead ? _endID : _startID;
_createdItems.Insert(hidehead ? _createdItems.Count : 0, itemSwith);
_contentCtrl.SetPosition(itemSwith);
if (onVisiable != null) onVisiable(itemSwith);
}
}
private void RefeshJump(bool hidehead, int count)
{
if (hidehead)
{
_startID += count;
_endID += count;
}
else
{
_startID -= count;
_endID -= count;
}
for (int i = 0; i < _contentCtrl.BestCount; i++)
{
T item = null;
if (hidehead)
{
item = _createdItems[0];
}
else
{
item = _createdItems[_createdItems.Count - 1];
}
_createdItems.Remove(item);
if (onInViesiable != null) onInViesiable.Invoke(item);
_objectPool.SavePoolObject(item, false);
}
for (int i = 0; i < _contentCtrl.BestCount; i++)
{
T item = _objectPool.GetPoolObject(pfb, parent, false);
_createdItems.Add(item);
item.Id = _startID + i;
_contentCtrl.SetPosition(item);
if (onVisiable != null) onVisiable(item);
}
}
3.比例适配
在纵向时使用的scrollbar是从下到上,面横向时使用的是从左到右。当条目创建在最左边时此时的scrollrect的水平值是0相对于纵向最上面是1的情况,所以单独写了一个方向管理的适配类如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace ListView.Internal
{
public class ScrollCtrl<T> where T : MonoBehaviour, IListItem
{
private ScrollRect scrollRect;
private Scrollbar verticalScrollbar { get { return scrollRect.verticalScrollbar; } }
private Scrollbar horizontalScrollbar { get { return scrollRect.horizontalScrollbar; } }
public UnityEngine.Events.UnityAction<float> onUpdateScroll;
private Direction dir;
public ScrollCtrl(ScrollRect scrollRect, Direction dir)
{
this.scrollRect = scrollRect;
this.dir = dir;
RegistScrollEvent();
}
private void RegistScrollEvent()
{
switch (dir)
{
case Direction.Vertical:
verticalScrollbar.onValueChanged.AddListener(UpdateItems);
break;
case Direction.Horizontal:
horizontalScrollbar.onValueChanged.AddListener(UpdateItems);
break;
default:
break;
}
}
private void UpdateItems(float ratio)
{
if (onUpdateScroll != null) onUpdateScroll.Invoke(dir==Direction.Vertical ? ratio:1-ratio);
}
public float NormalizedPosition
{
get { return dir == Direction.Vertical ? scrollRect.verticalNormalizedPosition: 1 - scrollRect.horizontalNormalizedPosition; }
set { if (dir == Direction.Vertical)
scrollRect.verticalNormalizedPosition = value;
else scrollRect.horizontalNormalizedPosition = 1 - value;
}
}
}
}
在ListCreater中的时候就轻松不少,无需关心是水平的列表还是垂直的了。
4.对象管理
加载还是创建,隐藏后再次使用必然想到的是对象池,在其他工程中使用的时候有一个单独的对象池单例。这里使用的时主要是在content的子物体的创建过程,所以写了个对象池的精简版借让ListCreater使用:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace ListView.Internal
{
public class ObjectPool<T> where T : MonoBehaviour, IListItem
{
private List<T> objectPool = new List<T>();
public T GetPoolObject(T pfb, Transform parent)
{
pfb.gameObject.SetActive(true);
//遍历每数组,得到一个隐藏的对象
for (int i = 0; i < objectPool.Count; i++)
{
if (!objectPool[i].gameObject.activeSelf)
{
objectPool[i].gameObject.SetActive(true);
objectPool[i].transform.SetParent(parent, false);
pfb.gameObject.SetActive(false);
return objectPool[i];
}
}
//当没有隐藏对象时,创建一个并返回
T currGo = CreateOne(pfb, parent);
objectPool.Add(currGo);
pfb.gameObject.SetActive(false);
return currGo;
}
public void SavePoolObject(T go, bool world = false)
{
if (!objectPool.Contains(go))
{
objectPool.Add(go);
}
go.gameObject.SetActive(false);
}
public T CreateOne(T pfb, Transform parent)
{
T currentGo = GameObject.Instantiate(pfb);
currentGo.name = pfb.name;
currentGo.transform.SetParent(parent, false);
return currentGo;
}
}
}
三、源码及使用说明
已经在github上开源:
https://github.com/zouhunter/ListView
目前可以正常使用,但步骤控制过程的脚本还有点乱,还有继续优化的空间,在使用过程中会逐步完善