引言
在直播间的互动场景中,礼物的托盘展示不仅是活跃氛围的重要手段,也是用户体验的关键环节。随着礼物类型的多样化,例如需要连续展示的连击礼物和一次性送出多个的数字礼物,礼物托盘的设计需要同时满足实时性、流畅性和视觉效果。
为了优化礼物的展示效率,我们采用了双托盘轨道的设计。与传统的单一托盘不同,双托盘通过动态分配,依据空闲状态来决定由哪个托盘展示礼物,这种设计不仅提升了并发情况下的展示性能,还保证了用户能够及时开到赠送的礼物效果。
本篇博客将会详细解析双托盘的实现原理,包括礼物的分配逻辑、托盘的协作机制,以及如何通过合理的动画设计和性能优化,打造兼顾美观与实用的礼物展示体验。
礼物托盘设计概述
礼物托盘是直播间礼物展示的核心组件,主要用于展示用户的小礼物送出效果,往往还会伴随着一些特殊效果,比如连击,数字礼物或者暴击等等。有的时候还会作为大礼物动画的降级策略。因此礼物托盘在直播间中尤为重要。为了兼顾实时性和流畅性,同时满足多样化礼物类型的展示需求,我们选择了双托盘轨道设计。以下是托盘设计的关键点:
双托盘设计初衷:
- 动态分配:在并发赠送礼物的场景中,单托盘可能出现堆积或者延迟的问题,双托盘通过动态分配空闲托盘,可以显著提升礼物展示的效率。
- 实时性保障:任何一个托盘都可以展示任意类型的礼物,不限制礼物类型,避免复杂场景下的逻辑耦合。
UI布局:
基本信息展示:
- 礼物预览图片:显示礼物的样式。
- 用户信息:显示赠送这的昵称头像等等。
- 礼物数量:显示礼物赠送个数,或者当前连击数量。
- 托盘位置:两个托盘一上一下,避免重叠。
动画效果
托盘会从屏幕左侧滑出,并显示数量动画,当同一个礼物展示完成后,再由左侧滑入。
礼物的分配与队列管理
所有的需要展示礼物托盘的数据由一个线程安全的队列来管理,当收到礼物消息时,礼物消息进入队列。然后在根据礼物托盘的空闲状态分配优先级更高且空闲的托盘来执行显示礼物的操作。
override func receiveIMMessage(_ message: MWIMMessage) {
// 礼物消息
if let giftMessage = message.data as? MWGiftMessage {
// 构建托盘数据模型
let giftTrayModel = MWGiftTrayModel(giftMessage: giftMessage)
// MWLogHelper.debug("收到礼物消息 开始展示托盘进队列",context: "MWGiftTrayShowModule")
.....
var isExist = false
....
// 添加到队列
if isExist == false {
addTrayModelToQueue(giftTrayModel: giftTrayModel)
}
}
} else {
addTrayModelToQueue(giftTrayModel: giftTrayModel)
}
// 如果当前没有正在显示的礼物 执行展示
if downGiftTrayView.giftTrayModel == nil {
startShowDownGiftTray()
}
if upGiftTrayView.giftTrayModel == nil {
startShowUpGiftTray()
}
}
}
连击礼物
连击礼物用于同一用户赠送同一礼物将会产生连击效果,相同的连击礼物会有相同的连击id。由于连击操作的存在,使得接收到礼物消息时的处理变得略微复杂。大概分为三种情况:
- 当收到的礼物为非连击礼物,或者是连礼物但当前没有相同的连击id时,直接进入队列。
- 当收到的礼物为连击礼物,且当前正在显示的托盘中有相同的连击id,则礼物不需要进入队列,直接在对应托盘更新。
- 当收到礼物为连击礼物,当前正在等待播放的队列中有相同的连击id,则礼物添加到对应礼物的连击队列。
完整代码如下:
override func receiveIMMessage(_ message: MWIMMessage) {
// 礼物消息
if let giftMessage = message.data as? MWGiftMessage {
// 构建托盘数据模型
let giftTrayModel = MWGiftTrayModel(giftMessage: giftMessage)
// MWLogHelper.debug("收到礼物消息 开始展示托盘进队列",context: "MWGiftTrayShowModule")
// 1.判断 是否有连击id
// 2.判断 是否当前正在显示的礼物是否与新礼物id的连击id相同。
// 3.判断队列中是否有相同的连击礼物id,如果有怎么办?
// 4.本身礼物就有很多数量
// 5.那个队列总数少添加到哪个队列
// 1. 获取连击id
if let comboId = giftMessage.comboId,comboId != 0 {
//2. 判断正在显示的礼物连击id是否与新礼物id的连击id相同
if comboId == downGiftTrayView.comboId {
MWLogHelper.debug("收到与下托盘相同的连击id",context: "MWGiftTrayShowModule")
// 与下托盘相同 直接给下托盘赋值
loadGiftPreviewImage(trayModel: giftTrayModel, type: .down)
} else if comboId == upGiftTrayView.comboId {
MWLogHelper.debug("收到与上托盘相同的连击id",context: "MWGiftTrayShowModule")
// 与上托盘相同 直接给上托盘赋值
loadGiftPreviewImage(trayModel: giftTrayModel, type: .up)
} else {
//3. 判断队列中是否有相同的连击礼物id
// 判断下队列中是否有相同的comboId 添加到同一个连击id的队列中
var isExist = false
giftTrayModelQueue.enumerateObjectsUsingBlock { trayModel in
if let trayModel = trayModel as? MWGiftTrayModel, trayModel.comboId == comboId {
trayModel.animationQueue.enqueue(animation: giftTrayModel)
isExist = true
}
}
// 添加到队列
if isExist == false {
addTrayModelToQueue(giftTrayModel: giftTrayModel)
}
}
} else {
addTrayModelToQueue(giftTrayModel: giftTrayModel)
}
// 如果当前没有正在显示的礼物 执行展示
if downGiftTrayView.giftTrayModel == nil {
startShowDownGiftTray()
}
if upGiftTrayView.giftTrayModel == nil {
startShowUpGiftTray()
}
}
}
数字礼物
关于数字礼物,就是送礼时可以选择礼物个数,那么礼物会携带一个count的字段,比如礼物的个数为88,那么该礼物托盘展示后,将会从1一直累计到88。这个效果相对容易一些,我们不需要从礼物进队时开始着手考虑它,只需要在托盘展示时,让托盘执行数字动画,当动画完成后回调表示托盘已显示完成,可以显示下一个托盘数据即可。
具体实现如下:
/// 开始渲染下一个
@objc func startRenderNext() {
if count < totalCount {
self.count += 1
self.render()
self.perform(#selector(startRenderNext), with: nil, afterDelay: 0.3)
} else {
MWLogHelper.debug("数字结束",context: "MWGiftTrayCountView")
/// 延迟2秒
self.perform(#selector(dismissAnimation), with: nil, afterDelay: 2.0)
}
}
/// 结束动画
@objc func dismissAnimation() {
MWLogHelper.debug("数字结束动画",context: "MWGiftTrayCountView")
self.count = 1
self.totalCount = 0
self.countEndAnimationBlock?()
}
托盘展示
当我们从队列中取出需要在托盘上渲染的数据模型时,还是执行展示托盘的操作,展示托盘分为两个流程,首先需要加载需要展示的预览图片资源。当资源加载完成后,切换回主线程使用构建的托盘数据直接渲染托盘。
加载礼物预览图:
/// 加载礼物预览图
/// - Parameter trayModel: 托盘数据
/// - Parameter type:托盘类型0下 1上
private func loadGiftPreviewImage(trayModel: MWGiftTrayModel,type:MWGiftTrayType = .down) {
// MWLogHelper.debug("开始加载预览礼物 \(type)",context: "MWGiftTrayShowModule")
if let yyImage = trayModel.yyImage {
MWLogHelper.debug("直接展示托盘",context: "MWGiftTrayShowModule")
// 直接展示
DispatchQueue.main.async {
self.showGiftTray(trayModel: trayModel,type: type)
}
return
}
// 新模型 需要加载
let giftId = trayModel.giftId
MWGiftLoader.shared.loadMsgImage(giftId: giftId) {[weak self] data in
guard let self = self else {
return
}
let yyImage = YYImage(data: data ?? Data())
trayModel.yyImage = yyImage
DispatchQueue.main.async {
self.showGiftTray(trayModel: trayModel,type: type)
}
}
}
展示和渲染托盘:
/// 展示托盘
/// - Parameter trayModel: 托盘数据
/// - Parameter yyImage: 动图
private func showGiftTray(trayModel: MWGiftTrayModel, type:MWGiftTrayType = .down) {
// MWLogHelper.debug("开始渲染托盘",context: "MWGiftTrayShowModule")
var giftTrayView:MWGiftTrayView!
if type == .down {
giftTrayView = downGiftTrayView
} else {
giftTrayView = upGiftTrayView
}
giftTrayView.render(giftTrayModel: trayModel)
}
托盘动画
托盘由我们实现的动画分为两类,第一类是托盘的入场以及出场动画,第二类是托盘的数字展示动画。动画的实现并不复杂,但是在动画结束后的处理稍微有一点繁琐。
在渲染托盘数据,开始执行动画时,仍然需要分成两种类型,连击礼物,以及数字礼物。连击礼物有很多需要注意的细节。比如是同一个连击id的不同礼物,或者是当前礼物已经累计了一些连击。我们就先从连击礼物说起。
连击礼物
连击礼物相对复杂一下,需要考虑当前礼物的连击id是否与正在显示的礼物相同,还需要考虑当前连击礼物是否已经堆积多个连击,具体代码实现如下:
/// 赋值
/// - Parameters:
/// - giftTrayModel: 礼物托盘模型
func render(giftTrayModel: MWGiftTrayModel) {
if giftTrayModel.comboId > 0 && giftTrayModel.count == 1 {
// 如果啊,这个礼物本身就带着队列
if !giftTrayModel.animationQueue.isEmpty() {
MWLogHelper.debug("赋值礼物带队列 count \(giftTrayModel.animationQueue.count())",context: "MWGiftTrayView")
// 遍历
while let model = giftTrayModel.animationQueue.dequeue() {
animationQueue.enqueue(animation: model)
}
}
// 如果啊,如果已经有一个礼物在显示,那么就加入队列
if let _ = self.giftTrayModel {
MWLogHelper.debug("加入队列",context: "MWGiftTrayView")
animationQueue.enqueue(animation: giftTrayModel)
nextAnimation()
return
}
MWLogHelper.debug("连击礼物主动播放", context: "MWGiftTrayView")
private_render_combo(giftTrayModel: giftTrayModel)
} else {
....
}
}
- 判断当前礼物的连击id大于0,且礼物个数为1则认为是连击礼物。
- 判断该连击礼物是否已经堆积多个连击,如果已经堆积了则遍历加入到托盘的连击礼物队列。
- 判断该连击礼物是否与正在显示的连击礼物id相同,如果是也直接加入到连击队列,并直接执行动画。
- 开始渲染连击礼物。
接下里就开始渲染连击礼物,并执行连击托盘的动画,如果动画从未执行过,则会有一个从左到右的动画。并且将在0.3秒来执行下一个动画。直到队列中没有需要连击的礼物,则停留2秒后消失,在这2秒内,如果有相同的礼物连击id礼物进入,则取消延迟,重复上述流程。
我们会将上述流程分为三个方法:
渲染连击礼物数据:
/// 渲染连击礼物数据
/// - Parameters:
func private_render_combo(giftTrayModel: MWGiftTrayModel) {
self.giftTrayModel = giftTrayModel
guard let giftMessage = giftTrayModel.giftMessage else {
return
}
// 图片
if giftImageView.image != giftTrayModel.yyImage {
giftImageView.image = giftTrayModel.yyImage
}
//头像
let url = giftMessage.portaitUrl ?? ""
avatarImageView.sd_setImage(with: URL(string: url.validResourceUrl()))
// 昵称
nickNameLabel.text = giftMessage.nickname
/// 礼物个数
let count = giftMessage.number ?? 1
countView.setCount(count: count)
// 礼物id
if let giftId = giftMessage.giftId {
if let giftModel = MWGiftPoolManager.shared.giftPoolMap[giftId] {
// 礼物名称
giftNameLabel.text = giftModel.currentDesc?.title
}
}
self.layoutIfNeeded()
self.setNeedsLayout()
showComoAnimation()
}
执行连击动画:
/// 显示连击动画
func showComoAnimation() {
MWLogHelper.debug("显示连击动画",context: "MWGiftTrayView")
self.isHidden = false
if state == .endAnimating || state == .hidden {
state = .startAnimating
UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseInOut) {
self.transform = CGAffineTransform(translationX: self.bounds.width, y: 0)
} completion: { [self] _ in
self.state = .showing
}
}
/// 0.3秒执行下一个
self.perform(#selector(nextAnimation), with: nil, afterDelay: 0.3)
}
读取下一个:
/// 连击读取 读下一个
@objc private func nextAnimation() {
if let giftTrayModel = animationQueue.dequeue() as? MWGiftTrayModel {
self.giftTrayModel = giftTrayModel
self.private_render_combo(giftTrayModel: giftTrayModel)
// 取消倒计时
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(dismissAnimation), object: nil)
return
}
state = .prepareEnd
MWLogHelper.debug("连击队列播放完毕",context: "MWGiftTrayView")
// 重新倒计时消失
self.perform(#selector(dismissAnimation), with: nil, afterDelay: 2.0)
}
数字礼物
而数字礼物相对简单一些,我们只需要将托盘数据渲染给托盘,然后执行托盘的入场动画,并启动count视图的倒计时动画。
而到count达到最大数值时,标记动画结束,回调给托盘,托盘执行隐藏动画操作。
渲染礼物数据:
/// 渲染普通礼物数据&数字礼物
/// - Parameters:
/// - giftTrayModel: 礼物托盘模型
/// - count: 礼物个数
func private_render_count(giftTrayModel: MWGiftTrayModel) {
self.giftTrayModel = giftTrayModel
guard let giftMessage = giftTrayModel.giftMessage else {
return
}
// 图片
if giftImageView.image != giftTrayModel.yyImage {
giftImageView.image = giftTrayModel.yyImage
}
//头像
let url = giftMessage.portaitUrl ?? ""
avatarImageView.sd_setImage(with: URL(string: url.validResourceUrl()))
// 昵称
nickNameLabel.text = giftMessage.nickname
/// 礼物个数
countView.setTotalCount(totalCount: giftTrayModel.count)
// 礼物id
if let giftId = giftMessage.giftId {
if let giftModel = MWGiftPoolManager.shared.giftPoolMap[giftId] {
// 礼物名称
giftNameLabel.text = giftModel.currentDesc?.title
}
}
self.layoutIfNeeded()
self.setNeedsLayout()
showCountAnimation()
}
显示数字动画:
/// 显示数字动画
func showCountAnimation() {
MWLogHelper.debug("显示数字动画",context: "MWGiftTrayView")
self.isHidden = false
state = .startAnimating
UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseInOut) {
self.transform = CGAffineTransform(translationX: self.bounds.width, y: 0)
} completion: { _ in
self.state = .showing
self.countView.startRenderNext()
}
}
数字礼物的数字动画达到指定个数之后会执行回调,告诉托盘执行隐藏动画:
// 数字动画结束回调
countView.countEndAnimationBlock = { [weak self] in
self?.dismissAnimation()
}
/// 隐藏动画
@objc func dismissAnimation() {
MWLogHelper.debug("隐藏动画",context: "MWGiftTrayView")
giftTrayModel = nil
self.state = .endAnimating
UIView.animate(withDuration: 0.05, delay: 0.0, options: .curveEaseInOut) {
self.transform = .identity
} completion: { _ in
self.isHidden = true
self.transform = .identity
self.state = .hidden
self.giftShowComplete?()
}
}
结语
礼物托盘是直播间中连接用户互动与视觉体验的关键组件。通过双托盘的设计,我们成功解决了并发礼物展示中的性能瓶颈,同时保证了连击礼物与数字礼物的动态展示效果。通过合理的分配逻辑与状态管理,礼物展示变得更加流畅、实时,为用户提供了更好的交互体验。
在未来,我们可以进一步优化托盘的展示效果,例如加入更多动画细节、支持更复杂的礼物类型,以及结合用户行为数据实现智能化的礼物优先级展示。礼物托盘的设计不仅是技术上的挑战,更是提升直播间氛围与用户参与感的重要一步。希望这些经验能为你的开发工作提供一些启发!
本篇博客,篇幅较长,如果有叙述不清晰的地方,大家可以留言或者私信。
标签:giftTrayModel,动画,连击,直播间,self,托盘,礼物 From: https://blog.csdn.net/weixin_39339407/article/details/144372177