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