概要
`最近给公司的项目做了一个方便扩展的红点系统,并且为了尽快适配项目,这个红点系统尽量趋向于低耦合,可适配多种UI情况的结构。这里因为每个人的项目需求不同,所以我的红点方案也只是仅供参考,提供思路
基础红点设计需求
红点的设计应该是围绕着需求展开的,而一般红点的基本需求应该会有以下几种
- 关联变更:当某个红点发生变化的时候,这个红点的父级,或者父级的父级也可能产生变化
- 红点刷新:当玩家产生一些操作,或者数据变更的时候,需要变更红点
扩展需求
- 有一些红点需要倒计时刷新
- 有些红点的状态是需要保存的,等下次打开后不再出现
基本设计思路
- -首先我们需要保存红点的状态,例如这个节点下,红点的数量,还有红点的关联关系,类似链表的形式,给红点设置父节点引用,让其可以索引到父节点。
- 然后我们需要提供对应的检测回调函数,用来判断是否刷新红点,并刷新对应红点的数据结构
- 当我们刷新红点数据后,就需要在适合的时机刷新UI,其实我们这里采用的是数据驱动的方式。
准备内容
- 首先我们需要预先定义一些内容。创建一个RedDotManager的红点控制类,红点的控制逻辑会写在这里。这个类最好使用单例。
- 创建红点的数据结构,一定要包括的是,红点组id,父节点id/父节点索引,用来初始化节点关系。红点状态,红点状态我这边使用01表示。子节点的索引,用来快速索引子节点,刷新回调,用来判断红点是否刷新。
public class RedDotData
{
/// <summary>
/// id
/// </summary>
public int id;
/// <summary>
/// 组Id
/// </summary>
public int gId;
/// <summary>
/// 依赖的父id
/// </summary>
public int pId;
/// <summary>
/// 红点数
/// </summary>
public int redCount;
/// <summary>
/// 计算回调
/// </summary>
public Action<RedDotData> updateCallBack;
/// <summary>
/// 子节点
/// </summary>
public List<RedDotData> childrens;
/// <summary>
/// 更新时间
/// </summary>
public float time;
public RedDotData()
{
id = 0;
gId = 0;
pId = 0;
redCount = 0;
updateCallBack = null;
childrens = new List<RedDotData>();
time = 0;
}
public void OverWrite(RedDotData data)
{
if (id != data.id)
return;
gId = data.gId;
pId = data.pId;
redCount = data.redCount;
updateCallBack = data.updateCallBack;
time = data.time;
}
public int GetCount()
{
if (childrens != null && childrens.Count > 0)
{
var add = 0;
foreach (var red in childrens)
{
add += red.GetCount();
}
return add;
}
else
{
return redCount;
}
}
}
- 内部定义一个字典的数据结构,用来以键值对的形式保存红点组和红点组数据,以快速索引红点数据。`
private Dictionary<int, List<RedDotData>> redsGroup = new Dictionary<int, List<RedDotData>>();
- 定义红点节点id和红点组id,用来索引红点节点
/// <summary>
/// 红点组ID
/// </summary>
public static class defGroupRedId
{
public const int Shop = 0;//商店红点
public const int QuickMenu = 1;//菜单
}
/// <summary>
/// 红点Id
/// </summary>
public static class defRedId
{
public const int Root = -1;//根节点
public const int Shop = 0;//商店红点总
public const int ShopChild1 = 1;//分页商店1
public const int ShopChild2 = 2;//分页商店2
public const int ShopChild2_Child1 = 3;//分页商店2-子对象1
public const int QuickMenu = 4;//菜单
public const int QuickMenuChild1 = 5;//菜单分页1
public const int QuickMenuChild2 = 6;//菜单分页2
public const int QuickMenuChild3 = 7;//菜单分页3
}
基础逻辑
- 首先需要划分节点关系,一个大功能中所有的红点都属于一个组,组组里的节点根据不同深度的功能继续细分。例如我使用明日方舟的UI作为例子。三张图中,第一张图的采购中心这边的红点作为一级红点,也就是这个商店的一级红点。以此类推,第二张图的红点是二级红点,第三张图是三级红点。
划分节点后,需要用代码来处理节点关系。创建一个OnPerAdd,初始化红点的节点数据,并关联节点的数据,将父节点,红点刷新函数作为参数传入。最后将初始化好的红点的节点数据存入我们开始创建的红点对应的的组数据结构中。
/// <summary>
/// 预先加入依赖关系
/// </summary>
private void OnPerAdd(int gId, int pId, int id, Action<RedDotData> updateCallBack)
{
if (redsGroup != null)
{
RedDotData redDotData = null;
List<RedDotData> redDotDatas = null;
if (redsGroup.TryGetValue(gId, out redDotDatas))
{
redDotData = redDotDatas.Find(m => m.id == id);
if (redDotData == null)
{
redDotData = new RedDotData();
redDotData.id = id;
redDotData.pId = pId;
redDotData.gId = gId;
redDotData.updateCallBack = updateCallBack;
redDotData.redCount = 0;
redDotDatas.Add(redDotData);
}
else
{
redDotData.gId = gId;
redDotData.id = id;
redDotData.updateCallBack = updateCallBack;
redDotData.redCount = 0;
redDotData.pId = pId;
}
}
else
{
redDotDatas = new List<RedDotData>();
redDotData = new RedDotData();
redDotData.id = id;
redDotData.pId = pId;
redDotData.gId = gId;
redDotData.updateCallBack = updateCallBack;
redDotData.redCount = 0;
redDotDatas.Add(redDotData);
redsGroup[gId] = redDotDatas;
}
}
}
最后调用函数,按照我们刚才划分好的红点的节点关系,创建初始化好各个节点。这里我们定义了商店的组id,和对应的三级红点结构。与上面 三张图的三个红点相对应(这里的命名需要根据实际项目规范进行命名),对应的红点刷新函数后面还会提到,先不讲。
//商店
onPerAdd(defGroupRedId.Shop, defRedId.Shop, defRedId.Shop, null);
onPerAdd(defGroupRedId.Shop, defRedId.Shop, defRedId.ShopChild1, null);
onPerAdd(defGroupRedId.Shop, defRedId.Shop, defRedId.ShopChild2, onCheckShopChild2RedCount);
onPerAdd(defGroupRedId.Shop, defRedId.ShopChild2, defRedId.ShopChild2_Child1, onCheckShopChildChild1RedCount);
- 创建一个onArrangeRed函数,用来处理我们定义好的红点节点关系,也就是根据红点父节点id,我们取得每个红点节点的子节点,并将其存入红点数据结构childrens中,这么做的目的是为了在计算红点数量的时候能较快的索引到子节点的红点数量。这个函数必须在设置好引用关系后进行调用
/// <summary>
/// 整理红点子节引用
/// </summary>
private void onArrangeRed()
{
if (redsGroup != null)
{
foreach (var groups in redsGroup)
{
foreach (var red in groups.Value)
{
var parent = groups.Value.Find(m => m.id == red.pId && red.pId != red.id);
if (parent != null)
{
if (parent.childrens.Find(m => m.id == red.id) == null)
{
parent.childrens.Add(red);
}
}
}
}
}
}
/// <summary>
/// 预先添加所有节点的引用关系
/// </summary>
private void onPerAddAll()
{
//商店
onPerAdd(defGroupRedId.Shop, defRedId.Shop, defRedId.Shop, null);
onPerAdd(defGroupRedId.Shop, defRedId.Shop, defRedId.ShopChild1, null);
onPerAdd(defGroupRedId.Shop, defRedId.Shop, defRedId.ShopChild2, onCheckShopChild2RedCount);
onPerAdd(defGroupRedId.Shop, defRedId.ShopChild2, defRedId.ShopChild2_Child1, onCheckShopChildChild1RedCount);
//菜单
onPerAdd(defGroupRedId.QuickMenu, defRedId.QuickMenu, defRedId.QuickMenu, null);
onPerAdd(defGroupRedId.QuickMenu, defRedId.QuickMenu, defRedId.QuickMenuChild1, onCheckQuickMenuChild1RedCount);
onPerAdd(defGroupRedId.QuickMenu, defRedId.QuickMenu, defRedId.QuickMenuChild2, onCheckQuickMenuChild2RedCount);
onPerAdd(defGroupRedId.QuickMenu, defRedId.QuickMenu, defRedId.QuickMenuChild3, onCheckQuickMenuChild3RedCount);
//必须在最后
onArrangeRed();
}
- 接下来处理一个很关键点就是,我们需要处理不同功能下的红点刷新函数,并将其保存到具体的节点数据中,用来判断这个红点是否刷新。因为每个项目的每个功能的红点刷新都不尽相同,所以这里我不会写细节的红点刷新函数,但是我会提供思路。
我这边的项目需求是,当某些数据变更的时候,需要显示红点,那么我在这里的刷新函数中,我通过判断红点的数据变更来判断红点是否显示,如果显示,红点数据的redCount设置为1,但是反之我不会将其设置为0。因为我的项目需求是点击红点对应的按钮会销毁红点,所以我红点的销毁并不是在这里进行处理的,所以我这里的逻辑只需要判断红点是否显示即可。
如果有同学的功能需求数据变更的时候也要删除红点,那么你就需要将删除红点的逻辑放在这里,如果满足条件redCount设置为1,否则redCount设置为0。 - 还有一个点,大家可以看到我的onPerAdd有一些并没有设置红点刷新函数,是因为我们的红点刷新函数只需要设置在最深层的红点节点,我们只需要关心最深处的红点是否存在。如果最深处红点存在,那么它的父节点,父节点的父节点,都相当于红点数+1,我们可以通过前面构造好的红点关系来计算任何一个红点节点的红点数。所以我们在红点的数据结构中开了一个计算红点数的方法。
public int GetCount()
{
if (childrens != null && childrens.Count > 0)
{
var add = 0;
foreach (var red in childrens)
{
add += red.GetCount();
}
return add;
}
else
{
return redCount;
}
}
- 当我们处理好红点数后,就可以用简单的红点判断函数,通过红点数判断,这个红点是否显示,这个函数跟红点刷新函数的功能是不一样的,不要混淆了。当我们外部需要判断某个红点是否存在的时候,就可以调用这个方法CheckRed。
/// <summary>
/// 判断是否显示红点,替换原本的红点判断逻辑
/// </summary>
/// <param name="gId"></param>
/// <param name="pId"></param>
/// <param name="id"></param>
/// <returns></returns>
public bool CheckRed(int gId, int id)
{
if (redsGroup != null)
{
List<RedDotData> redDotDatas = null;
RedDotData redDotData = null;
if (redsGroup.TryGetValue(gId, out redDotDatas))
{
redDotData = redDotDatas.Find(m => m.id == id);
if (redDotData != null)
{
return redDotData.GetCount() > 0;
}
}
}
return false;
}
- 然后就是也是一个很关键的点,这里我曾经出现过bug,就是在何时去刷新UI。记得我说过,我这套红点是基于数据驱动的,数据改变的时候,才会去改变红点。所以,在数据变更的地方(服务器消息更新通知,玩家操作)我们需要去通知对应的红点刷新数据。这里我提供了三个函数,分别是UpdateRedDotById更新单个红点,更新完之后必须更新一下组红点的所有UI(这里是可以优化的,当有真的变更的时候再去刷新会好一点,UpdateRedDotByGroup更新组红点,和onUpdateGroupUI更新红点UI。
onUpdateGroupUI需要根据项目的不同去具体实现,这里提供一个思路,在每个界面的实例类提供一个专门的红点刷新函数,然后使用switch去做分别刷新每个红点Id对应的界面实例,这样子将红点刷新与界面UI刷新分离开,更好维护。或者可以使用事件通知,通知对应的UI去刷新。反正就是,当我更新了红点数据,那么就一定要去刷新整个组的Ui,因为这个组的UI是互相关联的。
/// <summary>
/// 单个红点更新
/// </summary>
/// <param name="gId"></param>
/// <param name="id"></param>
public void UpdateRedDotById(int gId, int id)
{
RedDotData redDotData = onFindRedDotData(gId, id);
if (redDotData != null)
{
if (redDotData.updateCallBack != null)
{
redDotData.updateCallBack.Invoke(redDotData);
}
onUpdateGroupUI(gId);
}
}
/// <summary>
/// 单个组更新,Update不和Check放一起是为了节约性能
/// </summary>
/// <param name="gId"></param>
/// <param name="id"></param>
public void UpdateRedDotByGroup(int gId)
{
if (redsGroup != null)
{
List<RedDotData> groups = null;
if (redsGroup.TryGetValue(gId, out groups))
{
foreach (var red in groups)
{
if (red.updateCallBack != null)
{
red.updateCallBack.Invoke(red);
}
}
onUpdateGroupUI(gId);
}
}
}
/// <summary>
/// 更新组红点UI---有需要倒计时之类的刷新可以使用
/// </summary>
private void onUpdateGroupUI(int gId)
{
//数据更新后刷新UI
switch (gId)
{
case defGroupRedId.Shop:
onUpdateUIRedDotShop();
break;
}
}
- 补充说明,关于红点的具体的UI显示和隐藏这个涉及到每个UI框架的不同的创建UI和管理UI的方式。这里也仅提供思路,因为本红点系统的重心是在数据处理和逻辑刷新方面。正常来说红点的UI应该是基于对象池进行动态创建和动态的附加,当需要显示红点的时候,我们从对象池里创建一个红点UI对象,并附在对应的UI上,不需要则移除,如果需要知道有几个红点,也可以通过我的红点计算接口计算出来。
主要流程总结
- 我们需要整理红点节点直接的依赖关系,并使用onPerAdd函数设置好依赖关系和红点刷新回调函数
- 补充好回调函数,明确项目需求
- 初始化UI的时候需要调用一次红点的数据更新UpdateRedDotById或者UpdateRedDotByGroup,根据需求。用来初始化红点数据。并记得再每个界面类提供一个UI的红点刷新接口,用来刷新实际的UI。
- 在红点刷新接口中,我们使用CheckRed函数用来判断是否存在红点或者获取红点的数据,用来做最后的显示工作。
- 需要在对应的数据刷新接口那边增加红点的刷新接口,用来刷新红点数据。
扩展应用
一般来说,以上的接口其实只能满足最低的需求,这里提供一些扩展思路。
- 当我存在一个功能,这个功能会在某个时刻刷新出一个红点,这个时候就需要一个计时器在某个固定的时刻刷新。我是这样子处理的,先设置一个刷新时间,然后写一个定时器来进行判断,判断是否到了这个给定的刷新时间,如果到了就进行红点的刷新操作。
/// <summary>
/// 设置单个红点计时器更新--提供倒计时刷新功能
/// </summary>
/// <param name="gId"></param>
/// <param name="id"></param>
/// <param name="time">下次更新时间,如果不传则不处理</param>
public void SetRedDotTimer(int gId, int id, float time)
{
RedDotData redDotData = onFindRedDotData(gId, id);
if (redDotData != null)
{
redDotData.time = time;
}
}
/// <summary>
/// 红点计时器,用于特殊倒计时红点刷新
/// </summary>
public void LoopUpdate()
{
if (redsGroup != null)
{
foreach (var groups in redsGroup)
{
bool isUpdateGroup = false;
foreach (var red in groups.Value)
{
red.time = red.time - Time.deltaTime;
if (red.time <= 0)
{
isUpdateGroup = true;
red.time = 0;
if (red.updateCallBack != null)
{
red.updateCallBack.Invoke(red);
}
}
}
if (isUpdateGroup)
{
onUpdateGroupUI(groups.Key);
}
}
}
}
- 有些时候会要求需要保存玩家的红点状态,例如点过之后就不再显示。这时候就要涉及到本地数据保存和比较,本地数据保存也是因为项目不同所以不会过多阐述。这里我们需要保存玩家的本地数据(或者如果服务器支持,也可以让服务器存)然后跟新的红点数据去比较,比较的代码是写在之前的红点刷新函数内。