先上几张效果图:

        

如果你需要的也是这种效果,那你就来对地方了!

目前,我们这个树形菜单展现出来的功能如下:

1、可以动态配置数据源;

2、点击每个元素的上下文菜单按钮(也就是图中的三角形按钮),可以收缩或展开它的子元素;

3、可以单独判断某一元素的复选框是否被勾选,或者直接获取当前树形菜单中所有被勾选的元素;

4、树形菜单统一控制其下所有子元素按钮的事件分发;

5、可自动调节的滚动视野边缘,根据当前可见的子元素数量进行横向以及纵向的伸缩;

一、首先,我们先制作子元素的模板(Template),也就是图中菜单的单个元素,用它来根据数据源动态克隆出多个子元素,这里的话,很显然我们的模板是由两个Button加一个Toggle和一个Text组成的,如下:

ContextButton    TreeViewToggle     TreeViewButton(TreeViewText)

图中的text是一个文本框,用于描述此元素的名称或内容,它们对应的结构就是这样:

二、我们的每个子元素都会携带一个TreeViewItem脚本,用于描述自身在整个树形菜单中与其他元素的父子关系,而整个树形菜单的控制由TreeViewControl来实现,首先,TreeViewControl会根据提供的数据源来生成所有的子元素,当然,改变数据源之后进行重新生成的时候也是这个方法,干的事情很简单,就是用模板不停的创建元素,并给他们建立父子关系:

  1. /// <summary>
  2. /// 生成树形菜单
  3. /// </summary>
  4. public void GenerateTreeView()
  5. {
  6. //删除可能已经存在的树形菜单元素
  7. if (_treeViewItems != null)
  8. {
  9. for (int i = 0; i < _treeViewItems.Count; i++)
  10. {
  11. Destroy(_treeViewItems[i]);
  12. }
  13. _treeViewItems.Clear();
  14. }
  15. //重新创建树形菜单元素
  16. _treeViewItems = new List<GameObject>();
  17. for (int i = 0; i < Data.Count; i++)
  18. {
  19. GameObject item = Instantiate(Template);
  20. if (Data[i].ParentID == -1)
  21. {
  22. item.GetComponent<TreeViewItem>().SetHierarchy(0);
  23. item.GetComponent<TreeViewItem>().SetParent(null);
  24. }
  25. else
  26. {
  27. TreeViewItem tvi = _treeViewItems[Data[i].ParentID].GetComponent<TreeViewItem>();
  28. item.GetComponent<TreeViewItem>().SetHierarchy(tvi.GetHierarchy() + 1);
  29. item.GetComponent<TreeViewItem>().SetParent(tvi);
  30. tvi.AddChildren(item.GetComponent<TreeViewItem>());
  31. }
  32. item.transform.name = "TreeViewItem";
  33. item.transform.FindChild("TreeViewText").GetComponent<Text>().text = Data[i].Name;
  34. item.transform.SetParent(TreeItems);
  35. item.transform.localPosition = Vector3.zero;
  36. item.transform.localScale = Vector3.one;
  37. item.transform.localRotation = Quaternion.Euler(Vector3.zero);
  38. item.SetActive(true);
  39. _treeViewItems.Add(item);
  40. }
  41. }

