一個簡簡單單的紅點系統框架
前言
今天我們簡簡單單做一個紅點系統框架。在應用和遊戲中,按鈕上的紅點非常常見。如圖所示:
紅點會讓強迫症煩躁不安,但又不可或缺。這裡分享一個自用的紅點系統框架。
轉載請註明出處://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中渲染一個黑洞,《星際穿越》的那種效果哦~
很慚愧,就做了一點微小的工作,謝謝大家。