先上几张效果图:

        

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

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

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. cookie & cookies

    cookie & cookies "use strict"; /** * * @author xgqfrms * @license MIT * @copyright xgq ...

  2. ZOJ 1654 Place the Robots

    题目大意: 在空地上放置尽可能多机器人,机器人朝上下左右4个方向发射子弹,子弹能穿过草地,但不能穿过墙, 两个机器人之间的子弹要保证互不干扰,求所能放置的机器人的最大个数 每个机器人所在的位置确定了, ...

  3. HDU 1254 条件过程复杂的寻找最短路

    这里一看就是找箱子到终点的最短路 一开始还傻傻的以为人的位置给的很没有意思- -,然后果然错了 没过多久想明白了错误,因为你推箱子并不是你想去哪里推就能去哪推的,首先得考虑人能否过的去,因为可能人被箱 ...

  4. [bzoj 1059][ZJOI 2007]矩阵游戏(二分图最大匹配)

    题目:http://www.lydsy.com/JudgeOnline/problem.php?id=1059 分析:不论如何交换,同一行或同一列的点还是同一行或同一列,如果我们称最后可以排成题目要求 ...

  5. 表单中的日期 字符串和Javabean中的日期类型的属性自动转换

    搞了一上午的bug最终还是因为自己springMVC的注解不熟悉的原因,特记录. 在实际操作中经常会碰到表单中的日期 字符串和Javabean中的日期类型的属性自动转换, 而springMVC默认不支 ...

  6. 为什么 Android Studio 工程文件夹占用空间这么大?

    为什么 Android Studio 工程文件夹占用空间这么大? 学习了: https://www.cnblogs.com/chengyujia/p/5791002.html

  7. 推荐美丽的flash网页MP3音乐播放器

    文章来源:PHP开发学习门户 地址:http://www.phpthinking.com/archives/491 在网页制作中.假设想在网页中插入mp3音乐来增添网页的互动感,提升用户体验度,这个时 ...

  8. 安装ubuntu远程桌面xrdp可视化设置界面

    ubuntu 远程桌面的时候须要从系统-首选项-远程桌面 可是有的ubuntu远程桌面的应用须要自己安装.例如以下是安装命令: sudo apt-get install xrdp

  9. cocos2dx 编译时间长问题

    { F:\cocos2dx\cocos2d-x-3.7.1\templates\cpp-template-default 彻底解决方式 为把cocos的模版项目编译好(详细是所有生成好并清理Hello ...

  10. luogu3379 【模板】最近公共祖先(LCA) Tarjan

    LCA的Tarjan算法是一个离线算法,复杂度$O(n+q)$. 我们知道Dfs搜索树时会形成一个搜索栈.搜索栈顶节点cur时,对于另外一个节点v,它们的LCA便是v到根节点的路径与搜索栈开始分叉的那 ...