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