邮件、私聊、成就和活动入口都可能显示红点。真正复杂的地方不是让一个节点显示出来,而是如何让子功能变化后,自动刷新上层入口,并让同一份红点状态同步到多个界面。
MyFramework 使用 RedPointSystem 管理红点数据、UI 绑定、事件刷新和父子节点传播。
项目地址:
一、红点应该是独立的数据对象
最直接的写法,是在界面打开时查询数据:
bash
mMailRedPoint.setActive(
mMailManager.hasUnreadMail());
这种方式存在几个问题:
bash
只有界面打开时才会刷新
多个界面需要重复查询
下层数据变化后,上层入口不会自动更新
红点逻辑逐渐散落到各个UI中
MyFramework 将红点状态独立为 RedPoint:
bash
业务数据
↓
RedPoint 计算是否显示
↓
绑定一个或多个 UI 节点
↓
RedPointSystem 统一刷新
UI 只负责展示,不负责判断业务条件。
二、使用树形结构组织红点
假设主界面中包含邮件、私聊和成就:
bash
主菜单红点
├── 邮件入口红点
│ ├── 邮件1
│ ├── 邮件2
│ └── 邮件3
├── 私聊入口红点
│ ├── 玩家A
│ └── 玩家B
└── 成就入口红点
├── 战斗成就
└── 收集成就
创建根节点和分类节点:
bash
using static FrameBaseHotFix;
public class RedPointManager : FrameSystem
{
public static RedPoint mMain;
public static RedPoint mMainMail;
public static RedPoint mMainPrivateChat;
public static RedPoint mMainAchievement;
public override void init()
{
base.init();
mRedPointSystem.createRedPoint(
out mMain);
mRedPointSystem.createRedPoint(
out mMainMail,
mMain);
mRedPointSystem.createRedPoint(
out mMainPrivateChat,
mMain);
mRedPointSystem.createRedPoint(
out mMainAchievement,
mMain);
}
}
这些中间节点直接使用基础的 RedPoint。
基础节点不判断具体业务,只根据子节点决定自己是否显示:
bash
public virtual void refresh()
{
bool enable = false;
foreach (RedPoint point in mChildren)
{
if (point.isEnable())
{
enable = true;
break;
}
}
setEnable(enable);
}
只要任意子节点开启,父节点就会开启。
三、只有叶节点负责业务判断
以单封邮件的红点为例:
bash
public class RedPointMail : RedPoint
{
protected Mail mMail;
protected override void initEventType()
{
base.initEventType();
addEvent<EventUnreadMailChanged>();
}
public override void refresh()
{
setEnable(!mMail.isRead());
}
public override void resetProperty()
{
base.resetProperty();
mMail = null;
}
public void setMail(Mail mail)
{
mMail = mail;
}
}
叶节点负责两件事:
bash
监听哪些事件
根据什么数据判断是否显示
收到 EventUnreadMailChanged 后,红点不会立刻重复查询数据,而是先标记:
bash
protected void onEventTrigger()
{
if (mChildren.Count > 0)
{
return;
}
mIsDirty = true;
}
RedPointSystem.update() 每帧统一刷新脏节点:
bash
public override void update(float elapsedTime)
{
base.update(elapsedTime);
foreach (RedPoint point in mRedPointList)
{
if (!point.isDirty())
{
continue;
}
point.setDirty(false);
point.refresh();
notifyRedPointChanged(point);
}
}
即使同一帧连续收到多次事件,同一个叶节点也只会刷新一次。
四、叶节点变化后如何更新父节点
叶节点刷新完成后,会向父节点递归传播:
bash
protected void onRedPointChanged(
RedPoint node)
{
RedPoint parent = node.getParent();
if (parent == null)
{
return;
}
parent.refresh();
onRedPointChanged(parent);
}
例如一封邮件从未读变成已读:
bash
RedPointMail 关闭
↓
刷新邮件分类节点
↓
检查是否还有其他未读邮件
↓
刷新主菜单节点
业务代码不需要分别刷新邮件入口和主菜单入口。
这也是树形红点结构最主要的价值。
五、为业务数据创建叶节点
每一封邮件创建时,同时创建对应红点:
bash
using static FrameBaseHotFix;
using static RedPointManager;
public class Mail : ClassObject
{
protected RedPointMail mRedPoint;
protected bool mHasRead;
public void init()
{
mRedPointSystem
.createRedPoint(
out mRedPoint,
mMainMail)
.setMail(this);
}
public override void destroy()
{
base.destroy();
mRedPointSystem?.destroyRedPoint(
mRedPoint);
mRedPoint = null;
}
public override void resetProperty()
{
base.resetProperty();
mRedPoint = null;
mHasRead = false;
}
public bool isRead()
{
return mHasRead;
}
public RedPointMail getRedPoint()
{
return mRedPoint;
}
}
红点应该尽早创建。
如果先收到未读邮件变化事件,之后才创建红点,那么这次事件就无法触发它刷新。
动态业务对象销毁时,也必须销毁自己的红点:
bash
mRedPointSystem.destroyRedPoint(
mRedPoint);
销毁叶节点后,框架会自动刷新它原来的父节点。
六、将红点绑定到 UI
邮件列表项创建后,只需要绑定显示节点:
bash
public void setMail(Mail mail)
{
mMail = mail;
mMail.getRedPoint().bindPointUI(
mUnreadMark);
}
绑定时会立即同步当前状态:
bash
public void bindPointUI(
myUGUIObject point,
bool showError = true)
{
mPointUIMap.TryAdd(
point,
getGameObjectPath(
point.getGameObject()));
point.setActive(mEnable);
}
所以无论 UI 在红点刷新前还是刷新后创建,都能获得正确的显示状态。
同一个红点可以同时绑定多个 UI:
bash
RedPointManager.mMainMail.bindPointUI(
mMainMailRedPoint);
RedPointManager.mMainMail.bindPointUI(
mMenuMailRedPoint);
邮件状态变化后,两个位置会同时刷新。
七、UI 销毁或回收时必须解除绑定
可回收列表项离开界面时,需要解除绑定:
bash
public override void recycle()
{
if (mMail.isValid())
{
mMail.getRedPoint()?.removePointUI(
mUnreadMark);
}
mMail = null;
base.recycle();
}
普通界面可以在销毁时处理:
bash
public override void destroy()
{
RedPointManager.mMainMail.removePointUI(
mMailRedPoint);
base.destroy();
}
如果 UI 已经销毁,但仍保留在红点中,下一次刷新时会访问无效对象。
框架会输出提示:
bash
红点的UI节点已经被销毁了,
但是仍然没有被移除绑定
因此红点使用需要关注四个生命周期节点:
bash
创建红点
绑定 UI
解除 UI 绑定
销毁红点
八、显示未读数量
私聊入口除了显示红点,还需要显示未读数量,可以继承 RedPointCount:
bash
public class RedPointPrivateChat : RedPointCount
{
protected PrivateChatInfo mChat;
protected override void initEventType()
{
base.initEventType();
addEvent<EventPrivateChange>();
}
public override void refresh()
{
setCount(
mChat.getUnreadCount());
}
public override void resetProperty()
{
base.resetProperty();
mChat = null;
}
public void setPrivateChat(
PrivateChatInfo chat)
{
mChat = chat;
}
}
绑定红点和数字文本:
bash
mPrivateChat.getRedPoint().setPointUI(
mMessagePoint,
mUnreadMessageCount);
解除绑定:
bash
mPrivateChat.getRedPoint().removePointUI(
mMessagePoint,
mUnreadMessageCount);
setCount() 会同时更新文本和显示状态:
bash
public void setCount(int count)
{
mCount = count;
mPointCountText?.setText(mCount);
setEnable(mCount > 0);
}
数量为零时,整个红点自动隐藏。
九、主动刷新全部红点
系统初始化完成,或者一次性加载大量服务器数据后,可以主动刷新整棵树:
bash
mRedPointSystem.refresh();
它只从根节点开始遍历:
bash
public void refresh()
{
foreach (RedPoint point in mRedPointList)
{
if (point.getParent() == null)
{
refreshRedPoint(point);
}
}
}
递归过程会先刷新叶节点,再刷新父节点:
bash
刷新全部子节点
↓
刷新当前父节点
这样父节点读取到的一定是子节点的最新状态。
十、哪些场景不适合使用这套红点树
并不是所有看起来像红点的需求,都适合创建长期红点对象。
例如:
bash
翻页列表中,只表示当前页某个奖励是否可领取
红点关联的数据对象频繁更换
显示前面所有页面是否存在奖励
纯粹由当前筛选条件临时计算的标记
这些状态与固定业务对象没有稳定的一对一关系。
如果为每次翻页反复创建和销毁红点,反而会增加管理复杂度。此类临时状态直接由当前界面计算通常更合适。
十一、总结
MyFramework 的红点系统使用树形结构组织数据:
bash
叶节点
监听业务事件并计算自身状态
父节点
根据子节点自动汇总状态
RedPointSystem
统一刷新脏节点并向上传播
UI节点
只绑定和显示,不判断业务逻辑
基本接入流程是:
- 尽早创建根节点和分类节点
- 为具体业务对象创建叶节点
- 在叶节点中监听事件并重写 refresh
- UI 创建时调用 bindPointUI
- UI 回收时调用 removePointUI
- 业务对象销毁时调用 destroyRedPoint
这样邮件、私聊、成就和活动等红点可以形成统一的数据树。下层状态变化后,上层入口自动刷新,同一份红点状态也能同时驱动多个界面。