一个简简单单的红点系统框架

前言

今天我们简简单单做一个红点系统框架。在应用和游戏中,按钮上的红点非常常见。如图所示:

红点会让强迫症烦躁不安,但又不可或缺。这里分享一个自用的红点系统框架。

转载请注明出处:https://www.cnblogs.com/GuyaWeiren/p/15259108.html

设计思路

每一个需要显示红点的地方,都视作一个节点。由于界面存在嵌套关系,所以节点可以包含N个子节点,如下图所示在运行时红点应当是一个树结构:

为了简化逻辑,所有红点都视作数字型红点。至于UI上到底要显示成数字,还是一个点,还是别的情况,可以在刷新的回调中单独处理。

如果某节点没有任何子节点,则该节点的计数只可能是0或1。因为如果某个按钮上显示一个2的红点,但点开之后界面中没有任何红点,看起来会很奇怪。

如果某节点包含至少一个子节点,则该节点的计数为所有子节点的计数和。

每一个节点使用一个字符串作为标识,可以通过文件路径的方式访问到指定节点,如“A/B/C”。

对于列表项,刷新时如果每次使用完整路径,会有额外的不必要的路径解析操作。因此这个系统还需要能够临时缓存某个节点作为根节点的功能。

当某一节点的计数变动时,整个节点树的更新逻辑应该为:

  1. 深度优先更新该节点的所有子节点计数
  2. 递归更新该节点及其父节点的计数

因此以如上节点树为例,当需要更新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中渲染一个黑洞,《星际穿越》的那种效果哦~

很惭愧,就做了一点微小的工作,谢谢大家。

posted @ 2021-09-12 16:37  GuyaWeiren  阅读(2372)  评论(0编辑  收藏  举报