Unity 自定义TreeView UI 支持自定义JSON数据的自动解析生成

Unity 自定义TreeView UI 支持自定义JSON数据的自动解析生成

前言

最近一个项目需要实现对一个产品结构的展示,该产品有多级结构,且需要对结构信息进行配置,结合网上一些资料,自己写了个TreeView的组件,采用双向链表结构来存储产品的结构数据,并且支持每一产品结构节点信息的存储,支持与产品模型的Animation、拖拽控制、零组件信息显示等等需求进行配合。仅使用UGUI自带的组件进行实现。

效果图

TreeView 支持多级结构展示。
展开效果:
请添加图片描述
折叠效果:
请添加图片描述

TreeViewItem Prefab设计

TreeViewItem视作一个节点,包括子物体Content和Children,分别表示该节点的信息和子节点集合,如下图:
请添加图片描述
请添加图片描述
UI风格可以自定义,可以增加toggle之类的组件,我这里设置了节点名称的Text、节点层级的标识Image和折叠Button。
注意:Content和Children的Anchor应设置为top-center,这样方便子节点开展时的向下扩展,同时Children应添加Vertical Layout Group和RectMask2D组件,方便对子节点进行排列和隐藏。如下图
请添加图片描述
同时为该预制体添加自定义的TreeViewNode脚本,并将相应参数拖拽赋值,这里可以自定义节点等级标识的颜色。脚本代码如下:.

public class TreeViewNode : MonoBehaviour
{
    [SerializeField]
    [Header("折叠按钮")]
    private Button BtnArrow;
    [SerializeField]
    [Header("节点按钮")]
    private Button BtnElement;   
    [SerializeField]
    [Header("节点等级Tf")]
    private Transform LevelTf;

    [SerializeField]
    [Header("子节点Tf")]
    public Transform ChildrenTf;

    [SerializeField]
    [Header("标识颜色")]
    public Color[] LevelColors;

    /// <summary>
    /// 该节点的子节点列表
    /// </summary>
    [HideInInspector]
    public List<TreeViewNode> Children;
    /// <summary>
    /// 该节点的父节点
    /// </summary>
    [HideInInspector]
    public TreeViewNode Parent;

    /// <summary>
    /// 设置的节点高度 当前:51
    /// </summary>
    private float nodeHeight = 51f;
    /// <summary>
    /// 获取节点高度
    /// </summary>
    public float NodeHeight { get { return nodeHeight; } }

    /// <summary>
    /// 是否为根节点
    /// </summary>
    private bool isRoot;
    /// <summary>
    /// 是否有子节点
    /// </summary>
    private bool hasChild;
    /// <summary>
    /// 该节点是否处于折叠状态
    /// </summary>
    private bool isFold;

    private PartInfo partInfo;
    /// <summary>
    /// 该节点的信息对象
    /// </summary>
    public PartInfo PartInfo { get { return partInfo; } }
    /// <summary>
    /// 父节点序号存储
    /// </summary>
    private Dictionary<NodeType,int> parentsIndex;

    private void Awake()
    {
        nodeHeight = Mathf.Floor(transform.GetComponent<RectTransform>().sizeDelta.y);
        //初始状态默认展开
        isFold = false;
    }
   
    private void Start()
    {
        BtnElement.onClick.AddListener(OnBtnElementClick);
        BtnArrow.onClick.AddListener(FoldElementNode);        
        //延迟.1s 执行
        DOVirtual.DelayedCall(.1f, SetInteraction);
    }

    private void OnBtnElementClick()
    {
        AnimeData animeData = new AnimeData
        {
            PartName = partInfo.PartName,
            Type = partInfo.NodeType,
            Index = partInfo.Index,
            ParentIndex = parentsIndex,
        };
        EventCenter.ModelEvent.TriggerModelAnime(animeData);
        EventCenter.UIEvent.UpdatePartInfo(partInfo);
    }

