unity 红点树
红点的实现,一般都是有层级关系的,子节点的红点亮起来的时候,要把父节点的红点也给点亮,当所有子节点红点都隐藏的时候,父节点的红点也需要跟着进行隐藏。红点的逻辑在设计的时候,尽量与系统功能进行分离,我们在实现红点系统的时候,只用关心最后面那个节点也就是叶子节点的显示隐藏状态,父节点会根据子节点的状态自动进行刷新处理。
如图所示,当我们要实现上面这个功能的红点时候,我们可以把每一层显示的红点抽出一个红点对象 RedDotModel,然后构建一个红点树,这个红点树可以通关配置进行初始化,配置内容需要包含红点类型,红点拥有的子节点,叶子节点不需要配置子节点.
红点model中保存的有自己父节点对象和所有子节点对象
红点简单配置如下
local redDotConfig = {
[1] = { name = "功能A 1级界面", serverRedModelName = "", clientRedModelName = "", redType = -1, functionList = {}, childrens = {2}},
[2] = { name = "功能A 2级界面", serverRedModelName = "", clientRedModelName = "", redType = -1, functionList = {}, childrens = {3,4}},
[3] = { name = "功能A 3级界面A", serverRedModelName = "", clientRedModelName = "ChallengeNewChapterRedDotModel", redType = 3, functionList = {}, childrens = {}},
[4] = { name = "功能A 3级界面B", serverRedModelName = "", clientRedModelName = "ChallengeRewardRedDotModel", redType = 2, functionList = {}, childrens = {}},
[5] = { name = "功能B 1级界面", serverRedModelName = "", clientRedModelName = "", redType = -1, functionList = {}, childrens = {6}},
[6] = { name = "功能B 2级界面", serverRedModelName = "DayTaskFinishRedDotModel", clientRedModelName = "", redType = 1, functionList = {}, childrens = {}},
}
红点配置映射到红点集合,
function RedDotManager:init(serverRedModels)
---@type RedDotModel[]
self.redModels = {}
self.bindList = {}
---初始化红点列表
for id, v in pairs(redDotConfig) do
local redModel
if v.clientRedModelName and v.clientRedModelName ~= "" then
local redId = id
printError(v.clientRedModelName)
---客户端红点需要重写一个继承RedDotModel的脚本,处理红点显示逻辑
local redModelClas = require("models.redDot."..v.clientRedModelName)
redModel = redModelClas.new(id, v)
else
redModel = RedDotModel.new(id, v)
end
self.redModels[id] = redModel
end
local leafRedModes = {} --叶节点红点model列表
---初始父子级关系
for i,redModel in ipairs(self.redModels) do
local childs = redModel.config.childrens
if childs and next(childs) then
for _,childId in ipairs(childs) do
if self.redModels[childId] then
local childModel = self.redModels[childId]
redModel:addChild(childModel)
else
printError("子节点的配置有缺失,redId = " ..redModel.id..", childId = " .. childId)
end
end
else
table.insert(leafRedModes, redModel)
end
end
end
在UI界面中,我们先从红点管理器中获取当前界面红点对象,然后把红点对象绑定到红点对象身上,当红点数据发生的时候,自动刷新关联的红点G阿么Object对象的显示隐藏状态
local gob = UnityEngine.GameObject.New()
gob.name = "1级界面";
gob.transform.position = UnityEngine.Vector3.New(1,1,1)
local redModel = RedDotManager:getRedModelById(1)
RedDotManager:bind(gob, redModel)
完整代码如下
红点model基类
RedDotModel = class("RedDotModel")
---@class RedDotModel
local RedDotModel = RedDotModel
---RedDotModel主要内容
---1 保存子节点列表 和 父节点对象
---2 通关setNum方法设置红点数量的时候,通关回调函数刷新界面显示
function RedDotModel:ctor(id, cfg)
self.id = id
self.config = cfg
---@type RedDotModel[] 子节点列表
self.childs = {}
---@type function[] 回调函数集合
self.callbacks = {}
self.num = 0
end
function RedDotModel:onInit()
self:refreshNum()
end
---添加子节点
function RedDotModel:addChild(child)
table.insert(self.childs, child)
child:_addParent(self)
end
---@param trarget GameObject
---@param callback function
function RedDotModel:addCallback( trarget, callback )
self.callbacks[trarget] = callback
end
function RedDotModel:removeCallback(target)
if self.callbacks[target] then
self.callbacks[target] = nil
end
end
---移除子红点(通常功能红点是不需要移除的,但是如果红点对象是某个武器,武器分解后,这个武器相关的红点是需要移除的)
---@param child RedDotModel
function RedDotModel:removeChild(child)
local index = table.indexof(self.childs, child)
if index and index > 0 then
table.remove(self.childs, index)
if #self.childs > 0 then
self:refreshNum()
else
if self.parent then
self.parent:removeChild(self)
self.parent:refreshNum()
end
end
end
child:onDestroy()
end
function RedDotModel:removeAllChild()
for i,child in ipairs(self.childs) do
self:removeAllChild(child)
end
end
function RedDotModel:getChildById(redId)
for i,model in ipairs(self.childs) do
if model.id == redId then
return model
end
end
end
---设置红点数量,只用于刷新没有父节点的对象,父节点的num通关通过子节点自动获取
function RedDotModel:setNum(num)
assert(not next(self.childs), "父节点不能直接设置数量: " .. self.id)
self:_setNum(num)
end
---refreshNum用于刷新父节点红点数量,子节点红点发生变化时候用setNum 方法
function RedDotModel:refreshNum()
if next(self.childs) then
local num = 0
local oldNum = self.num
--获取所有子红点数量
for idx, childModel in ipairs (self.childs) do
num = num + childModel:getNum()
end
self:_setNum(num)
end
end
function RedDotModel:getNum()
return self.num
end
---红点对象消耗时候调用
function RedDotModel:onDestroy()
end
---添加父节点
function RedDotModel:_addParent(parent)
self.parent = parent
end
function RedDotModel:_setNum(num)
local oldNum = self.num
self.num = num
self:_onNumChange(self.num - oldNum)
end
function RedDotModel:_onNumChange(diffNum)
if diffNum and diffNum ~= 0 then
for target, callback in pairs(self.callbacks) do
callback(target)
end
end
if self.parent then
self.parent:refreshNum()
end
end
红点管理类
RedDotManager = {}
---@class RedDotManager
local RedDotManager = RedDotManager
require("RedDot/RedDotBinder")
RedDotShowType = {
Normal = 1, --常规红点
Reward = 2, --奖励类型红点
New = 3, --“新” 红点
}
---红点配置
local redDotConfig = {
[1] = { name = "功能A 1级界面", serverRedModelName = "", clientRedModelName = "", redType = -1, functionList = {}, childrens = {2}},
[2] = { name = "功能A 2级界面", serverRedModelName = "", clientRedModelName = "", redType = -1, functionList = {}, childrens = {3,4}},
[3] = { name = "功能A 3级界面A", serverRedModelName = "", clientRedModelName = "ChallengeNewChapterRedDotModel", redType = 3, functionList = {}, childrens = {}},
[4] = { name = "功能A 3级界面B", serverRedModelName = "", clientRedModelName = "ChallengeRewardRedDotModel", redType = 2, functionList = {}, childrens = {}},
[5] = { name = "功能B 1级界面", serverRedModelName = "", clientRedModelName = "", redType = -1, functionList = {}, childrens = {6}},
[6] = { name = "功能B 2级界面", serverRedModelName = "DayTaskFinishRedDotModel", clientRedModelName = "", redType = 1, functionList = {}, childrens = {}},
}
function RedDotManager:init(serverRedModels)
---@type RedDotModel[]
self.redModels = {}
self.bindList = {}
---初始化红点列表
for id, v in pairs(redDotConfig) do
local redModel
if v.clientRedModelName and v.clientRedModelName ~= "" then
local redId = id
printError(v.clientRedModelName)
---客户端红点需要重写一个继承RedDotModel的脚本,处理红点显示逻辑
local redModelClas = require("models.redDot."..v.clientRedModelName)
redModel = redModelClas.new(id, v)
else
redModel = RedDotModel.new(id, v)
end
self.redModels[id] = redModel
end
local leafRedModes = {} --叶节点红点model列表
---初始父子级关系
for i,redModel in ipairs(self.redModels) do
local childs = redModel.config.childrens
if childs and next(childs) then
for _,childId in ipairs(childs) do
if self.redModels[childId] then
local childModel = self.redModels[childId]
redModel:addChild(childModel)
else
printError("子节点的配置有缺失,redId = " ..redModel.id..", childId = " .. childId)
end
end
else
table.insert(leafRedModes, redModel)
end
end
---初始化子红点方法
for i,redModel in ipairs(leafRedModes) do
redModel:onInit()
end
if serverRedModels and next(serverRedModels) then
---填充服务器数据
for i,v in ipairs(serverRedModels) do
self:updateRedModelByServerData(v)
end
end
end
---绑定红点对象
---@param targetGo GameObject
---@param redDotModel RedDotManager
function RedDotManager:bind(targetGo, redDotModel)
if self.bindList[targetGo] then
---@type RedDotBinder
local binder = self.bindList[targetGo]
binder:unBind()
binder:onDestroy()
end
---@type RedDotBinder
local binder = RedDotBinder.new(targetGo, redDotModel)
self.bindList[targetGo] = binder
end
function RedDotManager:unBind(targetGo)
if self.bindList[targetGo] then
---@type RedDotBinder
local binder = self.bindList[targetGo]
binder:unBind()
end
end
---通关服务器数据刷新红点
function RedDotManager:updateRedModelByServerData(serverRedData)
local redId = serverRedData.redId
if self.redModels[redId] then
local isShow = serverRedData.isShow
local num = isShow and 1 or 0
self.redModels[redId]:setNum(num)
end
end
---获取红点数据
---@return RedDotModel
function RedDotManager:getRedModelById(redId)
if self.redModels[redId] then
return self.redModels[redId]
end
printError("没有找到红点数据,红点Id="..redId)
end
事件名称
eventTypes = {
ChallengeChapterUnlock = "ChallengeChapterUnlock",
ChallengeChapterFinish = "ChallengeChapterFinish",
}
事件管理器
EventManager = {}
function EventManager:init()
self.eventListenList_ = {}
self:addEvent("APP_ENTER_FOREGROUND_EVENT", self, self.EnterBack)
self.sdkEventList = {}
end
function EventManager:addEvent(eventName, target, handler)
if eventName == nil then
return
end
if type(eventName) == "table" then
for _, subEventName in pairs(eventName) do
if type(subEventName) == "number" then
subEventName = tostring(subEventName)
end
self:addEvent_(subEventName, target, handler)
end
elseif type(eventName) == "string" then
self:addEvent_(eventName, target, handler)
elseif type(eventName) == "number" then
self:addEvent_(tostring(eventName), target, handler)
else
print("uncall " .. eventName)
end
end
function EventManager:addEvent_(eventName, target, handler)
local eventTable = self.eventListenList_[eventName] or {}
for _, eventHandle in pairs(eventTable) do
if eventHandle.target == target then
eventHandle.handler = handler
return
end
end
table.insert(eventTable, { target = target, handler = handler })
self.eventListenList_[eventName] = eventTable
end
function EventManager:pushEvent(eventName, ...)
if self.eventListenList_ then
local eventsTabel = self.eventListenList_[eventName]
if eventsTabel then
local args = { ... }
for index, hanlderItem in pairs(eventsTabel) do
if hanlderItem.target and hanlderItem.handler then
xpcall(function()
hanlderItem.handler(hanlderItem.target, eventName,unpack(args))
end,function(err)
printError(err)
end)
end
end
end
end
end
function EventManager:removeEventByName(eventName)
self.eventListenList_[eventName] = {}
end
function EventManager:removeEventByTarget(target)
if target then
for name, eventTable in pairs(self.eventListenList_) do
for index, handler in pairs(eventTable) do
if handler.target == target then
eventTable[index] = nil
--清理没有监听的事件@libin
if not next(eventTable) then
self.eventListenList_[name] = nil
break
end
end
end
end
end
end
---@param target table
---@param eventName string
function EventManager:removeEvent(target, eventName)
local eventslist = self.eventListenList_[eventName]
if eventslist then
for index, eventTable in pairs(eventslist) do
if eventTable.target == target then
eventslist[index] = nil
break
end
end
end
end
function EventManager:dump()
--debugLuaObj(self.eventListenList_,"ddd",true)
for eventName, tbl in pairs(self.eventListenList_) do
local eventsTabel = self.eventListenList_[eventName]
if eventsTabel and not table_is_empty(eventsTabel) then
print("eventName: [" .. eventName .. "]")
for index, hanlderItem in pairs(eventsTabel) do
if hanlderItem.target and hanlderItem.target.name then
print(hanlderItem.target.name)
else
local obj = hanlderItem.target
local t = type(obj)
local mt
if t == "table" then
mt = getmetatable(obj)
elseif t == "userdata" then
-- mt = tolua.getpeer(obj)
end
if mt then
print(mt.__cname)
else
--print("mt == null")
local key = table.keyof(_G, obj)
if key then
print(key)
else
dump(obj, "", 3)
end
end
end
--if hanlderItem.handler then
-- print("hanlderItem.handler")
-- print(hanlderItem.handler)
--end
print("_______________________________________")
end
end
end
end
function EventManager:destory(...)
self.eventListenList_ = {}
_G["EventManager"] = nil
end
EventManager:init()
return EventManager
红点绑定关系处理类
RedDotBinder = class("RedDotBinder")
---@class RedDotBinder
local RedDotBinder = RedDotBinder
function RedDotBinder:ctor(targetGo, redDotModel)
---@type GameObject
self.targetGo = targetGo
--self.targetImage = self.targetGo:GetComponent(typeof(Image))
self:bind(redDotModel)
end
---绑定红点数量变化事件
---@param redModel RedDotModel
function RedDotBinder:bind(redModel)
self:unBind()
---@type RedDotModel
self.redDotModel = redModel
self.redDotModel:addCallback(self.targetGo, function()
self:onRedNumChange()
end)
self:onRedNumChange() --初始化显示刷新
end
function RedDotBinder:unBind()
if self.redDotModel then
self.redDotModel:removeCallback(self.targetGo)
end
end
---红点数量变化时候需要处理的内容
function RedDotBinder:onRedNumChange()
self:refreshRedShow()
end
---刷新红点显示
function RedDotBinder:refreshRedShow()
local redNum = self.redDotModel:getNum()
local isShow = redNum > 0
self.targetGo:SetActive(isShow)
if isShow then --刷新红点显示的图片
--如果是父节点类型红点,需要获取所有显示的叶子节点,根据优先级进行排序,筛选出需要显示的红点类型
self:collectEndChild(self.redDotModel, true)
table.sort(self.collectList, function (a, b)
if a.config.redType < b.config.redType then
return a.id < b.id
else
return a.config.redType < b.config.redType
end
end)
---根据优先级最高的红点类型,刷新红点image需要显示的图片
local redShowType = self.collectList[1].config.redType
local spriteId
if redShowType == RedDotShowType.Reward then
spriteId = 10001
elseif redShowType == RedDotShowType.New then
spriteId = 10002
else
spriteId = 10001
end
print("显示红点图片:"..spriteId)
-- self.targetImage.sprite = ResourceManage.getSpriteById(spriteId)
end
end
---获取红点显示类型
---@return RedDotShowType
function RedDotBinder:getRedShowType()
local redType = RedDotShowType.Normal
if next(self.redDotModel.childs) then --存在子红点
local visibleRedList = {}
else --不存在子红点
redType = self.redDotModel.config.redType
end
return redType
end
---获取所有红点num大于0的叶子节点对象
---@param redModel RedDotModel
---@param isRoot boolean
function RedDotBinder:collectEndChild(redModel, isRoot)
if isRoot then
self.collectList = {}
end
if next(redModel.childs) then --存在子节点
for i,v in ipairs(redModel.childs) do
self:collectEndChild(v)
end
else
if redModel:getNum() > 0 then
table.insert(self.collectList, redModel)
end
end
end
function RedDotBinder:onDestroy()
end
具体实现逻辑的红点类
ChallengeNewChapterRedDotModel = class("ChallengeNewChapterRedDotModel", RedDotModel)
---@class ChallengeNewChapterRedDotModel:RedDotModel
local ChallengeNewChapterRedDotModel = ChallengeNewChapterRedDotModel
-- function ChallengeNewChapterRedDotModel:ctor(id, cfg)
-- RedDotModel.ctor(self, id, cfg)
-- end
function ChallengeNewChapterRedDotModel:onInit()
EventManager:addEvent(eventTypes.ChallengeChapterUnlock,self, self.check)
--添加监听事件,处理红点刷新
self.isHaveNewChapter = true
self:check()
end
function ChallengeNewChapterRedDotModel:check()
---判断是否有没有查看过的新章节
local num = self.isHaveNewChapter and 1 or 0
self:setNum(num)
self.isHaveNewChapter = not self.isHaveNewChapter
end
function ChallengeNewChapterRedDotModel:onDestroy()
--移除监听事件
EventManager:removeEventByTarget(self)
end
return ChallengeNewChapterRedDotModel
ChallengeRewardRedDotModel = class("ChallengeRewardRedDotModel", RedDotModel)
---@class ChallengeRewardRedDotModel:RedDotModel
local ChallengeRewardRedDotModel = ChallengeRewardRedDotModel
function ChallengeRewardRedDotModel:onInit()
EventManager:addEvent(eventTypes.ChallengeChapterFinish, self, self.check)
self.isHaveRewards = true
--添加监听事件,处理红点刷新
self:check()
end
function ChallengeRewardRedDotModel:check()
---判断是否有没有需要领取的章节奖励
local num = self.isHaveRewards and 1 or 0
self:setNum(num)
self.isHaveRewards = not self.isHaveRewards
end
function ChallengeRewardRedDotModel:onDestroy()
--移除监听事件
EventManager:removeEventByTarget(self)
end
return ChallengeRewardRedDotModel
红点显示状态测试
创建红点对象
function testRedModle(...)
local gob = UnityEngine.GameObject.New()
gob.name = "1级界面";
gob.transform.position = UnityEngine.Vector3.New(1,1,1)
local redModel = RedDotManager:getRedModelById(1)
RedDotManager:bind(gob, redModel)
local gob2 = UnityEngine.GameObject.New()
gob.transform.position = UnityEngine.Vector3.New(11,1,1)
gob2.name = "2级界面";
local redModel2 = RedDotManager:getRedModelById(2)
RedDotManager:bind(gob2, redModel2)
local gob3 = UnityEngine.GameObject.New()
gob3.name = "3级界面";
gob.transform.position = UnityEngine.Vector3.New(31,1,1)
local redModel3 = RedDotManager:getRedModelById(3)
RedDotManager:bind(gob3, redModel3)
local gob4 = UnityEngine.GameObject.New()
gob4.name = "4级界面";
gob.transform.position = UnityEngine.Vector3.New(41,1,1)
local redModel4 = RedDotManager:getRedModelById(4)
RedDotManager:bind(gob4, redModel4)
end
模拟状态变化时候,发送事件刷新显示
EventManager:pushEvent(eventTypes.ChallengeChapterFinish)
EventManager:pushEvent(eventTypes.ChallengeChapterUnlock)