zoukankan      html  css  js  c++  java
  • 一个简简单单的红点系统框架

    前言

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

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

    转载请注明出处: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中渲染一个黑洞,《星际穿越》的那种效果哦~

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

  • 相关阅读:
    html2pdf后逐页固定位置盖公章
    c#Stream学习笔记
    Flume -- 开源分布式日志收集系统
    Sqoop -- 用于Hadoop与关系数据库间数据导入导出工作的工具
    Hive -- 基于Hadoop的数据仓库分析工具
    HBase -- 基于HDFS的开源分布式NoSQL数据库
    ZooKeeper -- 分布式开源协调服务
    Hadoop学习(4)-- MapReduce
    Hadoop学习(3)-- 安装1.x版本
    Hadoop学习(2)-- HDFS
  • 原文地址:https://www.cnblogs.com/GuyaWeiren/p/15259108.html
Copyright © 2011-2022 走看看