在程序開發過程中難免要涉及到列表的生成,而程序總是相似的。雖然列表中每一條及列表的處理內容不相同,然創建列表,顯示列表這個過程是可以抽象出來的。也就是說可以在其他業務邏輯的實現的基礎上,將這個共用的列表生成器嵌入到程序中,以防止每次都去重複創建對象設置對象座標這個繁瑣的事情。
前一段時間寫過一個列表生成腳本,挺實用,應該初始化的時候指定預製體和父級,數據來的時候調用創建就可以實現一個列表顯示。一直都是那樣用的,但最近需要創建一個比較長的列表,用於日誌信息的顯示。當然前一雄段時間這個功能實現是通過翻頁實現的,但考慮到能像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
目前可以正常使用,但步驟控制過程的腳本還有點亂,還有繼續優化的空間,在使用過程中會逐步完善