一個簡簡單單的紅點系統框架

GuyaWeiren發表於2021-09-12

前言

今天我們簡簡單單做一個紅點系統框架。在應用和遊戲中,按鈕上的紅點非常常見。如圖所示:

紅點會讓強迫症煩躁不安,但又不可或缺。這裡分享一個自用的紅點系統框架。

轉載請註明出處: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中渲染一個黑洞,《星際穿越》的那種效果哦~

很慚愧,就做了一點微小的工作,謝謝大家。

相關文章