    /// <summary>
    /// 展开/折叠 该节点的子节点
    /// </summary>
    private void FoldElementNode()
    {
        BtnArrow.interactable = false;

        if (!hasChild) return;
        float start = 0f;
        float target = 0f;
        if (isFold)
        {
            target = CalChildrenHeight();
            BtnArrow.transform.DOLocalRotate(new Vector3(0, 0, -90f), .3f);
        }
        else
        {
            start = CalChildrenHeight();
            BtnArrow.transform.DOLocalRotate(Vector3.zero, .3f);
        }
        DOTween.To(() => start, x => {
            transform.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, x + nodeHeight);
            ChildrenTf.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, x);
            if (!isRoot) Parent.ModifyNodeHeight();
        },target,.3f).OnComplete(()=>
        {
            BtnArrow.interactable = true;
            isFold = !isFold;
            EventCenter.UIEvent.ChangeTreeViewHeight();
        });        
    }

    /// <summary>
    /// 添加UI子节点
    /// </summary>
    /// <param name="child"></param>
    public void AddChild(TreeViewNode child)
    {
        if (Children == null)
        {
            Children = new List<TreeViewNode>();
        }
        Children.Add(child);
        hasChild = true;
    }
    /// <summary>
    /// 计算该节点Children元素的高度(所有子一级节点的高度和)
    /// </summary>
    /// <returns></returns>
    public float CalChildrenHeight()
    {
        if (Children == null) return nodeHeight;
        float height = 0f;
        foreach (TreeViewNode node in Children)
        {
            height += node.GetComponent<RectTransform>().sizeDelta.y;
        }
        return height;
    }
    /// <summary>
    /// 更新本节点的高度 和Children元素的高度 (递归)
    /// </summary>
    /// <param name="modify">修改量</param>
    public void ModifyNodeHeight()
    {
        float temp = CalChildrenHeight();
        transform.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, nodeHeight + temp);
        ChildrenTf.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, temp);
        if (Parent != null) Parent.ModifyNodeHeight();
    }
    /// <summary>
    /// 初始化-节点信息
    /// </summary>
    /// <param name="name">节点名称</param>
    /// <param name="nodeType">节点类型</param>
    /// <param name="root">是否为根节点</param>
    public void InitNodeSet(PartInfo partInfo,bool root = false)
    {
        BtnElement.GetComponentInChildren<Text>().text = partInfo.PartName;
        this.partInfo = partInfo;
        GetNodeParentsIndexList();
        float leftPadding = 20f * (int)partInfo.NodeType;
        BtnElement.transform.GetComponentInRealChildren<RectTransform>().offsetMin = new Vector2(10f + leftPadding, 0f);
        BtnArrow.transform.localEulerAngles = new Vector3(0, 0, -90f);
        if (partInfo.NodeType == NodeType.Product) LevelTf.gameObject.SetActive(false);
        else
        {
            LevelTf.gameObject.SetActive(true);
            LevelTf.GetComponent<Image>().color = LevelColors[(int)partInfo.NodeType - 1];
            LevelTf.GetComponentInRealChildren<Text>().text = ((int)partInfo.NodeType).ToString();
        }
        isRoot = root;
    }

    /// <summary>
    /// 初始化-递归设置所有节点中transform和Children的高度
    /// </summary>
    /// <returns></returns>
    public float SetAllNodeHeight()
    {
        if (Children == null || Children.Count == 0) return nodeHeight;
        float temp = 0;
        foreach (TreeViewNode item in Children)
        {
            temp += item.SetAllNodeHeight();
        }
        ChildrenTf.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, temp);
        transform.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, temp + nodeHeight);
        return temp + nodeHeight;
    }
   
    /// <summary>
    /// 设置是否需要折叠按钮 (延迟执行)
    /// </summary>
    private void SetInteraction()
    {
        if(Children == null || Children.Count == 0)
        {
            BtnArrow.gameObject.SetActive(false);
        }
    }
    /// <summary>
    /// 设置父节点(父父节点...)的序号列表
    /// </summary>
    private void GetNodeParentsIndexList()
    {
        parentsIndex = new Dictionary<NodeType, int>();
        if (partInfo.NodeType == NodeType.Product) return;
        TreeViewNode temp = Parent;
        while(temp.PartInfo.NodeType > 0)
        {
            parentsIndex.Add(temp.PartInfo.NodeType, temp.PartInfo.Index);
            temp = temp.Parent;
        }
    }   
}

TreeView 控制

首先定义节点的数据对象格式,以便于JSON数据解析,如下:

//节点类型枚举
public enum NodeType
{
    Product = 0,
    First = 1,
    Second = 2,
    Third = 3,
    Fourth = 4
}
//产品节点数据对象
public class ProductNode
{
    public string Name;
    public NodeType Type;
    public int Index;
    public Dictionary<string,string> ParaInfo;
    public string Desc;
    public List<ProductNode> Children;  

