最近项目上需要对一些编组信息进行树状展示,为了通用,将目录树写成一个组件,完整的代码包括测试代码已经上传到了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;
}
}