標籤:anti 資料 1.2 res ddc ini hid 遞迴迴圈 迴圈
先上幾張:
如果你需要的也是這種效果,那你就來對地方了!
目前,我們這個樹形菜單展現出來的功能如下:
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]); }
如下:
外掛程式連結:已經上傳,等審核過了貼連結
Unity UGUI自訂樹形菜單(TreeView)