    public void AddChild(ProductNode child)
    {
        if (Children == null)
        {
            Children = new List<ProductNode>();
        }
        Children.Add(child);
    }
}

其次,需定义一个与产品数据类型一致的JSON文件,示例如下

{  
  "Name": "xx产品",  
  "Type": "Product",  
  "Index": 0,  
  "ParaInfo": {  
    "长度": "xx m",  
    "直径": "xx m"
  },  
  "Desc": "产品描述",  
  "Children": [  
    {  
      "Name": "组件一",  
      "Type": "First",  
      "Index": 1,  
      "Desc": "组件一描述",  
      "Children": [  
        {  
          "Name": "零件一",  
          "Type": "Second",  
          "Desc": "零件一描述"  
        },  
        {  
          "Name": "零件二",  
          "Type": "Second",  
          "Desc": "零件二描述"  
        }  
      ]  
    },  
    {  
      "Name": "组件二",  
      "Type": "First",  
      "Index": 2,  
      "Desc": "组件二描述",  
      "Children": [  
        {  
          "Name": "零件三",  
          "Type": "Second",  
          "Desc": "零件三描述"  
        }  
      ]  
    },  
    {  
      "Name": "组件三",  
      "Type": "First",  
      "Index": 3,  
      "Desc": "组件三描述",  
      "Children": [  
        {  
          "Name": "零件四",  
          "Type": "Second",  
          "Desc": "零件四描述"  
        },  
        {  
          "Name": "零件五",  
          "Type": "Second",  
          "Desc": "零件五描述"  
        }  
      ]  
    }
  ]  
}

准备工作完成后,开始写TreeView的控制逻辑,先根据json得到一个ProductNode根对象,其包括了产品的所有数据信息。其次根据ProductNode对象逐级生成TreeViewItem实例对象,并进行节点数据初始化,逻辑代码如下:

public class TreeViewController : SingletonMonoBase<TreeViewController>
{
    /// <summary>
    /// 根节点
    /// </summary>
    private TreeViewNode rootScript;

    private void Start()
    {
        EventCenter.UIEvent.ChangeTreeViewHeightAction += ChangeTreeViewHeight;
        CreateTreeView();
        ModelControl.Instance.InitBillAfterCreateRoot(rootScript);
    }

    private void OnDestroy()
    {
        EventCenter.UIEvent.ChangeTreeViewHeightAction -= ChangeTreeViewHeight;
    }