三、树形菜单生成完毕之后此时所有元素虽然都记录了自身与其他元素的父子关系,但他们的位置都是在Vector3.zero的,毕竟我们的菜单元素在创建的时候都是一股脑儿的丢到原点位置的,创建君可不管这么多元素挤在一堆会不会憋死,好吧,之后规整列队的事情就交给刷新君来完成了,刷新君玩的一手好递归,它会遍历所有元素并剔除不可见的元素(也就是点击三角按钮隐藏了),并将它们一个一个的重新排列整齐,子排在父之后,孙排在子之后,以此类推......它会遍历每个元素的子元素列表,发现子元素可见便进入子元素列表,发现孙元素可见便进入孙元素列表:

  1. /// <summary>
  2. /// 刷新树形菜单
  3. /// </summary>
  4. public void RefreshTreeView()
  5. {
  6. _yIndex = 0;
  7. _hierarchy = 0;
  8. //复制一份菜单
  9. _treeViewItemsClone = new List<GameObject>(_treeViewItems);
  10. //用复制的菜单进行刷新计算
  11. for (int i = 0; i < _treeViewItemsClone.Count; i++)
  12. {
  13. //已经计算过或者不需要计算位置的元素
  14. if (_treeViewItemsClone[i] == null || !_treeViewItemsClone[i].activeSelf)
  15. {
  16. continue;
  17. }
  18. TreeViewItem tvi = _treeViewItemsClone[i].GetComponent<TreeViewItem>();
  19. _treeViewItemsClone[i].GetComponent<RectTransform>().localPosition = new Vector3(tvi.GetHierarchy() * HorizontalItemSpace, _yIndex,0);
  20. _yIndex += (-(ItemHeight + VerticalItemSpace));
  21. if (tvi.GetHierarchy() > _hierarchy)
  22. {
  23. _hierarchy = tvi.GetHierarchy();
  24. }
  25. //如果子元素是展开的,继续向下刷新
  26. if (tvi.IsExpanding)
  27. {
  28. RefreshTreeViewChild(tvi);
  29. }
  30. _treeViewItemsClone[i] = null;
  31. }
  32. //重新计算滚动视野的区域
  33. float x = _hierarchy * HorizontalItemSpace + ItemWidth;
  34. float y = Mathf.Abs(_yIndex);
  35. transform.GetComponent<ScrollRect>().content.sizeDelta = new Vector2(x, y);
  36. //清空复制的菜单
  37. _treeViewItemsClone.Clear();
  38. }
  39. /// <summary>
  40. /// 刷新元素的所有子元素
  41. /// </summary>
  42. void RefreshTreeViewChild(TreeViewItem tvi)
  43. {
  44. for (int i = 0; i < tvi.GetChildrenNumber(); i++)
  45. {
  46. tvi.GetChildrenByIndex(i).gameObject.GetComponent<RectTransform>().localPosition = new Vector3(tvi.GetChildrenByIndex(i).GetHierarchy() * HorizontalItemSpace, _yIndex, 0);
  47. _yIndex += (-(ItemHeight + VerticalItemSpace));
  48. if (tvi.GetChildrenByIndex(i).GetHierarchy() > _hierarchy)
  49. {
  50. _hierarchy = tvi.GetChildrenByIndex(i).GetHierarchy();
  51. }
  52. //如果子元素是展开的,继续向下刷新
  53. if (tvi.GetChildrenByIndex(i).IsExpanding)
  54. {
  55. RefreshTreeViewChild(tvi.GetChildrenByIndex(i));
  56. }
  57. int index = _treeViewItemsClone.IndexOf(tvi.GetChildrenByIndex(i).gameObject);
  58. if (index >= 0)
  59. {
  60. _treeViewItemsClone[index] = null;
  61. }
  62. }
  63. }

我这里将所有的元素复制了一份用于计算位置,主要就是为了防止在进行一轮刷新时某个元素被访问两次或以上,因为刷新的时候会遍历所有可见元素,如果第一次访问了元素A(元素A的位置被刷新),根据元素A的子元素列表访问到了元素B(元素B的位置被刷新),一直到达子元素的底部后,当不存在更深层次的子元素时,那么返回到元素A之后的元素继续访问,这时在所有元素列表中元素B可能在元素A之后,也就是说元素B已经通过父元素访问过了,不需要做再次访问,他的位置已经是最新的了,而之后根据列表索引很可能再次访问到元素B,如果是这样的话元素B的位置又要被刷新一次,甚至多次,性能影响不说,第二次计算的位置已经不是正确的位置了(总之也就是一个计算逻辑的问题,没看明白可以直接忽略)。

四、菜单已经创建完毕并且经过了一轮刷新,此时它展示出来的就是这样一个所有子元素都展开的形状(我在demo中指定了数据源,关于数据源怎么设置在后面):

