在Unity中實現TreeView
在Unity中要實現如下的樹形狀結構顯示,是比較複雜的,相比於專門做二維的軟體,效果也不咋樣;但想想畢竟Unity主要是開發三維場景的工具,用來做二維介面確實有點可笑,但是也不是說不能實現,只要Unity有Image,那什麼都是可以實現的...【Demo下載地址】
如何實現這種效果呢,主要的難點在哪裡?載入資料並儲存到物件中不難,利用得到的資料進行UI動態生成才是關鍵。
程式設計思路:
1、建立一個通用的預製體,載入各級的條目
2、以TreeView物件為父級,載入第一層選項,以每一個條目下的一個元件為父級載入下一級
3、解析xml得到的資料物件轉化為條目物件
4、在條目物件自身的指令碼下進行初始化
能想到以上一幾點基本上也就有了大概思路了;本程式的實現過程使用了pureMvc架構(如果不想用,那解析xml資料的方法直接寫入到靜態工具類中也未嘗不可,只是程式的耦合度增加),UI介面相對比較獨立
那麼具體實現需要進行以下幾點的深入:
1、製作資料模型
2、解析xml資料到資料模型
3、正確使用pureMVC架構
4、製作UI預製體
5、將資料物件轉換為UI物件
6、動態顯示功能
7、事件註冊
這些問題 是在製作程式的過程中遇到的,也算是一點小經驗吧,不一定都能適用,實現TreeView的效果也可能只有這一種。
下面是具體實現:
一、資料模型:
樹形圖資料的一個特點就是父節點有一堆子節點,和xml資料差不多(解析起來也比較容易),定義了一個XMLDataProxy類,有一個父結點和子結點列表
public class XMLDataProxy : Proxy {
public XMLDataProxy(string name):base(name){
}
public XMLDataProxy ParentNode { get; set; }
public List<XMLDataProxy> ChildNodes { get; set; }
}
二、解析XML資料到XMLDataProxy
由於程式使用了PureMVC架構,所以這裡直接用Commond類來實現以上資料的解析和註冊
public class XMLLoadCommond : SimpleCommand
{
public override void Execute(INotification notification)
{
string xmlPath = Application.streamingAssetsPath + "/Projects.xml";
XMLDataProxy m_XmlDataProxy = XMLParse(xmlPath); //解析資料的核心
AppFacade.Instance.RegisterProxy(m_XmlDataProxy); //將解析得到的資料註冊了Model
AppFacade.Instance.SendNotification(NotiConst.ThreeView); //通知View層進行解析
}
/// <summary>
/// 從XML源載入資料
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static XMLDataProxy XMLParse(string fileName)
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(fileName);
XmlNode rootNode = xmlDoc.SelectSingleNode("Projects");
XMLDataProxy rootProxyNode = new XMLDataProxy("Project");
XMLDataProxyAppendChild(rootNode, rootProxyNode); //解析第一層資料
return rootProxyNode;
}
/// <summary>
/// 利用XMLNode建立xmlDataProxy
/// </summary>
/// <param name="xNode"></param>
/// <returns></returns>
public static void XMLDataProxyAppendChild(XmlNode xNode, XMLDataProxy parent) //遞迴遍歷所有子結點資料
{
if (!xNode.HasChildNodes)
{
return;
}
else
{
foreach (XmlElement item in xNode)
{
XMLDataProxy cNode = new XMLDataProxy(item.GetAttribute("name"));
cNode.ParentNode = parent;
if (parent.ChildNodes == null)
{
parent.ChildNodes = new List<XMLDataProxy>();
}
parent.ChildNodes.Add(cNode);
XMLDataProxyAppendChild(item, cNode);
}
}
}
}
三、關於PureMvc的使用
pureMVC中的mvc的物件要經過註冊也能使用,實現INotifier介面的物件一般有三種傳送資訊的方式,一是隻傳送通知的內容,二是傳送通知的內容和資料包(object)三是還要傳送型別。其中最常用的是第二種,對指定的觀察者傳送一個數據包。但由於載入 的資料常常不是死的,而是動態加載出來的而且常常不是一個簡單的int,float ,string 和bool等型別。更為高階的方式是將proxy 資料註冊到model中,在Media需要的時候查找出來就可以了。而載入資料這個過程,交給commond最適合不過了,相對於靜態工具類也更為合理。
1.commond類的註冊與執行
void Start () {
AppFacade.Instance.RegisterCommand(NotiConst.LoadProject, typeof(XMLLoadCommond));
}
// Update is called once per frame
void OnGUI()
{
if (GUILayout.Button("載入xml文件"))
{
AppFacade.Instance.SendNotification(NotiConst.LoadProject);
}
}
2、meida類的註冊
public class TreeView : Mediator
{
private XMLDataProxy m_XmlDataProxy;
private GameObject itemPfb;//一級節點
public override IList<NotiConst> ListNotificationInterests()
{
return new List<NotiConst>() { NotiConst.ThreeView};
}
public override void HandleNotification(INotification notification)
{
//將資訊載入到View
m_XmlDataProxy = AppFacade.Instance.RetrieveProxy("Project") as XMLDataProxy;
LoadAllNodes();
}
void Awake()
{
itemPfb = transform.Find("Item").gameObject;
AppFacade.Instance.RegisterMediator(this);
}
}
四、製作預製體
1、根目錄上新增兩個元件,一個是動態調整,一個是垂直列表
2、通用預製體物件
將Item作為根目錄的子物體,這樣就可以實現列表效果了
這個過程最最關鍵的是錨點問題,可以從上圖看到,TreeView物件的錨點在左上角的同時,其中心點也必須是左上角,因為動態載入Item時,希望是最高點不發出動,整個向下擴張。
在第一級製作成功後,想第二級當然也要實現如第一級一樣的擴充套件性,於是在item下建立了一個Panel,也增加了如TreeView的兩個元件。但為什麼要放置在Contant下,見下圖:
如果想偷懶不在程式碼中來控制Panel的座標,這個方法實現是不錯,在Panel下增加一個item後的效果變成:
很顯然後Panel的對齊方式是右上角對齊...也就是說只需要一個item物件,就可以建立無限多個層級了。其實有人已經發現了這個過程中每創建出來一個末端就會多出一個Content和一個Panel,這位下來影響程式的效能,遇到這個問題,其實將panel和item拆開載入也是可以的。,這個程式還是很有優化的空間的,在說吧...
五,將資料物件轉換為UI物件
這個過程是在TreeView得到了資料後進行的,由於建立物件過程中需要進行多個調整,最好的辦法還是建立一個TreeItem指令碼,來操作創建出來 的物件:
public class TreeItem : MonoBehaviour
{
public string NodeName {
get
{
return GetComponentInChildren<Text>().text;
}
set
{
GetComponentInChildren<Text>().text = value;
}
}//中文名
public Transform Parent {
set { transform.SetParent(value); }
get { return transform.parent; }
}
public Transform ClildPanel {
get { return transform.Find("Contant/Panel"); }
}
public ToggleGroup ToggleGroup{
set { GetComponent<Toggle>().group = value; }
}//設定group
private bool isLastOne;//最後一層,取消自動選中
private bool Selected
{
get { return ClildPanel.gameObject.activeSelf; }
set { ClildPanel.gameObject.SetActive(value); }
}//是否選中(同級之中最多隻有一個可以選中)
private Toggle m_toggle;
void Awake () {
ClildPanel.gameObject.SetActive(false);
m_toggle = GetComponent<Toggle>();
m_toggle.onValueChanged.AddListener((x)=> { Selected = x; });
}
void Start()
{
transform.localScale = Vector3.one;
}
}
將物件上的一些元件性質與屬性進行繫結,這樣,只要對這些屬性進行賦值和取值就可以了
在TreeView獲得資料後進行建立的過程寫在自身指令碼中:
public class TreeView : Mediator
{
private XMLDataProxy m_XmlDataProxy;
private GameObject itemPfb;//一級節點
public override IList<NotiConst> ListNotificationInterests()
{
return new List<NotiConst>() { NotiConst.ThreeView};
}
public override void HandleNotification(INotification notification)
{
//將資訊載入到View
m_XmlDataProxy = AppFacade.Instance.RetrieveProxy("Project") as XMLDataProxy;
LoadAllNodes();
}
void Awake()
{
itemPfb = transform.Find("Item").gameObject;
AppFacade.Instance.RegisterMediator(this);
}
private void LoadAllNodes()
{
ToggleGroup tgg = gameObject.AddComponent<ToggleGroup>();
foreach (var item in m_XmlDataProxy.ChildNodes)
{
CreateQuaders(item, transform, itemPfb,tgg);
}
itemPfb.SetActive(false);
AppFacade.Instance.RemoveCommand(NotiConst.LoadProject);
}
public void CreateQuaders(XMLDataProxy data, Transform parent, GameObject btnpfb,ToggleGroup tgg) //遞迴全部建立
{
TreeItem treeItem = Instantiate(btnpfb).GetComponent<TreeItem>();
treeItem.NodeName = data.ProxyName;
treeItem.Parent = parent;
treeItem.ToggleGroup = tgg;
if (data.ChildNodes != null && data.ChildNodes.Count > 0)
{
ToggleGroup Newtgg = treeItem.gameObject.AddComponent<ToggleGroup>();
foreach (var item in data.ChildNodes)
{
CreateQuaders(item, treeItem.ClildPanel, btnpfb, Newtgg);
}
}
else//給最後一級toggle新增事件
{
treeItem.ToggleSelected(OnLastItemSelected);
}
}
private void OnLastItemSelected(string itemName)
{
Debug.LogWarning("you need notify"+itemName);
}
}
六、動態顯示功能
動態顯示滑鼠移動到的物件上,這些條目本身是考慮用Button的,但想到這個問題的時候,實現過程比較困難,還需要考慮哪些條目剛剛打開了,哪些條目需要進行關閉。最後想到Toggle有一個屬性是可以實現多個Toggle同時只有一個是isOn。這樣一想和treeView的效果簡直就是一模一樣嘛。於是在建立物件的過程上中只需要在父級上新增 一 個ToggleGroup,並將Toggle的這個屬性賦上這個ToggleGroup。這樣就實現了點選開啟對應的專案。
說好的滑鼠移動到物件上可以動態顯示呢,不急,點選都實現了還差一個OnMouseEnter類似的功能麼,當然,在UGUI上滑鼠移入的事件是繼承了IPointerEnterHandler,完善條目指令碼TreeItem如下,其中點選事件的註冊也寫入了,值得注意的最後一級常常需要觸發不同的事件(不單單是展開),於是在建立物件的時候判斷並將OnLastItemSelected這個方法註冊到上上點中事件。
public class TreeItem : MonoBehaviour,IPointerEnterHandler {
public string NodeName {
get
{
return GetComponentInChildren<Text>().text;
}
set
{
GetComponentInChildren<Text>().text = value;
}
}//中文名
public Transform Parent {
set { transform.SetParent(value); }
get { return transform.parent; }
}
public Transform ClildPanel {
get { return transform.Find("Contant/Panel"); }
}
public ToggleGroup ToggleGroup{
set { GetComponent<Toggle>().group = value; }
}//設定group
private bool isLastOne;//最後一層,取消自動選中
private bool Selected
{
get { return ClildPanel.gameObject.activeSelf; }
set { ClildPanel.gameObject.SetActive(value); }
}//是否選中(同級之中最多隻有一個可以選中)
private Toggle m_toggle;
void Awake () {
ClildPanel.gameObject.SetActive(false);
m_toggle = GetComponent<Toggle>();
m_toggle.onValueChanged.AddListener((x)=> { Selected = x; });
}
void Start()
{
transform.localScale = Vector3.one;
}
public void OnPointerEnter(PointerEventData eventData)
{
m_toggle.isOn = !isLastOne;
}
/// <summary>
/// 點選回撥
/// </summary>
/// <param name="action"></param>
public void ToggleSelected(UnityAction<string> action)
{
isLastOne = true;
m_toggle.onValueChanged.RemoveAllListeners();
m_toggle.onValueChanged.AddListener((x) => { if (x) action(NodeName); });
}
}