前言
之前在项目中多次用到目录树,由于UGUI没有目录树这样组件,需要我们拿基础的UI去拼凑,但是这样拼凑的目录树一般需要制作为预制物,在我们想要迁移到别的工程时,总是因为打包且少资源而产生一些问题。而且很多新手也会遇到自己写的目录树因为逻辑问题只能打开/关闭几个层级,并不是可无限打开关闭的,且容易出现多种Bug。基于这些问题,我们可以基于UGUI的部分源码开发一个目录树组件(UITree),简单易用并且可复用。
主要内容
1.如何一键创建UITree组件
2.UITree组件的核心逻辑
3.如何使用UITree组件
实现效果
实现方法
1.不依靠预制物,在Hierarchy面板中一键创建UITree组件(类似于我们创建Button、Text等组件的方式一样),我们得结合UnityEngine.UI.DefaultControls.cs和UnityEditor.UI.MenuOptions.cs两个脚本中部分代码,因此我们得把这两个脚本的部分代码拷贝到我们自己的脚本中SpringGUI.SpringGUIDefaultControls.cs和SpringGUI.SpringGUIMenuOptions.cs。详细的代码在文章结尾给出,这里给出创建UITree组件的关键代码,与我们上一遍博文按钮的代码相似,具体如下:
public class SpringGUIMenuOptions
{
// 创建 UITree 组件
[MenuItem("GameObject/UI/UITree",false,2063)]
public static void AddUITree( MenuCommand menuCommand )
{
GameObject uiTree = SpringGUIDefaultControls.CreaatUITree(GetStandardResources());
PlaceUIElementRoot(uiTree,menuCommand);
}
// 创建 UITreeNode 模板
[MenuItem("GameObject/UI/UITreeNode",false,2064)]
public static void AddUITreeNode( MenuCommand menuCommand )
{
GameObject uiTreeNode = SpringGUIDefaultControls.CreaatUITree(GetStandardResources());
PlaceUIElementRoot(uiTreeNode,menuCommand);
}
}
public class SpringGUIDefaultControls
{
// 创建 UITree 组件的详细代码
public static GameObject CreaatUITree( Resources resources )
{
//create ui tree
//整个列表的原型我们采用原生的ScrollView组件我们给他添加GridLayoutGroup和ContentSizeFitter组件保证Content和滚动条的自适应效果
GameObject uiTree = DefaultControls.CreateScrollView(convertToDefaultResources(resources));
// UITree 是自定义的UITee组件
uiTree.AddComponent<UITree>();
//设置组件的默认尺寸
ScrollRect uiTreeScrollRect = uiTree.GetComponent<ScrollRect>();
uiTree.GetComponent<RectTransform>().sizeDelta = _defaultUITreeSize;
uiTreeScrollRect.horizontal = false;
uiTree.name = "UITree";
Transform content = uiTree.transform.FindChild("Viewport/Content");
//添加自适应组件并修改组件的自适应属性
GridLayoutGroup glg = content.gameObject.AddComponent<GridLayoutGroup>();
glg.cellSize = _defaultUITreeNodeSize;
glg.spacing = new Vector2(0 , 2);
glg.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
glg.constraintCount = 1;
ContentSizeFitter csf = content.gameObject.AddComponent<ContentSizeFitter>();
csf.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
//create root node
GameObject rootNode = CreateUITreeNode(resources);
SetParentAndAlign(rootNode, content);
return uiTree;
}
// 创建 UITreeNode 模板的详细代码
public static GameObject CreateUITreeNode( Resources resources )
{
//create tree node
//创建父级并设置锚点和模板的默认尺寸,自适应左上角
GameObject treeNode = CreateUIElementRoot("TreeNodeTemplate", _defaultUITreeNodeSize);
RectTransform treeNodeRect = treeNode.GetComponent<RectTransform>();
float size = treeNodeRect.sizeDelta.x;
treeNodeRect.anchorMin = new Vector2(0,1);
treeNodeRect.anchorMax = new Vector2(1,1);
treeNodeRect.pivot = new Vector2(0.5f,1);
treeNodeRect.sizeDelta = new Vector2(0,_defaultUITreeNodeSize.y);
// UITreeNode是自定义的节点组件
treeNode.AddComponent<UITreeNode>();
//create container for toggle/icon/text
//创建放置Toggle、Image和Text的容器,容器的另一个作用是实现不同层级后退的距离,因为有GridLayoutGroup组件是不能直接操作父级的Transform组件,所以创建一个容器来实现后退的功能
GameObject container = CreateUIObject("Container",treeNode.transform);
SetParentAndAlign(container,treeNode.transform);
RectTransform containerRect = container.GetComponent<RectTransform>();
//设置容器的默认尺寸
containerRect.sizeDelta = new Vector2(_defaultUITreeSize.x ,_defaultUITreeNodeSize.y);
//create toggle
// 创建展开/关闭子级的toggle,以及设定他的默认属性
GameObject toggle = CreateUIElementRoot("Toggle" , _defaultUITreeNodeSize);
toggle.AddComponent<Toggle>();
SetParentAndAlign(toggle, container.transform);
RectTransform toggleRect = toggle.GetComponent<RectTransform>();
toggleRect.anchorMax = new Vector2(0,1);
toggleRect.anchorMin = Vector2.zero;
toggleRect.pivot = new Vector2(0 , 0.5f);
toggleRect.sizeDelta = new Vector2(_defaultUITreeNodeSize.y , 0);
//create toogleimage
// 创建Toggle组件图标、并设置图标的属性
GameObject toggleImage = DefaultControls.CreateImage(convertToDefaultResources(resources));
SetParentAndAlign(toggleImage , toggle.transform);
RectTransform imageRect = toggleImage.GetComponent<RectTransform>();
imageRect.anchorMax = Vector2.one;
imageRect.anchorMin = Vector2.zero;
imageRect.sizeDelta = new Vector2(-8 , -8);
toggleImage.GetComponent<Image>().sprite = resources.dropdown;
toggle.GetComponent<Toggle>().targetGraphic = toggleImage.GetComponent<Image>();
//create icon container
// 创建Icon,设置Icon的属性
GameObject iconContainer = CreateUIObject("IconContainer", container.transform);
SetParentAndAlign(iconContainer, container.transform);
RectTransform icRect = iconContainer.GetComponent<RectTransform>();
icRect.anchorMax = new Vector2(0 , 1);
icRect.anchorMin = Vector2.zero;
icRect.pivot = new Vector2(0 , 0.5f);
icRect.sizeDelta = new Vector2(_defaultUITreeNodeSize.y , 0);
icRect.transform.localPosition = toggle.transform.localPosition + new Vector3(_defaultUITreeNodeSize.y , 0);
//create icon
// 将Icon图标放置在子级是因为我们利用父级去实现自适应,但是父级就没法对图片的大小进行合理的控制,所以我们放置在子级中,以便得到更好的效果
GameObject icon = DefaultControls.CreateImage(convertToDefaultResources(resources));
icon.name = "Icon";
SetParentAndAlign(icon , iconContainer.transform);
RectTransform iconRect = icon.GetComponent<RectTransform>();
iconRect.sizeDelta = new Vector2(_defaultUITreeNodeSize.y , _defaultUITreeNodeSize.y);
//create text
// 创建字体并设置它的属性
GameObject text = DefaultControls.CreateText(convertToDefaultResources(resources));
text.name = "Text";
SetParentAndAlign(text , container.transform);
RectTransform textRect = text.GetComponent<RectTransform>();
textRect.anchorMax = new Vector2(0 , 1);
textRect.anchorMin = Vector2.zero;
textRect.pivot = new Vector2(0 , 0.5f);
textRect.sizeDelta = new Vector2(size - 2 * _defaultUITreeNodeSize.y - 8 , 0 );
text.transform.localPosition = iconContainer.transform.localPosition + new Vector3(_defaultUITreeNodeSize.y + 8 , 0);
Text textText = text.GetComponent<Text>();
textText.alignment = TextAnchor.MiddleLeft;
return treeNode;
}
}
完成上述步骤的代码之后我们就可以直接在Hierarchy面板中创建UITree组件了,只是暂时还没有实现数据的挂在和显示逻辑,接下来我们讲解一下UITreeData,目录树的数据结构逻辑。
2.UITreeData 是实现目录树的关键数据结构
public class UITreeData
{
/// <summary>
/// 父级
/// </summary>
public UITreeData Parent;
/// <summary>
/// 子级
/// </summary>
public List<UITreeData> ChildNodes;
/// <summary>
/// 层级 根节点是0,层级用于计算后退的距离
/// </summary>
public int Layer = 0;
/// <summary>
/// 内容 Text显示的内容
/// </summary>
public string Name = String.Empty;
public UITreeData( ) { }
public UITreeData( string name , int layer = 0 )
{
Name = name;
Layer = layer;
Parent = null;
ChildNodes = new List<UITreeData>();
}
public UITreeData( string name, List<UITreeData> childNodes , int layer = 0 )
{
Name = name;
Parent = null;
ChildNodes = childNodes;
if ( null == ChildNodes )
ChildNodes = new List<UITreeData>();
Layer = layer;
// ResetChildren是个迭代方法,会通过迭代给所有子级赋值
ResetChildren(this);
}
}
ResetChilden() 这个迭代方法中,我们会为当前节点的子级设置父级和Layer层级以确保正确显示,具体如下:
public class UITreeData
{
private void ResetChildren( UITreeData treeData )
{
for ( int i = 0 ; i < treeData.ChildNodes.Count ; i++ )
{
UITreeData node = treeData.ChildNodes[i];
node.Parent = treeData;
node.Layer = treeData.Layer + 1;
//迭代
ResetChildren(node);
}
}
}
在这个类中还有提供几个public方法用于为当前节点添加子级或者是移除子级、或者是设置父级的操作,并提供几个重载的方法,便于操作。详细的代码会在文章的末尾给出。
3.接下来就是控制节点显示的UITreeNode类型,控制Toggle、Icon和Text的内容显示,负责开发子级关闭子级的操作,具体如下:
public class UITreeNode : UIBehaviour
{
/// <summary>
/// 节点数据
/// </summary>
private UITreeData TreeData = null;
/// <summary>
/// 持有UITree方便调用缓冲池
/// </summary>
private UITree UITree = null;
private Toggle toggle = null;
private Image icon = null;
private Text text = null;
private Transform _toggleTransform = null;
private Transform _myTransform = null;
private Transform _container = null;
/// <summary>
/// 子级GameObject
/// </summary>
private List<GameObject> _children = new List<GameObject>();
// 赋值后将自动填充显示
public void SetData( UITreeData data )
{
//获取基本组件
if(null == _myTransform)
getComponent();
//重置基本组件 比如toggle的旋转 整体内容的后退 和Toggle隐藏
resetComponent();
TreeData = data;
text.text = data.Name;
toggle.isOn = false;
//Toggle监听打开关闭事件
toggle.onValueChanged.AddListener(openOrClose);
//计算后退的距离
_container.localPosition += new Vector3(_container.GetComponent<RectTransform>().sizeDelta.y * TreeData.Layer , 0 , 0);
//设置应该显示Icon图标
if (data.ChildNodes.Count.Equals(0))
{
_toggleTransform.gameObject.SetActive(false);
icon.sprite = UITree.m_lastLayerIcon;
}
else
icon.sprite = toggle.isOn ? UITree.m_openIcon : UITree.m_closeIcon;
}
private void openOrClose( bool isOn )
{
if ( isOn ) openChildren();
else closeChildren();
}
//在关闭时用迭代方法将所有的子级关闭
protected void closeChildren( )
{
for (int i = 0; i < _children.Count; i++)
{
UITreeNode node = _children[i].GetComponent<UITreeNode>();
node.RemoveListener();
node.closeChildren();
}
UITree.push(_children);
_children = new List<GameObject>();
}
}
closeChildren()迭代方法会在关闭节点时对所有已经打开的子节点进行关闭操作。
4.接下来我们讲解UITree关键组件,其中有缓冲池,以减小大量节点反复生成带来的性能消耗,以及唯一对外接口SetData,只要像SetData传入UITreeData即可驱动组件完成目录树的绘制,稍后会有使用的方法。
如上图所示,我们可以对未打开子节点的节点设置一个图标,对打开子节点的节点设置一个图标和没有子节点的节点设置一个图标。
public class UITree : UIBehaviour
{
[HideInInspector]
//根节点
public UITreeData TreeRoot = null;
[HideInInspector]
//根节点组件
public UITreeNode TreeRootNode = null;
private Transform m_container = null;
private GameObject m_nodePrefab = null;
//节点模板
public GameObject NodePrefab
{
get { return m_nodePrefab ?? ( m_nodePrefab = m_container.GetChild(0).gameObject ); }
set { m_nodePrefab = value; }
}
public void SetData( UITreeData rootData )
{
if ( null == m_container )
getComponent();
TreeRootNode.SetData(rootData);
}
//缓冲池的详细设计在结尾给出
}
5.通过上面的伪代码讲解,我们已经完成了UITree组件的设计,接下来我们如何使用它。
在Hierarchy面板中右键UI->UITree即可生成,然后我们给它添加一个Program.cs驱动UITree。具体如下:(目录树的创建还可以根据UITreeData中提供的其他方法创建,类似于Xml的创建)
public class Program : MonoBehaviour
{
private void Start ()
{
UITreeData TreeRoot = new UITreeData("我的博客" , new List<UITreeData>()
{
new UITreeData("Unity专栏",new List<UITreeData>()
{
new UITreeData("自定义UI组件专栏",new List<UITreeData>()
{
new UITreeData("饼图"),
new UITreeData("函数图",new List<UITreeData>()
{
new UITreeData("函数图上"),
new UITreeData("函数图下")
})
}),
new UITreeData("网格编程专栏"),
new UITreeData("Shader专栏",new List<UITreeData>()
{
new UITreeData("Dissolve Shader"),
new UITreeData("Transparent Shader")
})
}),
new UITreeData("C#专栏",new List<UITreeData>()
{
new UITreeData("基础篇"),
new UITreeData("高级篇",new List<UITreeData>()
{
new UITreeData("反射"),
new UITreeData("委托")
})
})
});
GameObject.Find("UITree").GetComponent<UITree>().SetData(TreeRoot);
}
}
后续拓展
1.UITree组件可以根据自己的需求进行进一步的修改和完善,比如点击节点想要触发某个事件我们可以在UITreeData中定义一个Action类型,在UITreeNode中进行监听执行即可。包括Icon小图标,我们也可以继续完善,根据自己的需求定义更多的显示方式。
2.通过阅读UGUI源码之后,我们可以在源码的基础上进行更多的我们常用的组件的创作。我们甚至可以还原Winform中的所有组件,接下来我将编写其他常用的UI组件与大家分享。
3.如果有什么问题,大家可以在评论区留言,欢迎指出不足和bug。
GitGub下载源码地址: https://github.com/ll4080333/UnityCodes
查看其他更多我的博文:
1.Unity自定义UI组件(四)双击按钮、长按按钮
2.Unity自定义UI组件(三)饼图篇
3.Unity自定义UI组件(一)函数图篇(上)