我们要在每个元素都携带的脚本TreeViewItem中对自身的那个三角形的上下文按钮监听,当鼠标点击它时它的子元素就会被折叠或者展开:

  1. /// <summary>
  2. /// 点击上下文菜单按钮,元素的子元素改变显示状态
  3. /// </summary>
  4. void ContextButtonClick()
  5. {
  6. if (IsExpanding)
  7. {
  8. transform.FindChild("ContextButton").GetComponent<RectTransform>().localRotation = Quaternion.Euler(0, 0, 90);
  9. IsExpanding = false;
  10. ChangeChildren(this, false);
  11. }
  12. else
  13. {
  14. transform.FindChild("ContextButton").GetComponent<RectTransform>().localRotation = Quaternion.Euler(0, 0, 0);
  15. IsExpanding = true;
  16. ChangeChildren(this, true);
  17. }
  18. //刷新树形菜单
  19. Controler.RefreshTreeView();
  20. }
  21. /// <summary>
  22. /// 改变某一元素所有子元素的显示状态
  23. /// </summary>
  24. void ChangeChildren(TreeViewItem tvi, bool value)
  25. {
  26. for (int i = 0; i < tvi.GetChildrenNumber(); i++)
  27. {
  28. tvi.GetChildrenByIndex(i).gameObject.SetActive(value);
  29. ChangeChildren(tvi.GetChildrenByIndex(i), value);
  30. }
  31. }

IsExpanding做为每个元素的字段用于设置或读取自身子元素的显示状态,这里根据改变的状态会递归循环此元素的所有子元素及孙元素,让他们可见或隐藏。

五、对所有的子元素进行统一的事件分发,这里主要就有鼠标点击这一个事件:

每个元素都会注册这个事件:(TreeViewItem.cs)

  1. void Awake()
  2. {
  3. //上下文按钮点击回调
  4. transform.FindChild("ContextButton").GetComponent<Button>().onClick.AddListener(ContextButtonClick);
  5. transform.FindChild("TreeViewButton").GetComponent<Button>().onClick.AddListener(delegate () {
  6. Controler.ClickItem(gameObject);
  7. });
  8. }

树形菜单控制器统一分发:(TreeViewControl.cs)

  1. public delegate void ClickItemdelegate(GameObject item);
  2. public event ClickItemdelegate ClickItemEvent;
  3. /// <summary>
  4. /// 鼠标点击子元素事件
  5. /// </summary>
  6. public void ClickItem(GameObject item)
  7. {
  8. ClickItemEvent(item);
  9. }

六、获取元素的复选框状态判断是否被勾选:

根据元素名称进行筛选,获取此元素的选中状态,如果存在同名元素的话这个可能不好使:

  1. /// <summary>
  2. /// 返回指定名称的子元素是否被勾选
  3. /// </summary>
  4. public bool ItemIsCheck(string itemName)
  5. {
  6. for (int i = 0; i < _treeViewItems.Count; i++)
  7. {
  8. if (_treeViewItems[i].transform.FindChild("TreeViewText").GetComponent<Text>().text == itemName)
  9. {
  10. return _treeViewItems[i].transform.FindChild("TreeViewToggle").GetComponent<Toggle>().isOn;
  11. }
  12. }
  13. return false;
  14. }

返回树形菜单中所有被勾选的子元素名称集合:

  1. /// <summary>
  2. /// 返回树形菜单中被勾选的所有子元素名称
  3. /// </summary>
  4. public List<string> ItemsIsCheck()
  5. {
  6. List<string> items = new List<string>();
  7. for (int i = 0; i < _treeViewItems.Count; i++)
  8. {
  9. if (_treeViewItems[i].transform.FindChild("TreeViewToggle").GetComponent<Toggle>().isOn)
  10. {
  11. items.Add(_treeViewItems[i].transform.FindChild("TreeViewText").GetComponent<Text>().text);
  12. }
  13. }
  14. return items;
  15. }

七、接下来是我们的数据格式TreeViewData,树形菜单的数据源是由这个格式组成的集合:

  1. /// <summary>
  2. /// 当前树形菜单的数据源
  3. /// </summary>
  4. [HideInInspector]
  5. public List<TreeViewData> Data = null;

每一个TreeViewData代表一个元素,Name为显示的文本内容,ParentID为它指向的父元素在整个数据集合中的索引,从0开始,-1代表不存在父元素的根元素,当然有时候数据源并不是这个样子的,可能是XML,可能是json,不过都可以通过解析数据源之后再变换成这种方式:

  1. /// <summary>
  2. /// 树形菜单数据
  3. /// </summary>
  4. public class TreeViewData
  5. {
  6. /// <summary>
  7. /// 数据内容
  8. /// </summary>
  9. public string Name;
  10. /// <summary>
  11. /// 数据所属的父ID
  12. /// </summary>
  13. public int ParentID;
  14. }

八、属性面板的参数:

