先上几张效果图:
如果你需要的也是这种效果,那你就来对地方了!
目前,我们这个树形菜单展现出来的功能如下:
1、可以动态配置数据源;
2、点击每个元素的上下文菜单按钮(也就是图中的三角形按钮),可以收缩或展开它的子元素;
3、可以单独判断某一元素的复选框是否被勾选,或者直接获取当前树形菜单中所有被勾选的元素;
4、树形菜单统一控制其下所有子元素按钮的事件分发;
5、可自动调节的滚动视野边缘,根据当前可见的子元素数量进行横向以及纵向的伸缩;
一、首先,我们先制作子元素的模板(Template),也就是图中菜单的单个元素,用它来根据数据源动态克隆出多个子元素,这里的话,很显然我们的模板是由两个Button加一个Toggle和一个Text组成的,如下:
ContextButton TreeViewToggle TreeViewButton(TreeViewText)
图中的text是一个文本框,用于描述此元素的名称或内容,它们对应的结构就是这样:
二、我们的每个子元素都会携带一个TreeViewItem脚本,用于描述自身在整个树形菜单中与其他元素的父子关系,而整个树形菜单的控制由TreeViewControl来实现,首先,TreeViewControl会根据提供的数据源来生成所有的子元素,当然,改变数据源之后进行重新生成的时候也是这个方法,干的事情很简单,就是用模板不停的创建元素,并给他们建立父子关系:
/// <summary> /// 生成树形菜单 /// </summary> public void GenerateTreeView() { //删除可能已经存在的树形菜单元素 if (_treeViewItems != null) { for (int i = 0; i < _treeViewItems.Count; i++) { Destroy(_treeViewItems[i]); } _treeViewItems.Clear(); } //重新创建树形菜单元素 _treeViewItems = new List<GameObject>(); for (int i = 0; i < Data.Count; i++) { GameObject item = Instantiate(Template); if (Data[i].ParentID == -1) { item.GetComponent<TreeViewItem>().SetHierarchy(0); item.GetComponent<TreeViewItem>().SetParent(null); } else { TreeViewItem tvi = _treeViewItems[Data[i].ParentID].GetComponent<TreeViewItem>(); item.GetComponent<TreeViewItem>().SetHierarchy(tvi.GetHierarchy() + 1); item.GetComponent<TreeViewItem>().SetParent(tvi); tvi.AddChildren(item.GetComponent<TreeViewItem>()); } item.transform.name = "TreeViewItem"; item.transform.FindChild("TreeViewText").GetComponent<Text>().text = Data[i].Name; item.transform.SetParent(TreeItems); item.transform.localPosition = Vector3.zero; item.transform.localScale = Vector3.one; item.transform.localRotation = Quaternion.Euler(Vector3.zero); item.SetActive(true); _treeViewItems.Add(item); } }
三、树形菜单生成完毕之后此时所有元素虽然都记录了自身与其他元素的父子关系,但他们的位置都是在Vector3.zero的,毕竟我们的菜单元素在创建的时候都是一股脑儿的丢到原点位置的,创建君可不管这么多元素挤在一堆会不会憋死,好吧,之后规整列队的事情就交给刷新君来完成了,刷新君玩的一手好递归,它会遍历所有元素并剔除不可见的元素(也就是点击三角按钮隐藏了),并将它们一个一个的重新排列整齐,子排在父之后,孙排在子之后,以此类推......它会遍历每个元素的子元素列表,发现子元素可见便进入子元素列表,发现孙元素可见便进入孙元素列表:
/// <summary> /// 刷新树形菜单 /// </summary> public void RefreshTreeView() { _yIndex = 0; _hierarchy = 0; //复制一份菜单 _treeViewItemsClone = new List<GameObject>(_treeViewItems); //用复制的菜单进行刷新计算 for (int i = 0; i < _treeViewItemsClone.Count; i++) { //已经计算过或者不需要计算位置的元素 if (_treeViewItemsClone[i] == null || !_treeViewItemsClone[i].activeSelf) { continue; } TreeViewItem tvi = _treeViewItemsClone[i].GetComponent<TreeViewItem>(); _treeViewItemsClone[i].GetComponent<RectTransform>().localPosition = new Vector3(tvi.GetHierarchy() * HorizontalItemSpace, _yIndex,0); _yIndex += (-(ItemHeight + VerticalItemSpace)); if (tvi.GetHierarchy() > _hierarchy) { _hierarchy = tvi.GetHierarchy(); } //如果子元素是展开的,继续向下刷新 if (tvi.IsExpanding) { RefreshTreeViewChild(tvi); } _treeViewItemsClone[i] = null; } //重新计算滚动视野的区域 float x = _hierarchy * HorizontalItemSpace + ItemWidth; float y = Mathf.Abs(_yIndex); transform.GetComponent<ScrollRect>().content.sizeDelta = new Vector2(x, y); //清空复制的菜单 _treeViewItemsClone.Clear(); } /// <summary> /// 刷新元素的所有子元素 /// </summary> void RefreshTreeViewChild(TreeViewItem tvi) { for (int i = 0; i < tvi.GetChildrenNumber(); i++) { tvi.GetChildrenByIndex(i).gameObject.GetComponent<RectTransform>().localPosition = new Vector3(tvi.GetChildrenByIndex(i).GetHierarchy() * HorizontalItemSpace, _yIndex, 0); _yIndex += (-(ItemHeight + VerticalItemSpace)); if (tvi.GetChildrenByIndex(i).GetHierarchy() > _hierarchy) { _hierarchy = tvi.GetChildrenByIndex(i).GetHierarchy(); } //如果子元素是展开的,继续向下刷新 if (tvi.GetChildrenByIndex(i).IsExpanding) { RefreshTreeViewChild(tvi.GetChildrenByIndex(i)); } int index = _treeViewItemsClone.IndexOf(tvi.GetChildrenByIndex(i).gameObject); if (index >= 0) { _treeViewItemsClone[index] = null; } } }
四、菜单已经创建完毕并且经过了一轮刷新,此时它展示出来的就是这样一个所有子元素都展开的形状(我在demo中指定了数据源,关于数据源怎么设置在后面):
我们要在每个元素都携带的脚本TreeViewItem中对自身的那个三角形的上下文按钮监听,当鼠标点击它时它的子元素就会被折叠或者展开:
/// <summary> /// 点击上下文菜单按钮,元素的子元素改变显示状态 /// </summary> void ContextButtonClick() { if (IsExpanding) { transform.FindChild("ContextButton").GetComponent<RectTransform>().localRotation = Quaternion.Euler(0, 0, 90); IsExpanding = false; ChangeChildren(this, false); } else { transform.FindChild("ContextButton").GetComponent<RectTransform>().localRotation = Quaternion.Euler(0, 0, 0); IsExpanding = true; ChangeChildren(this, true); } //刷新树形菜单 Controler.RefreshTreeView(); } /// <summary> /// 改变某一元素所有子元素的显示状态 /// </summary> void ChangeChildren(TreeViewItem tvi, bool value) { for (int i = 0; i < tvi.GetChildrenNumber(); i++) { tvi.GetChildrenByIndex(i).gameObject.SetActive(value); ChangeChildren(tvi.GetChildrenByIndex(i), value); } }
五、对所有的子元素进行统一的事件分发,这里主要就有鼠标点击这一个事件:
每个元素都会注册这个事件:(TreeViewItem.cs)
void Awake() { //上下文按钮点击回调 transform.FindChild("ContextButton").GetComponent<Button>().onClick.AddListener(ContextButtonClick); transform.FindChild("TreeViewButton").GetComponent<Button>().onClick.AddListener(delegate () { Controler.ClickItem(gameObject); }); }树形菜单控制器统一分发:(TreeViewControl.cs)
public delegate void ClickItemdelegate(GameObject item); public event ClickItemdelegate ClickItemEvent; /// <summary> /// 鼠标点击子元素事件 /// </summary> public void ClickItem(GameObject item) { ClickItemEvent(item); }
根据元素名称进行筛选,获取此元素的选中状态,如果存在同名元素的话这个可能不好使:
/// <summary> /// 返回指定名称的子元素是否被勾选 /// </summary> public bool ItemIsCheck(string itemName) { for (int i = 0; i < _treeViewItems.Count; i++) { if (_treeViewItems[i].transform.FindChild("TreeViewText").GetComponent<Text>().text == itemName) { return _treeViewItems[i].transform.FindChild("TreeViewToggle").GetComponent<Toggle>().isOn; } } return false; }
/// <summary> /// 返回树形菜单中被勾选的所有子元素名称 /// </summary> public List<string> ItemsIsCheck() { List<string> items = new List<string>(); for (int i = 0; i < _treeViewItems.Count; i++) { if (_treeViewItems[i].transform.FindChild("TreeViewToggle").GetComponent<Toggle>().isOn) { items.Add(_treeViewItems[i].transform.FindChild("TreeViewText").GetComponent<Text>().text); } } return items; }
/// <summary> /// 当前树形菜单的数据源 /// </summary> [HideInInspector] public List<TreeViewData> Data = null;
/// <summary> /// 树形菜单数据 /// </summary> public class TreeViewData { /// <summary> /// 数据内容 /// </summary> public string Name; /// <summary> /// 数据所属的父ID /// </summary> public int ParentID; }
八、属性面板的参数:
Template:当前树形菜单的元素模板;
TreeItems:当前树形菜单的元素根物体,自动指定的,这个别去动;
VerticalItemSpace:相邻元素之间的纵向间距;
HorizontalItemSpace:不同层级元素之间的横向间距;
ItemWidth:元素的宽度,若自行修改过Template,这里的值需要自己去计算Template的大概宽度;
ItemHeight:元素的高度,若自行修改过Template,这里的值需要自己去计算Template的大概高度;
导入TreeView.unitypackage以后,先在场景中创建一个Canvas(画布),然后右键直接创建TreeView:
之后在其他脚本中拿到这个TreeView,直接为他指定数据源(我这里是手动生成,篇幅有点长):
//生成数据 List<TreeViewData> datas = new List<TreeViewData>(); TreeViewData data = new TreeViewData(); data.Name = "第一章"; data.ParentID = -1; datas.Add(data); data = new TreeViewData(); data.Name = "1.第一节"; data.ParentID = 0; datas.Add(data); data = new TreeViewData(); data.Name = "1.第二节"; data.ParentID = 0; datas.Add(data); data = new TreeViewData(); data.Name = "1.1.第一课"; data.ParentID = 1; datas.Add(data); data = new TreeViewData(); data.Name = "1.2.第一课"; data.ParentID = 2; datas.Add(data); data = new TreeViewData(); data.Name = "1.1.第二课"; data.ParentID = 1; datas.Add(data); data = new TreeViewData(); data.Name = "1.1.1.第一篇"; data.ParentID = 3; datas.Add(data); data = new TreeViewData(); data.Name = "1.1.1.第二篇"; data.ParentID = 3; datas.Add(data); data = new TreeViewData(); data.Name = "1.1.1.2.第一段"; data.ParentID = 7; datas.Add(data); data = new TreeViewData(); data.Name = "1.1.1.2.第二段"; data.ParentID = 7; datas.Add(data); data = new TreeViewData(); data.Name = "1.1.1.2.1.第一题"; data.ParentID = 8; datas.Add(data); //指定数据源 TreeView.Data = datas;
//重新生成树形菜单 TreeView.GenerateTreeView(); //刷新树形菜单 TreeView.RefreshTreeView();
//注册子元素的鼠标点击事件 TreeView.ClickItemEvent += CallBack; void CallBack(GameObject item) { Debug.Log("点击了 " + item.transform.FindChild("TreeViewText").GetComponent<Text>().text); }
bool isCheck = TreeView.ItemIsCheck("第一章"); Debug.Log("当前树形菜单中的元素 第一章 " + (isCheck?"已被选中!":"未被选中!"));
List<string> items = TreeView.ItemsIsCheck(); for (int i = 0; i < items.Count; i++) { Debug.Log("当前树形菜单中被选中的元素有:" + items[i]); }
插件链接:http://download.csdn.net/detail/qq992817263/9750031
请注意Unity的版本为5.5.0!