    private void ChangeTreeViewHeight()
    {
        transform.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, GetRootHeight());
        transform.parent.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, GetRootHeight());
    }

    /// <summary>
    /// 结构树根节点数据
    /// </summary>
    private ProductNode rootData;
    private void CreateTreeView()
    {
        rootData = ParseJson();
        GameObject nodeGo = GameObject.Instantiate(Resources.Load("Prefabs/UI/TreeViewItem")) as GameObject;
        nodeGo.transform.SetParent(transform, false);
        TreeViewNode rootUINode = nodeGo.GetComponent<TreeViewNode>();
        this.rootScript = rootUINode;
        rootUINode.InitNodeSet(new PartInfo { PartName = rootData.Name, NodeType = rootData.Type, Index = rootData.Index, ParaInfo = rootData.ParaInfo, Desc = rootData.Desc }, true);
        CreateTreeNodeUI(rootData, rootUINode);
        rootUINode.SetAllNodeHeight();
        ChangeTreeViewHeight();
        EventCenter.UIEvent.UpdatePartInfo(rootUINode.PartInfo);
    }

    /// <summary>
    /// 生成除根节点外的所有结构树节点UI
    /// </summary>
    /// <param name="dataNode"></param>
    /// <param name="uiNode"></param>
    private void CreateTreeNodeUI(ProductNode dataNode, TreeViewNode uiNode)
    {
        if (dataNode.Children != null)
        {
            foreach (ProductNode child in dataNode.Children)
            {
                GameObject nodeGo = GameObject.Instantiate(Resources.Load("Prefabs/UI/TreeViewItem")) as GameObject;
                nodeGo.transform.SetParent(uiNode.ChildrenTf, false);
                TreeViewNode childUINode = nodeGo.GetComponent<TreeViewNode>();
                //构建双向链
                uiNode.AddChild(childUINode);
                childUINode.Parent = uiNode;
                //初始化节点
                childUINode.InitNodeSet(new PartInfo { PartName=child.Name,NodeType = child.Type, Index = child.Index, ParaInfo = child.ParaInfo,Desc = child.Desc});
                //递归到子级
                CreateTreeNodeUI(child, childUINode);
            }
        }
    }

    /// <summary>
    /// 解析Json生成ProductNode根节点数据
    /// </summary>
    /// <returns></returns>
    private ProductNode ParseJson()
    {
        //node为产品信息树根节点
        TextAsset ta = Resources.Load<TextAsset>("Json/ProductInfo");
        ProductNode node = JsonConvert.DeserializeObject<ProductNode>(ta.text);
        return node;
    }


    /// <summary>
    /// 通过零件模型名称获取父级(到根据节点)物体名称组
    /// </summary>
    /// <param name="partName"></param>
    /// <returns></returns>
    public List<string> GetParentInfoByPartName(string partName)
    {
        return FindParentNames(rootScript, partName);
    }

    /// <summary>
    /// 获取该节点的所有父节点
    /// </summary>
    /// <param name="currentNode"></param>
    /// <param name="targetNodeName"></param>
    /// <returns></returns>
    public List<string> FindParentNames(TreeViewNode currentNode, string targetNodeName)
    {
        var parentNames = new List<string>();
        // 如果当前节点为空 
        if (currentNode == null)
        {
            return parentNames;
        }
        // 如果当前节点的名称与目标节点名称匹配,则开始收集父节点名称  
        if (currentNode.PartInfo.PartName == targetNodeName)
        {
            parentNames.Add(targetNodeName);
            while (currentNode.Parent != null)
            {
                parentNames.Add(currentNode.Parent.PartInfo.PartName); // 添加父节点名称到列表中  
                currentNode = currentNode.Parent; // 移动到上一级父节点  
            }
            // 反转列表,以便它按从根节点到自身的顺序排列  
            parentNames.Reverse();
            return parentNames;
        }
        // 如果当前节点不是目标节点,则对其子节点进行递归搜索  
        foreach (var child in currentNode.Children)
        {
            var result = FindParentNames(child, targetNodeName);
            if (result.Count > 0) // 如果在子树中找到了目标节点,则返回结果  
            {
                return result;
            }
        }
        // 如果在所有子节点中都没有找到目标节点,则返回空列表  
        return parentNames;
    }

    /// <summary>
    /// 更新零件信息的显示panel
    /// </summary>
    /// <param name="partName"></param>
    public void UpdatePartInfoByName(string partName)
    {
        EventCenter.UIEvent.UpdatePartInfo(GetPartNodeByName(partName).PartInfo);
    }


    /// <summary>
    /// 根据零件名称获取零件节点  没有返回 null
    /// </summary>
    /// <param name="partName"></param>
    /// <returns></returns>
    public TreeViewNode GetPartNodeByName(string partName)
    {
        return GetPartTreeNode(rootScript, partName);
    }

    /// <summary>
    /// 递归寻找节点
    /// </summary>
    /// <param name="node"></param>
    /// <param name="partName"></param>
    /// <returns></returns>
    private TreeViewNode GetPartTreeNode(TreeViewNode node, string partName)
    {
        if (node.PartInfo.PartName == partName) return node;
        if (node.Children == null ||  node.Children.Count == 0) return null;
        foreach (var child in node.Children)
        {
            var res = GetPartTreeNode(child, partName);
            if (res != null) return res;
        }
        return null;
    }

    /// <summary>
    /// 获取整个tree的高度
    /// </summary>
    /// <returns></returns>
    private float GetRootHeight()
    {
        return rootScript.NodeHeight + rootScript.ChildrenTf.GetComponent<RectTransform>().sizeDelta.y;
    }
}

这里将该脚本作为一个单例(SingletonMonoBase),方便其他地方直接调用内部的方法,如何写继承于MonoBehaviour的单例,网上有很多案例。这里有点绕的就是关于折叠时的节点高度变化了,因为预制体中的物体都设置了Anchor,所以其高度调整需要用GetComponent().SetSizeWithCurrentAnchors() 方法来进行设置。折叠的UI动作用到了Dotween插件,很常用的一个插件,不多说了。

扩展

控制脚本中写了个GetPartNodeByName()方法,可以根据零件名称获取到对应的零件节点的TreeViewNode。如果你的产品模型中的名称与产品Json中一致且唯一的话,就可以打通模型与数据的关联通道了,比如点击TreeView中的某个节点,自动定位到对应的零件,并且播放相应的Animation,同时显示该零件的参数信息等。

  • 23
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值