Template:当前树形菜单的元素模板;

TreeItems:当前树形菜单的元素根物体,自动指定的,这个别去动;

VerticalItemSpace:相邻元素之间的纵向间距;

HorizontalItemSpace:不同层级元素之间的横向间距;

ItemWidth:元素的宽度,若自行修改过Template,这里的值需要自己去计算Template的大概宽度;

ItemHeight:元素的高度,若自行修改过Template,这里的值需要自己去计算Template的大概高度;

九、我已经将TreeView打包成了一个插件,在Unity中导入他,便可以直接使用TreeView:

导入TreeView.unitypackage以后,先在场景中创建一个Canvas(画布),然后右键直接创建TreeView:

之后在其他脚本中拿到这个TreeView,直接为他指定数据源(我这里是手动生成,篇幅有点长):

  1. //生成数据
  2. List<TreeViewData> datas = new List<TreeViewData>();
  3. TreeViewData data = new TreeViewData();
  4. data.Name = "第一章";
  5. data.ParentID = -1;
  6. datas.Add(data);
  7. data = new TreeViewData();
  8. data.Name = "1.第一节";
  9. data.ParentID = 0;
  10. datas.Add(data);
  11. data = new TreeViewData();
  12. data.Name = "1.第二节";
  13. data.ParentID = 0;
  14. datas.Add(data);
  15. data = new TreeViewData();
  16. data.Name = "1.1.第一课";
  17. data.ParentID = 1;
  18. datas.Add(data);
  19. data = new TreeViewData();
  20. data.Name = "1.2.第一课";
  21. data.ParentID = 2;
  22. datas.Add(data);
  23. data = new TreeViewData();
  24. data.Name = "1.1.第二课";
  25. data.ParentID = 1;
  26. datas.Add(data);
  27. data = new TreeViewData();
  28. data.Name = "1.1.1.第一篇";
  29. data.ParentID = 3;
  30. datas.Add(data);
  31. data = new TreeViewData();
  32. data.Name = "1.1.1.第二篇";
  33. data.ParentID = 3;
  34. datas.Add(data);
  35. data = new TreeViewData();
  36. data.Name = "1.1.1.2.第一段";
  37. data.ParentID = 7;
  38. datas.Add(data);
  39. data = new TreeViewData();
  40. data.Name = "1.1.1.2.第二段";
  41. data.ParentID = 7;
  42. datas.Add(data);
  43. data = new TreeViewData();
  44. data.Name = "1.1.1.2.1.第一题";
  45. data.ParentID = 8;
  46. datas.Add(data);
  47. //指定数据源
  48. TreeView.Data = datas;

然后生成树形菜单,连带刷新一次:

  1. //重新生成树形菜单
  2. TreeView.GenerateTreeView();
  3. //刷新树形菜单
  4. TreeView.RefreshTreeView();

然后注册子元素的鼠标点击事件(委托类型为返回值void,带一个Gameobject类型参数,参数item为被鼠标点中的那个元素的gameobject):

  1. //注册子元素的鼠标点击事件
  2. TreeView.ClickItemEvent += CallBack;
  3. void CallBack(GameObject item)
  4. {
  5. Debug.Log("点击了 " + item.transform.FindChild("TreeViewText").GetComponent<Text>().text);
  6. }

以及要获取某一元素的勾选状态:

  1. bool isCheck = TreeView.ItemIsCheck("第一章");
  2. Debug.Log("当前树形菜单中的元素 第一章 " + (isCheck?"已被选中!":"未被选中!"));

和获取所有被勾选的元素:

  1. List<string> items = TreeView.ItemsIsCheck();
  2. for (int i = 0; i < items.Count; i++)
  3. {
  4. Debug.Log("当前树形菜单中被选中的元素有:" + items[i]);
  5. }

效果图如下:

插件链接:http://download.csdn.net/detail/qq992817263/9750031

请注意Unity的版本为5.5.0!

 
 

