最近項目上需要對一些編組信息進行樹狀展示,爲了通用,將目錄樹寫成一個組件,完整的代碼包括測試代碼已經上傳到了github上目錄樹代碼,代碼需要配合RectTransform的錨點使用,所以寫了一個編輯器擴展,可以像創建其他UI組件一樣創建一個目錄樹編輯器代碼
使用方式
1.在編輯器中創建一個Tree
2.在代碼中獲取並創建
首先要在對應面板中獲取到Tree組件,用於構造一棵樹的函數爲
Tree.GenerateTree(TreeNode rootNode)
這個函數需要傳入一個節點,這個節點可以是一個空節點,也可以是一個已經構造好父子關係的根節點。傳入之後,會根據這個節點的子節點信息創建一棵樹。
TreeNode支持鏈式添加父子關係,可以像下面一樣構造一棵樹。
TreeNode root = new TreeNode("1");
root.AddChild(new TreeNode("11")
.AddChild(new TreeNode("111")
.AddChild(new TreeNode("1111")
.AddChild(new TreeNode("11111"))
.AddChild(new TreeNode("11112")))
.AddChild(new TreeNode("1112")))
.AddChild(new TreeNode("112")))
.AddChild(new TreeNode("12")
.AddChild(new TreeNode("121"))
.AddChild(new TreeNode("122")
.AddChild(new TreeNode("1221"))
.AddChild(new TreeNode("1222")))
.AddChild(new TreeNode("123")))
.AddChild(new TreeNode("13")
.AddChild(new TreeNode("131")));
tree.GenerateTree(root);
但大多數情況下,樹的信息應該是由另外一個信息類或者數據表之類的東西轉來的,所以父子關係的創建需要自己寫。
代碼
1.樹的節點
在大多數的時候外界並不需要對TreeNode進行直接控制,但是在初始化樹的時候,要傳入一個已經設置好父子關係的根節點,這個類裏面的幾個核心方法都利用了遞歸,所以在更新一個節點的信息的時候,這個節點的所有子節點也會改變。
樹的節點的初始化是根據對應的模板來,目前還不是很靈活,模板結構變化比較大的話需要修改InitEnity方法。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace XFramework.UI
{
/// <summary>
/// 樹節點
/// </summary>
public class TreeNode
{
private RectTransform m_Rect;
/// <summary>
/// 當前結點所歸屬的樹
/// </summary>
private Tree m_Tree;
/// <summary>
/// 子結點
/// </summary>
private List<TreeNode> m_Childs;
/// <summary>
/// 父結點
/// </summary>
private TreeNode m_Parent;
/// <summary>
/// 開啓狀態
/// </summary>
private bool m_IsOn;
/// <summary>
/// 層級
/// </summary>
private int m_level;
/// <summary>
/// 顯示文字
/// </summary>
public string Text { get; private set; }
public TreeNode()
{
m_IsOn = true;
m_Childs = new List<TreeNode>();
}
public TreeNode(string text) : this()
{
this.Text = text;
}
/// <summary>
/// 初始化場景中對應的實體
/// </summary>
/// <param name="tree"></param>
private void InitEnity(Tree tree)
{
m_Tree = tree;
// 創建自己
if (m_Parent == null)
{
m_Rect = Object.Instantiate(m_Tree.NodeTemplate, m_Tree.NodeTemplate.transform.parent.Find("Root")).GetComponent<RectTransform>();
m_level = 0;
}
else
{
m_Rect = Object.Instantiate(m_Tree.NodeTemplate, m_Parent.m_Rect.Find("Child")).GetComponent<RectTransform>();
m_level = m_Parent.m_level + 1;
}
// UI組件設置
m_Rect.Find("Toggle").GetComponent<Toggle>().onValueChanged.AddListener((value) =>
{
m_IsOn = value;
Refresh(value);
Root.RefreshPos();
tree.onOn_Off.Invoke(value, this);
});
m_Rect.Find("Button").GetComponent<Button>().onClick.AddListener(() =>
{
tree.onSelectNode.Invoke(this);
});
m_Rect.Find("Button").Find("Text").GetComponent<Text>().text = this.Text;
}
/// <summary>
/// 刷新位置及顯示隱藏
/// </summary>
private void Refresh(bool isOn)
{
isOn &= m_IsOn;
if (isOn)
{
foreach (var item in m_Childs)
{
item.Refresh(isOn);
item.m_Rect.localScale = new Vector3(1, 1, 1);
}
}
else
{
foreach (var item in m_Childs)
{
item.Refresh(isOn);
item.m_Rect.localScale = new Vector3(1, 0, 1);
}
}
}
/// <summary>
/// 刷新位置
/// </summary>
public void RefreshPos()
{
int index = 0;
if (m_Parent != null)
{
foreach (var item in m_Parent.m_Childs)
{
if (item == this)
break;
index += item.GetItemCount();
}
}
m_Rect.anchoredPosition = new Vector2(0, -index * 30);
foreach (var item in m_Childs)
{
item.RefreshPos();
}
}
/// <summary>
/// 添加一個父子關係
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public TreeNode AddChild(TreeNode item)
{
m_Childs.Add(item);
item.m_Parent = this;
return this;
}
public TreeNode AddChild(string text)
{
return AddChild(new TreeNode(text));
}
/// <summary>
/// 根據已有的父子關係創建一顆(子)樹
/// </summary>
/// <param name="m_Parent"></param>
/// <param name="gameObject"></param>
public void CreateTree(Tree tree)
{
InitEnity(tree);
// 繼續創建
foreach (var child in m_Childs)
{
child.CreateTree(m_Tree);
}
if (m_Parent == null)
{
RefreshPos();
}
}
/// <summary>
/// 添加一個父子關係並創建實體
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public TreeNode CreateChild(TreeNode item)
{
AddChild(item);
item.InitEnity(m_Tree);
Refresh(m_IsOn);
Root.RefreshPos();
return this;
}
public TreeNode CreateChild(string text)
{
TreeNode item = new TreeNode(text);
return CreateChild(item);
}
/// <summary>
/// 刪除自身
/// </summary>
public void Delete()
{
if(m_Parent == null)
{
Debug.Log("根結點不能刪除");
return;
}
Object.Destroy(m_Rect.gameObject);
m_Parent.m_Childs.Remove(this);
Root.RefreshPos();
}
/// <summary>
/// 子物體的數量 +1
/// </summary>
public int GetItemCount()
{
if (m_Childs.Count == 0 || !m_IsOn)
{
return 1;
}
else
{
int count = 0;
foreach (var item in m_Childs)
{
count += item.GetItemCount();
}
return count + 1;
}
}
/// <summary>
/// 獲取自己在父物體種的索引
/// </summary>
public int GetSiblingIndex()
{
if(m_Parent != null)
{
int index = 0;
foreach (var item in m_Parent.m_Childs)
{
if (item == this)
return index;
}
}
return 0;
}
/// <summary>
/// 根結點
/// </summary>
public TreeNode Root
{
get
{
TreeNode item = this;
while(item.m_Parent != null)
{
item = item.m_Parent;
}
return item;
}
}
/// <summary>
/// 重置父物體
/// </summary>
/// <param name="parent"></param>
public void SetParent(TreeNode parent)
{
m_Parent.m_Childs.Remove(this);
parent.AddChild(this);
}
/// <summary>
/// 通過字符串找尋子節點
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public TreeNode Find(string path)
{
var temp = path.Split(new char[] { '/' }, 2);
if (temp.Length == 1)
return this;
TreeNode node = null;
foreach (var item in m_Childs)
{
if (item.Text == temp[0])
{
node = item;
break;
}
}
if (node == null)
return node;
else
return node.Find(temp[1]);
}
/// <summary>
/// 根據索引獲取子節點
/// </summary>
public TreeNode GetChild(int index)
{
return m_Childs[index];
}
}
}
2.樹
這個類的使用方式就像Button一樣,可以先註冊事件,然後調用GenerateTree在場景中構造一棵樹,注意事件的實際調用者不是Tree而是TreeNode
using UnityEngine;
namespace XFramework.UI
{
/// <summary>
/// 目錄樹
/// </summary>
public class Tree : MonoBehaviour
{
/// <summary>
/// 模板
/// </summary>
public GameObject NodeTemplate { get; private set; }
/// <summary>
/// 樹的根節點
/// </summary>
private TreeNode m_RootTreeNode;
public string rootText = "Root";
/// <summary>
/// 節點被選中的事件
/// </summary>
public TreeEvent onSelectNode = new TreeEvent();
/// <summary>
/// 節點展開關閉事件
/// </summary>
public SwitchEvent onOn_Off = new SwitchEvent();
private void Awake()
{
NodeTemplate = transform.Find("NodeTemplate").gameObject;
NodeTemplate.GetComponent<RectTransform>().anchoredPosition = new Vector2(10000, 10000);
}
/// <summary>
/// 構造一棵樹
/// </summary>
/// <param name="rootNode">父子關係已經設置好的根節點</param>
public void GenerateTree(TreeNode rootNode)
{
if (m_RootTreeNode != null)
m_RootTreeNode.Delete();
m_RootTreeNode = rootNode;
m_RootTreeNode.CreateTree(this);
}
/// <summary>
/// 刪除某個節點
/// </summary>
/// <param name="path">路徑</param>
public bool Delete(string path)
{
TreeNode node = m_RootTreeNode.Find(path);
if (node != null)
{
node.Delete();
return true;
}
return false;
}
public class TreeEvent : UnityEngine.Events.UnityEvent<TreeNode> { }
public class SwitchEvent : UnityEngine.Events.UnityEvent<bool,TreeNode> { }
}
}
3.Editor
最後放上編輯器代碼,可以直接在場景中創建出一棵樹
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
/// <summary>
/// 用於創建一些自定義UI組件
/// </summary>
public class CreateComponent
{
private static DefaultControls.Resources s_StandardResources;
private const string kUILayerName = "UI";
private const string kStandardSpritePath = "UI/Skin/UISprite.psd";
private const string kBackgroundSpritePath = "UI/Skin/Background.psd";
private const string kInputFieldBackgroundPath = "UI/Skin/InputFieldBackground.psd";
private const string kKnobPath = "UI/Skin/Knob.psd";
private const string kCheckmarkPath = "UI/Skin/Checkmark.psd";
private const string kDropdownArrowPath = "UI/Skin/DropdownArrow.psd";
private const string kMaskPath = "UI/Skin/UIMask.psd";
[MenuItem("GameObject/UI/Tree")]
public static void CreateTree()
{
GameObject parent = Selection.activeGameObject;
RectTransform tree = new GameObject("Tree").AddComponent<RectTransform>();
tree.SetParent(parent.transform);
tree.localPosition = Vector3.zero;
tree.gameObject.AddComponent<XFramework.UI.Tree>();
tree.sizeDelta = new Vector2(180, 30);
// 設置模板
RectTransform itemTemplate = new GameObject("NodeTemplate").AddComponent<RectTransform>();
itemTemplate.SetParent(tree);
itemTemplate.pivot = new Vector2(0, 1);
itemTemplate.anchorMin = new Vector2(0.5f, 1);
itemTemplate.anchorMax = new Vector2(0.5f, 1);
itemTemplate.anchoredPosition = new Vector2(-90, 0);
itemTemplate.sizeDelta = new Vector2(180, 30);
RectTransform button = DefaultControls.CreateButton(GetStandardResources()).GetComponent<RectTransform>();
button.SetParent(itemTemplate);
button.anchoredPosition = new Vector2(10, 0);
button.sizeDelta = new Vector2(160, 30);
RectTransform toggle = DefaultControls.CreateToggle(GetStandardResources()).GetComponent<RectTransform>();
toggle.SetParent(itemTemplate);
Object.DestroyImmediate(toggle.Find("Label").gameObject);
toggle.anchoredPosition = new Vector2(-80, 0);
toggle.sizeDelta = new Vector2(20, 20);
RectTransform child = new GameObject("Child").AddComponent<RectTransform>();
child.SetParent(itemTemplate);
child.pivot = new Vector2(0, 1);
child.anchorMin = new Vector2(0, 1);
child.anchorMax = new Vector2(0, 1);
child.sizeDelta = Vector2.zero;
child.anchoredPosition = new Vector2(20, -30);
// 設置樹的跟結點位置
RectTransform treeRoot = new GameObject("Root").AddComponent<RectTransform>();
treeRoot.SetParent(tree);
treeRoot.anchoredPosition = new Vector2(-90, 0);
treeRoot.sizeDelta = new Vector2(100, 30);
}
private static DefaultControls.Resources GetStandardResources()
{
if (s_StandardResources.standard == null)
{
s_StandardResources.standard = AssetDatabase.GetBuiltinExtraResource<Sprite>(kStandardSpritePath);
s_StandardResources.background = AssetDatabase.GetBuiltinExtraResource<Sprite>(kBackgroundSpritePath);
s_StandardResources.inputField = AssetDatabase.GetBuiltinExtraResource<Sprite>(kInputFieldBackgroundPath);
s_StandardResources.knob = AssetDatabase.GetBuiltinExtraResource<Sprite>(kKnobPath);
s_StandardResources.checkmark = AssetDatabase.GetBuiltinExtraResource<Sprite>(kCheckmarkPath);
s_StandardResources.dropdown = AssetDatabase.GetBuiltinExtraResource<Sprite>(kDropdownArrowPath);
s_StandardResources.mask = AssetDatabase.GetBuiltinExtraResource<Sprite>(kMaskPath);
}
return s_StandardResources;
}
}