一个简简单单的红点系统框架
前言
今天我们简简单单做一个红点系统框架。在应用和游戏中,按钮上的红点非常常见。如图所示:
红点会让强迫症烦躁不安,但又不可或缺。这里分享一个自用的红点系统框架。
转载请注明出处:https://www.cnblogs.com/GuyaWeiren/p/15259108.html
设计思路
每一个需要显示红点的地方,都视作一个节点。由于界面存在嵌套关系,所以节点可以包含N个子节点,如下图所示在运行时红点应当是一个树结构:
为了简化逻辑,所有红点都视作数字型红点。至于UI上到底要显示成数字,还是一个点,还是别的情况,可以在刷新的回调中单独处理。
如果某节点没有任何子节点,则该节点的计数只可能是0或1。因为如果某个按钮上显示一个2的红点,但点开之后界面中没有任何红点,看起来会很奇怪。
如果某节点包含至少一个子节点,则该节点的计数为所有子节点的计数和。
每一个节点使用一个字符串作为标识,可以通过文件路径的方式访问到指定节点,如“A/B/C”。
对于列表项,刷新时如果每次使用完整路径,会有额外的不必要的路径解析操作。因此这个系统还需要能够临时缓存某个节点作为根节点的功能。
当某一节点的计数变动时,整个节点树的更新逻辑应该为:
- 深度优先更新该节点的所有子节点计数
- 递归更新该节点及其父节点的计数
因此以如上节点树为例,当需要更新C的红点时,更新顺序为:E、F、G、C、A(Root用于管理所有节点,不是逻辑节点)
在保证以上需求后,这个系统还应当对红点的数据部分和显示部分分开处理。比如,一个界面关闭后,有对应的功能刷新了,是不需要去处理显示的。
代码
废话不多说,直接上代码(部分地方使用了框架的接口,请替换成适配自己工程的代码。比如Traversal
可以替换为foreach
):
//————————————————————————————————————————————
// RedPointManager.cs
// For project: TooSimple Framework
//
// Created by Chiyu Ren on 2021-5-23 17:11
//————————————————————————————————————————————
using System.Collections.Generic;
using TooSimpleFramework.Common;
using TooSimpleFramework.Utils;
namespace TooSimpleFramework.Components.Managers
{
/// <summary>
/// 红点提示管理器
/// </summary>
public class RedPointManager : Singleton<RedPointManager>
{
#region Delegates
/// <summary>
/// 红点检查代理
/// </summary>
public delegate bool DataRefreshFunc();
/// <summary>
/// 红点视图刷新代理
/// </summary>
public delegate void ViewRefreshFunc(int pValue);
#endregion
private Node m_RootNode = new Node("Root");
private Stack<Node> m_RootStack = new Stack<Node>();
#region Public Methods
/// <summary>
/// 将指定路径的节点入栈,在PopRoot前所有操作都会以此作为根节点。需配合PopRoot(string)使用
/// </summary>
public void PushRoot(string pPath)
{
var node = this._GetNode(pPath);
if (node == null)
{
Debugger.LogError("RedPointManager.PushRoot >>Invalid pPath: {0}", pPath);
}
else
{
this.m_RootStack.Push(node);
}
}
/// <summary>
/// 将当前的节点出栈,需配合PushRoot(string)使用
/// </summary>
public void PopRoot()
{
if (this.m_RootStack.Count > 0)
{
this.m_RootStack.Pop();
}
else
{
Debugger.LogError("RedPointManager.PopRoot >>Unbalance PushRoot and PopRoot pair");
}
}
/// <summary>
/// 注册红点路径并为最后一个节点设置数据刷新回调。节点存在时将覆盖回调
/// </summary>
public void RegisterPath(string pPath, DataRefreshFunc pFunc)
{
var paths = this._SplitPath(pPath);
if (paths == null)
{
return;
}
var node = this._GetCurrentRoot();
for (int i = 0, count = paths.Length; i < count; i++)
{
node = this._GetOrAddNode(paths[i], node);
if (i == count - 1)
{
node.DataRefreshFunc = pFunc;
}
}
}
/// <summary>
/// 移除红点路径
/// </summary>
public void UnregisterPath(string pPath)
{
var node = this._GetNode(pPath);
if (node != null)
{
node.ChildMap.Remove(node.Name);
if (node.Parent != null)
{
node.Parent.RefreshData();
}
node.Dispose();
}
}
/// <summary>
/// 清空指定路径的红点的所有子节点
/// </summary>
public void ClearPath(string pPath)
{
var node = this._GetNode(pPath);
if (node != null)
{
node.ChildMap.Remove(node.Name);
}
}
/// <summary>
/// 为指定路径的红点绑定视图刷新回调
/// </summary>
public void BindViewRefreshCallback(string pPath, ViewRefreshFunc pRefreshFunc)
{
var node = this._GetNode(pPath);
if (node != null)
{
node.ViewRefreshFunc = pRefreshFunc;
}
}
/// <summary>
/// 为指定路径的红点解绑视图刷新回调
/// </summary>
public void UnbindViewRefreshCallback(string pPath)
{
var node = this._GetNode(pPath);
if (node != null)
{
node.ViewRefreshFunc = null;
}
}
/// <summary>
/// 刷新指定路径的红点
/// </summary>
public void Refresh(string pPath)
{
var node = this._GetNode(pPath);
if (node == null)
{
return;
}
// 刷新自己和下级节点的数据和视图
node.RefreshData();
node.NoticeRefreshView();
// 依次刷新至最顶层节点
while (node.Parent != this.m_RootNode)
{
node = node.Parent;
node.RefreshSelf();
}
}
#endregion
#region Private Methods
private Node _GetCurrentRoot()
{
return this.m_RootStack.Count > 0 ? this.m_RootStack.Peek() : this.m_RootNode;
}
private string[] _SplitPath(string pPath)
{
string[] ret = null;
do
{
if (string.IsNullOrEmpty(pPath))
{
Debugger.LogError("RedPointManager._SplitPath >>pPath is empty or null");
break;
}
var paths = pPath.Split('/');
if (paths.Length == 0)
{
Debugger.LogError("RedPointManager._SplitPath >>Invalid pPath: {0}", pPath);
break;
}
ret = paths;
} while (false);
return ret;
}
private Node _GetOrAddNode(string pName, Node pParentNode)
{
var parentChildMap = pParentNode.ChildMap;
if (!parentChildMap.TryGetValue(pName, out var ret))
{
ret = new Node(pName, pParentNode);
parentChildMap.Add(pName, ret);
}
return ret;
}
private Node _GetNode(string pPath)
{
Node ret = null;
do
{
var paths = this._SplitPath(pPath);
if (paths == null)
{
break;
}
if (!this._GetCurrentRoot().ChildMap.TryGetValue(paths[0], out var node))
{
break;
}
for (int i = 1, count = paths.Length; i < count; i++)
{
if (!node.ChildMap.TryGetValue(paths[i], out node))
{
break;
}
}
if (node != null)
{
ret = node;
}
} while (false);
return ret;
}
#endregion
////////////////////////////////////////////////////////////
private class Node
{
public string Name { get; private set; }
public Node Parent { get; private set; }
public Dictionary<string, Node> ChildMap { get; private set; }
public DataRefreshFunc DataRefreshFunc { private get; set; } // 刷新数据的回调
public ViewRefreshFunc ViewRefreshFunc { private get; set; } // 刷新试图的回调
private int m_nValue;
public Node(string pName, Node pParent = null)
{
this.Name = pName;
this.Parent = pParent;
this.ChildMap = new Dictionary<string, Node>();
}
public void Dispose()
{
new List<string>(this.ChildMap.Keys).Traversal((item) =>
{
if (this.ChildMap.TryGetValue(item, out var node))
{
node.Dispose();
}
});
if (this.Parent != null)
{
this.Parent.ChildMap.Remove(this.Name);
}
this.Name = null;
this.Parent = null;
this.ChildMap.Clear();
this.ChildMap = null;
this.DataRefreshFunc = null;
this.ViewRefreshFunc = null;
}
// 递归刷新自身和所有子节点的数据
public void RefreshData()
{
this._RefreshData(this);
if (this.ChildMap.Count == 0 && this.DataRefreshFunc != null)
{
this.m_nValue = this.DataRefreshFunc.Invoke() ? 1 : 0;
}
}
// 递归通知刷新自身和所有子节点的视图
public void NoticeRefreshView()
{
this._NoticeRefreshView(this);
}
// 刷新自己的数据和视图
public void RefreshSelf()
{
if (this.ChildMap.Count > 0)
{
this.m_nValue = 0;
this.ChildMap.Traversal((_, v) =>
{
this.m_nValue += v.m_nValue;
});
}
else
{
this.m_nValue = this.DataRefreshFunc.Invoke() ? 1 : 0;
}
this.ViewRefreshFunc?.Invoke(this.m_nValue);
}
private void _RefreshData(Node pNode)
{
this.m_nValue = 0;
this.ChildMap.Traversal((_, v) =>
{
v.RefreshData();
this.m_nValue += v.m_nValue;
});
}
private void _NoticeRefreshView(Node pNode)
{
this.ChildMap.Traversal((_, v) =>
{
v.NoticeRefreshView();
});
this.ViewRefreshFunc?.Invoke(this.m_nValue);
}
}
}
}
使用方法
以Unity为例,如图所示,点击按钮A后,弹出界面B,B中的列表每一项都带有红点:
点击其中几个列表项后,可以看到列表项本身和下方按钮上的红点都有变化:
所有带红点的列表项都点完后,可以看到下方按钮也没有红点了:
接下来是代码逻辑。在应用/游戏启动的时候,需要注册所有红点的更新逻辑:
var rpMgr = RedPointManager.Instance;
// 初始化数据
//
this.m_Datas = new bool[Count]; // 这里我们认为对应data为true则需要显示红点
for (int i = 0; i < Count; i++)
{
this.m_Datas[i] = Random.Range(0, 10000) < 5000;
}
// 注册按钮的红点,并绑定视图设置
//
rpMgr.RegisterPath("List", null);
rpMgr.BindViewRefreshCallback("List", (val) =>
{
this.m_ButtonRedPoint.SetActive(val > 0); // 按钮红点为数字型
this.m_ButtonRedPointText.text = val.ToString();
});
// 注册列表项的红点路径
//
rpMgr.PushRoot("List");
for (int i = 0; i < Count; i++)
{
var idx = i;
rpMgr.RegisterPath("ListItem_" + (idx + 1), () =>
{
return this.m_Datas[idx];
});
}
// 创建列表的子项,绑定列表项的红点视图设置
//
this.m_TempletObject.SetActive(false);
for (int i = 0; i < Count; i++)
{
var newObj = this.m_TempletObject.Copy(this.m_ListRoot);
newObj.SetActive(true);
this.m_ListViewItems[i] = new ListItem(i, newObj, this.m_Datas);
}
rpMgr.PopRoot();
列表项创建的时候,在构造方法中绑定视图设置:
public ListItem(int pIndex, GameObject pGameObject, bool[] pSrcData)
{
RedPointManager.Instance.BindViewRefreshCallback("ListItem_" + (this.m_nIndex + 1), (val) =>
{
this.m_RedPointObj.SetActive(val > 0);
});
}
在列表项被点击的时候,更新红点:
private void OnClicked()
{
this.m_SrcData[this.m_nIndex] = false;
RedPointManager.Instance.Refresh("List/ListItem_" + (this.m_nIndex + 1));
}
在界面打开时,刷新红点显示:
rpMgr.Refresh("List"); // 界面打开时
在界面关闭时,注销列表项的显示回调
private void OnClose()
{
RedPointManager.Instance.UnbindViewRefreshCallback("List/ListItem_" + (this.m_nIndex + 1));
}
后记
这篇有点水,下一篇给大家整个硬货,在Unity中渲染一个黑洞,《星际穿越》的那种效果哦~
很惭愧,就做了一点微小的工作,谢谢大家。