Unity 引擎UGUI之自定义树形菜单(TreeView)的更多相关文章

  1. js实现鼠标右键自定义菜单(弹出层),并与树形菜单(TreeView)、iframe合用(兼容IE、Firefox、Chrome)

    <table class="oa-el-panel-tree"> <tr> <td style="vertical-align: top; ...

  2. Unity引擎 UGUI

    Unity UGUI讲解 1.导入UI图片资源 2.设置参数: TextureType(纹理类型) 精灵 2D and UI SpriteMode(精灵模式)  Single(单) multiple( ...

  3. jQuery树形菜单(1)jquery.treeview

    jQuery的树形插件资料URL:http://bassistance.de/jquery-plugins/jquery-plugin-treeview/从该网站Download得到jquery.tr ...

  4. jquery树形菜单插件treeView

    Jquery的treeview很好用,如果是简单的树形菜单按照下面的源码实例模仿就可以. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Tr ...

  5. treeview树形菜单,递归

    我使用的是递归是实现无限级树形菜单: using System; using System.Collections; using System.Configuration; using System. ...

  6. 实用的树形菜单控件tree

     jQuery plugin: Treeview  这个插件能够把无序列表转换成可展开与收缩的Tree. jQuery plugin: Treeview  jQuery  jstree  jsTree ...

  7. html树形菜单控件

    html树形菜单控件  链接 http://www.ithao123.cn/content-713974.html         jQuery plugin: Treeview  这个插件能够把无序 ...

  8. 【转】html树形菜单控件

    Query plugin: Treeview  这个插件能够把无序列表转换成可展开与收缩的Tree. 主页:http://bassistance.de/jQuery-plugins/jquery-pl ...

  9. ERP存储过程的调用和树形菜单的加载(四)

    引用:DAL:System.Data.SqlClient;System.Data; namespace CommTool { public class SqlComm { /// <summar ...

随机推荐

  1. [luoguP2680] 运输计划(lca + 二分 + 差分)

    传送门 暴力做法 50 ~ 60 枚举删边,求最大路径长度的最小值. 其中最大路径长度运用到了lca 我们发现,求lca的过程已经不能优化了,那么看看枚举删边的过程能不能优化. 先把边按照权值排序,然 ...

  2. 夜话JAVA设计模式之适配器模式(adapter pattern)

    适配器模式:将一个类的接口,转换成客户期望的另一个接口,让不兼容的接口变成兼容. 1.类适配器模式:通过多重继承来实现适配器功能.多重继承就是先继承要转换的实现类,再实现被转换的接口. 2.对象适配器 ...

  3. 上帝说:要约炮!于是有了XMPP

    一.导入XMPP框架 下载 XMPPFramework 框架 GitHub: XMPPFramework 导入依赖框架 CocoaLumberjack : 日志框架 CocoaAsyncSocket  ...

  4. ubuntu 安装 swift

    第一步 安装mysql和mysql的python支持 apt-get install python-mysqldb mysql-server 第二步 配置mysql vim /etc/mysql/my ...

  5. python基础学习之02 元组

    #encoding=utf-8 # 元组与列表一样,也是一种序列 print (1,2,3) print 1,2,3 print(1,) print 1, a=1 print(type(a)) a=1 ...

  6. 喜欢玩warcraft的ltl

    喜欢玩warcraft的ltl 时间限制:2000 ms  |  内存限制:65535 KB 难度:3 描写叙述 ltl 很喜欢玩warcraft.由于warcraft十分讲究团队总体实力,而他自己如 ...

  7. Java字符编码的转化问题

    概述: 我想字符串的编码问题的确会困扰到非常多开发人员.我近期也是被困扰到了. 问题是这种,我们通过二维码扫描来获得二维码中的信息.可是.我们的二维码的产生过程却是"多样化"的.即 ...

  8. 【Android实战】记录自学自己定义GifView过程,能同一时候支持gif和其它图片!【有用篇】

    之前写了一篇博客.<[Android实战]记录自学自己定义GifView过程,具体解释属性那些事! [学习篇]> 关于自己定义GifView的,具体解说了学习过程及遇到的一些类的解释,然后 ...

  9. nmap,port扫描,获取sshserver的ip地址

    // 查看局域网的ip地址 arp - a // 同一个网段.假设用虚拟机桥接则不行 sudo nmap -sS 192.168.1.* //或者sudo nmap -sS -p 22 192.168 ...

  10. JavaScript的原生引用类型

    引用类型是一种数据结构,用于将数据和功能组织在一起,也常称做类.ECMAScript从技术上说是一门面向对象的语言.但它不具备传统的面向对象语言所支持的类和接口等基本结构. Object类型 大多数引 ...