先上幾張效果圖:
如果你需要的也是這種效果,那你就來對地方了!
目前,我們這個樹形菜單展現出來的功能如下:
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; } } }我這里將所有的元素復制了一份用于計算位置,主要就是為了防止在進行一輪刷新時某個元素被訪問兩次或以上,因為刷新的時候會遍歷所有可見元素,如果第一次訪問了元素A(元素A的位置被刷新),根據元素A的子元素列表訪問到了元素B(元素B的位置被刷新),一直到達子元素的底部后,當不存在更深層次的子元素時,那么返回到元素A之后的元素繼續訪問,這時在所有元素列表中元素B可能在元素A之后,也就是說元素B已經通過父元素訪問過了,不需要做再次訪問,他的位置已經是最新的了,而之后根據列表索引很可能再次訪問到元素B,如果是這樣的話元素B的位置又要被刷新一次,甚至多次,性能影響不說,第二次計算的位置已經不是正確的位置了(總之也就是一個計算邏輯的問題,沒看明白可以直接忽略)。四、菜單已經創建完畢并且經過了一輪刷新,此時它展示出來的就是這樣一個所有子元素都展開的形狀(我在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); } }IsExpanding做為每個元素的字段用于設置或讀取自身子元素的顯示狀態,這里根據改變的狀態會遞歸循環此元素的所有子元素及孫元素,讓他們可見或隱藏。五、對所有的子元素進行統一的事件分發,這里主要就有鼠標點擊這一個事件:
每個元素都會注冊這個事件:(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; }七、接下來是我們的數據格式TreeViewData,樹形菜單的數據源是由這個格式組成的集合:/// <summary> /// 當前樹形菜單的數據源 /// </summary> [HideInInspector] public List<TreeViewData> Data = null;每一個TreeViewData代表一個元素,Name為顯示的文本內容,ParentID為它指向的父元素在整個數據集合中的索引,從0開始,-1代表不存在父元素的根元素,當然有時候數據源并不是這個樣子的,可能是xml,可能是json,不過都可以通過解析數據源之后再變換成這種方式:/// <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打包成了一個插件,在Unity中導入他,便可以直接使用TreeView:導入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();然后注冊子元素的鼠標點擊事件(委托類型為返回值void,帶一個Gameobject類型參數,參數item為被鼠標點中的那個元素的gameobject)://注冊子元素的鼠標點擊事件 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!
新聞熱點
疑難解答