前言
- 在游戏和app中,为了提示用户最新的消息,往往会在按钮的左上角添加红点
- 本文教大家,从零开始,使用unity实现红点系统
显示逻辑
- 父节点的红点数量等于其子节点数量之和
- 只要存在一个子节点,父节点就显示,父节点下所有子节点消失,父节点才消失
红点树节点RedDotNode
每个节点
public class RedDotNode
{
public int value = 0;//红点的数量,为所有子节点红点数量的和
public bool isShow => value != 0;//数量不为0,显示红点,为0,不显示红点
public List<RedDotNode> children = new List<RedDotNode>();//子节点
public RedDotNode parent;//父节点
public string name;//节点名字
}
加上是否是叶节点isLeaf;
节点对应的t(Transform);
对应的t下的redDot(ui中的红点图片节点);
num,t下的redDot下的num,ui中的红点数量;
public class RedDotNode
{
public int value = 0;//红点的数量,为所有子节点红点数量的和
public bool isShow => value != 0;//数量不为0,显示红点,为0,不显示红点
public bool isLeaf => children.Count == 0;//是否是叶子节点
public List<RedDotNode> children = new List<RedDotNode>();//子节点
public RedDotNode parent;//父节点
public string name;//节点名字
public Transform t;//对应的Transform
public Transform redDot;//对应的t下的redDot,ui中的红点图片
public Text num;//对应的对应的t下的redDot下的num,ui中的红点数量
}
RedDotNode下的方法
创建,添加
//创建
public RedDotNode(string name)
{
this.name = name;
}
/// <summary>
/// 添加子节点
/// </summary>
/// <param name="name">子节点名字</param>
/// <param name="t">子节点的Transform</param>
/// <returns></returns>
public bool Add(string name, Transform t)
{
//遍历该节点下所有子节点,如果存在,无法添加
//后面根据名字搜索,要保证同一目录下的子节点名字唯一
foreach (RedDotNode child in children)
{
if (child.name.Equals(name))
return false;
}
RedDotNode childNode = new RedDotNode(name);//创建子节点
childNode.parent = this;//设置子节点的父亲
childNode.t = t;//设置子节点的子节点的Transform
childNode.redDot = t.Find("redDot");//设置子节点的子节点的redDot Transform
childNode.num = t.Find("redDot").GetComponentInChildren<Text>();//设置子节点的子节点的num Transform
children.Add(childNode);//将子节点添加到子节点数组中
return true;
}
搜索
/// <summary>
/// 寻找该节点下,有没有名称为name的子节点
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public RedDotNode Find(string name)
{
if (children.Count == 0) return null;
foreach (RedDotNode child in children)//遍历子节点
{
if (child.name.Equals(name))
return child;
};
return null;
}
删除
/// <summary>
/// 移除该节点下指定名称的子节点
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public bool Remove(string name)
{
RedDotNode needDeleteNode = Find(name);//寻找子节点
if (needDeleteNode != null)
{
needDeleteNode.parent = null;//数组父节点为null
children.Remove(needDeleteNode);//移除子节点
return true;
}
else
{
return false;
}
}
其它方法
- 刷新
public void Refresh()
{
redDot.gameObject.SetActive(isShow);
num.text = value.ToString();
}
- 获取该节点下所有的子节点
如果要强行删除一个父节点,获取所有子节点隐藏
public List<RedDotNode> GetAllChilden(RedDotNode node, List<RedDotNode> childenNodes)
{
for (int i = 0; i < node.children.Count; i++)
{
childenNodes.Add(node.children[i]);
GetAllChilden(node.children[i], childenNodes);//深度搜索子节点
}
return childenNodes;
}
- 刷新子节点上所有父节点
两种更新方式,父节点的值等于所有叶节点的和
1.父节点的值等于所有子节点的和,先更新子节点,再往上依次更新父节点
2.父节点的值等于所有叶节点的和,更新父节点,再往下依次更新子节点
1的效果要明显优于2,介绍2为了拓展思维
//父节点的值等于所有子节点的和
//在ShowRedDot时,调用
public void Update()
{
List<RedDotNode> childs = this.children;
int parentValue = 0;
foreach (RedDotNode child in childs)
{
parentValue += child.value;
}
this.value = parentValue;
}
//父节点的值等于所有叶节点的和
//在ShowRedDot时,调用
public void Update2()
{
List<RedDotNode> childs = GetLeaves(this, new List<RedDotNode>());
int parentValue = 0;
foreach (RedDotNode child in childs)
{
parentValue += child.value;
}
this.value = parentValue;
}
//获取父节点下所有的叶节点
public List<RedDotNode> GetLeaves(RedDotNode node, List<RedDotNode> childenNodes)
{
if (node.children.Count == 0)
{
childenNodes.Add(node);
return childenNodes;
}
for (int i = 0; i < node.children.Count; i++)
{
GetLeaves(node.children[i], childenNodes);
}
return childenNodes;
}
红点树RedDotTree
添加
/// <summary>
/// 添加子节点到节点树上,子节点的父节点必须先存在
/// </summary>
/// <param name="fullName"></param>
/// <param name="t"></param>
/// <returns></returns>
public bool AddNode(string fullName, Transform t)
{
string[] names = fullName.Split('|');//分割UI/Canvas/Main/MainPanel
RedDotNode cur = root;
//遍历从0- names.Length - 2,从分割UI到Main
for (int i = 0; i < names.Length - 1; i++)
{
cur = cur.Find(names[i]);
if (cur == null)//节点不存在,直接返回
{
return false;
}
}
//cur为Main,names.Length - 1应为不存在,要添加的
dict.Add(t, fullName);//记录Transform->UI/Canvas/Main/MainPanel的映射
cur.Add(names[names.Length - 1], t);//将MainPanel添加到Main中
return true;
}
显示指定名称节点
public void ShowRedDot(string fullName)
{
RedDotNode node = SearchNode(fullName);
string[] names = fullName.Split('|');
if (node == null) return;//找不到节点返回
RedDotNode cur = root;
if (node.isLeaf)//是叶节点
{
//叶节点的值为0,更新所有的父节点,为1说明叶节点没有改变,父节点不用修改
bool parentChange = node.value == 0;
if (parentChange)
{
node.value = 1;//叶节点值为1
node.Refresh();//值更改了,ui上的显示和数量要更新
//更新所有的父节点,0-names.Length - 2
#region 第1种更新方式
cur = node;//UI/Canvas/Main/MainPanel
for (int i = names.Length - 2; i >= 0; i--)
{
cur = cur.parent;
//cur = cur.Find(names[i]);
cur.Update();//更新父节点
//不能cur.value+=1,因为先加入A,A.value=1,再加入A/a,A.value=2
//父节点的值应该是它所有叶节点值的和
cur.Refresh();//刷新父节点
}
#endregion 第1种更新方式
/* 第2种更新方式
cur = root;
for (int i = 0; i >= names.Length - 2; i++)
{
cur = cur.Find(names[i]);
cur.Update2(cur);//更新父节点
//不能cur.value+=1,因为先加入A,A.value=1,再加入A/a,A.value=2
//父节点的值应该是它所有叶节点值的和
cur.Refresh();//刷新父节点
}
*/
}
}
}
添加并显示节点
//和AddNode一样,多了ShowRedDot(fullName);
public RedDotTree AddAndShowNode(string fullName, Transform t)
{
bool rs=AddNode(fullName, t);
if (!rs) return null;
ShowRedDot(fullName);
nowNode = fullName;//记录加入节点的路径
return this;
}
添加并显示节点2
在之前添加节点下,添加节点,只传入节点名字
public void AddAndShowSubNode(string name, Transform t)
{
string tmp = nowNode;
if (nowNode == null)
{
Debug.LogError("nowNode is null");
}
AddAndShowNode(nowNode + "|" + name, t);
//AddAndShowNode,更改了nowNode节点
//希望AddAndShowSubNode在AddNode之前节点上添加
nowNode = tmp;
}
在节点树下搜索节点
/// <summary>
/// 在节点树中搜索节点
/// </summary>
/// <param name="fullName"></param>
/// <returns></returns>
public RedDotNode SearchNode(string fullName)
{
//分割UI/Canvas/Main/MainPanel
string[] names = fullName.Split('|');
RedDotNode cur = root;
//搜索从0- names.Length - 1,从UI->MainPanel
for (int i = 0; i < names.Length; i++)
{
cur = cur.Find(names[i]);
if (cur == null)
return null;
}
return cur;
}
从节点树上删除节点
/// 从节点树上删除节点
/// </summary>
/// <param name="fullName"></param>
/// <returns></returns>
public bool RemoveNode(string fullName)
{
string[] names = fullName.Split('|');
RedDotNode cur = root;
RedDotNode node = SearchNode(fullName);
if (node == null)
{
return false;
}
node.parent.Remove(node.name);
return true;
}
隐藏节点
isForcedHideParent,是否强制隐藏父节点及下面所有子节点
/// <summary>
/// 通过传入fullName,隐藏ui节点
/// </summary>
/// <param name="t"></param>
/// 逻辑和ShowRedDot类似
public void HideRedDot(string fullName, bool isForcedHideParent = false)
{
RedDotNode node = SearchNode(fullName);
string[] names = fullName.Split('|');
if (node == null) return;//找不到节点返回
RedDotNode cur = root;
if (node.isLeaf)//叶子节点
{
bool parentChange = node.value == 1;
if (parentChange)
{
node.value = 0;//叶节点为0
node.Refresh();
//更新所有的父节点,0-names.Length - 2
for (int i = 0; i < names.Length - 1; i++)
{
cur = cur.Find(names[i]);
cur.value -= 1;
cur.Refresh();
}
}
}
else if (isForcedHideParent)//不是叶子节点,要强制隐藏父节点及其下面的子节点
{//UI/Canvas/Main/MainPanel
//node=UI/Canvas/Main
for (int i = 0; i < names.Length - 1; i++)//遍历上面的节点
{
cur = cur.Find(names[i]);
cur.value -= node.value;
cur.Refresh();
}
//获取并隐藏所有的子节点
List<RedDotNode> ChildenNodes = node.GetAllChilden(node, new List<RedDotNode>());
foreach (RedDotNode child in ChildenNodes)
{
child.value = 0;
child.Refresh();
}
node.value = 0;//父节点为0
node.Refresh();//刷新父节点
}
else
{
Debug.LogWarning($"{fullName}不是叶节点,如果要强行隐藏,考虑传入closeParentNode=true");
}
}
通过传入Transform,显示隐藏ui节点
/// <summary>
/// 通过传入Transform,显示ui节点
/// </summary>
/// <param name="t"></param>
public void ShowRedDot(Transform t)
{
if (dict.ContainsKey(t))
ShowRedDot(dict[t]);
}
/// <summary>
/// 通过传入Transform,隐藏ui节点
/// </summary>
/// <param name="t"></param>
public void HideRedDot(Transform t)
{
if (dict.ContainsKey(t))
HideRedDot(dict[t]);
}
打印红点树
广度优先遍历红点树
public void PrintTree()
{
StringBuilder sb = new StringBuilder();
if (root == null)
{
Debug.LogError("RedDotTree root is null");
return;
}
printTree(root,sb);
level = 0;
Debug.Log(sb.ToString());
}
private int level = 0;//记录遍历的层级
string printTree(RedDotNode node,StringBuilder sb)
{
if (node == null)
return sb.ToString();
List<RedDotNode> children = node.children;
foreach (RedDotNode child in children)
{//遍历一个节点的所有节点
for (int i = 0; i < level; i++)
{
sb.Append("-");
}
sb.Append(child.name+"\n");
}
level++;//遍历完成子节点,循环递归遍历节点
foreach (RedDotNode child in children)
{
printTree(child,sb);
}
return sb.ToString();
}
使用
void TestRedDot()
{
RedDotTree redDotTree = new RedDotTree();
redDotTree.AddAndShowNode("Task", GameObject.Find("UILayer/UICanvas/Main/MainPanel").transform);
redDotTree.AddAndShowNode("Task|task", GameObject.Find("UILayer/UICanvas/Main/MainPanel/Panel").transform);
redDotTree.AddAndShowSubNode("task_1", GameObject.Find("UILayer/UICanvas/Main/MainPanel/Panel/Sub1").transform);
//AddAndShowSubNode,在刚刚添加的Task|task下添加task_1
redDotTree.AddAndShowSubNode("task_2", GameObject.Find("UILayer/UICanvas/Main/MainPanel/Panel/Sub2").transform);
AddAndShowSubNode,在刚刚添加的Task|task下添加task_2
redDotTree.AddAndShowNode("Task|task1", GameObject.Find("UILayer/UICanvas/Main/MainPanel/Panel1").transform);
redDotTree.HideRedDot("Task|task|task_2");
redDotTree.PrintTree();
}
打印结果
Task
-task
-task1
–task_1
–task_2
结束语
红点系统,根据大家项目的需求,制定修改代码
如果有不懂的地方或者建议,欢迎在评论区交流
制作不易,希望大家支持一下,谢谢~~😊