一个简简单单的红点系统框架
前言
今天我们简简单单做一个红点系统框架。在应用和游戏中,按钮上的红点非常常见。如图所示:
红点会让强迫症烦躁不安,但又不可或缺。这里分享一个自用的红点系统框架。
转载请注明出处: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中渲染一个黑洞,《星际穿越》的那种效果哦~
很惭愧,就做了一点微小的工作,谢谢大家。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!