首页 > 其他分享 >直播间互动升级:双轨道礼物托盘的展示与连击

直播间互动升级:双轨道礼物托盘的展示与连击

时间:2024-12-14 17:00:01浏览次数:8  
标签:giftTrayModel 动画 连击 直播间 self 托盘 礼物

引言

在直播间的互动场景中,礼物的托盘展示不仅是活跃氛围的重要手段,也是用户体验的关键环节。随着礼物类型的多样化,例如需要连续展示的连击礼物和一次性送出多个的数字礼物,礼物托盘的设计需要同时满足实时性、流畅性和视觉效果。

为了优化礼物的展示效率,我们采用了双托盘轨道的设计。与传统的单一托盘不同,双托盘通过动态分配,依据空闲状态来决定由哪个托盘展示礼物,这种设计不仅提升了并发情况下的展示性能,还保证了用户能够及时开到赠送的礼物效果。

本篇博客将会详细解析双托盘的实现原理,包括礼物的分配逻辑、托盘的协作机制,以及如何通过合理的动画设计和性能优化,打造兼顾美观与实用的礼物展示体验。

礼物托盘设计概述

礼物托盘是直播间礼物展示的核心组件,主要用于展示用户的小礼物送出效果,往往还会伴随着一些特殊效果,比如连击,数字礼物或者暴击等等。有的时候还会作为大礼物动画的降级策略。因此礼物托盘在直播间中尤为重要。为了兼顾实时性和流畅性,同时满足多样化礼物类型的展示需求,我们选择了双托盘轨道设计。以下是托盘设计的关键点:

双托盘设计初衷:

  • 动态分配:在并发赠送礼物的场景中,单托盘可能出现堆积或者延迟的问题,双托盘通过动态分配空闲托盘,可以显著提升礼物展示的效率。
  • 实时性保障:任何一个托盘都可以展示任意类型的礼物,不限制礼物类型,避免复杂场景下的逻辑耦合。

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。由于连击操作的存在,使得接收到礼物消息时的处理变得略微复杂。大概分为三种情况:

  1. 当收到的礼物为非连击礼物,或者是连礼物但当前没有相同的连击id时,直接进入队列。
  2. 当收到的礼物为连击礼物,且当前正在显示的托盘中有相同的连击id,则礼物不需要进入队列,直接在对应托盘更新。
  3. 当收到礼物为连击礼物,当前正在等待播放的队列中有相同的连击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 {
            .... 
        }
     }
  1. 判断当前礼物的连击id大于0,且礼物个数为1则认为是连击礼物。
  2. 判断该连击礼物是否已经堆积多个连击,如果已经堆积了则遍历加入到托盘的连击礼物队列。
  3. 判断该连击礼物是否与正在显示的连击礼物id相同,如果是也直接加入到连击队列,并直接执行动画。
  4. 开始渲染连击礼物。

接下里就开始渲染连击礼物,并执行连击托盘的动画,如果动画从未执行过,则会有一个从左到右的动画。并且将在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

相关文章

  • 双十二李佳琦直播间佣金曝光,是系统漏洞还是精心策划?
    刚刚过去的双十二,李佳琦直播间的佣金比例意外曝光,引发了广泛关注。据透露,每套化妆品的佣金比例在10%到20%之间。我们暂且不讨论此佣金比例是否合理,而是聚焦于一个问题:这次佣金比例的显示,究竟是意外,还是背后另有隐情?佣金曝光:是失误还是刻意为之?此次佣金比例的曝光,引发了外界......
  • 如何把直播间的用户变成你的好友
    IM即时通信(IinstantMessaging,lM)基于QQ底层IM能力开发,仅需植入SDK即可轻松集成聊天、会话、群组、资料管理能力,帮助您实现文字、图片、短语音、短视频等富媒体消息收发,全面满足通信需要。APP一般具备以下功能I赛事直播功能,比分预测,比赛赛事,比赛数据,赛事社区,视频,新闻......
  • 直播间氛围提升工具:音效助手
    概述在直播领域,创造一个活跃和吸引人的氛围是至关重要的。今天,我们将介绍一款专为直播场景设计的音效辅助工具——音效助手。这款工具以其丰富的音效库和便捷的操作界面,成为了许多主播的得力助手。视频演示功能介绍音效库音效助手拥有一个庞大的音效库,涵盖了各种流行......
  • Qt托盘消息通知(③托盘消息列表)
    实现思路创建消息窗口:使用 QWidget 或 QDialog 来创建一个显示消息的窗口。使用 QListWidget:在消息窗口中使用 QListWidget 来动态显示消息。添加滑块:如果消息数量超过5条,使用 QScrollArea 来实现滑动功能。更新消息列表:每次接收到新的消息时更新消息列表。完整代......
  • Qt托盘消息通知(①托盘图标)
    创建托盘图标工程的思路步骤创建Qt项目:首先,您需要创建一个新的QtWidgets应用程序项目。添加必要的Qt模块:在项目文件(.pro)中添加对 QtWidgets 和 QtGui 模块的支持。创建主窗口类:我们将创建一个主窗口类来处理托盘图标的创建和管理。设置托盘图标:使用 QSystemTrayIcon ......
  • Qt托盘消息通知(②托盘消息通知)
    实现思路准备消息内容:我们需要定义通知的标题和内容。调用 showMessage 方法:使用 showMessage 方法在托盘区域显示通知。设置通知图标:可以选择不同的图标来表示不同类型的消息(信息、警告、错误等)。完整代码示例在之前的托盘图标工程基础上,我们将添加消息通知的功能。m......
  • Qt实现系统托盘消息
    实现思路创建主应用程序:使用 QApplication 作为应用程序的基础。创建系统托盘图标:使用 QSystemTrayIcon 来显示图标在系统托盘中。添加右键菜单:为托盘图标添加右键菜单,允许用户选择退出应用程序。显示新消息:使用 QTimer 定期触发显示消息,模拟新消息到达的情况。处理槽......
  • tauri2.x+vue3实践篇|封装多窗口|tauri2.0自定义托盘闪烁消息提示+右键菜单
    最近一直在捣鼓Tauri2.0跨平台框架,之前也有分享几篇tauri1.x实例项目。相较于1.0,tauri2.x框架api有了比较多的变更,而且支持创建android/ios移动端应用。实现类似QQ托盘闪烁消息提醒及右键菜单。框架信息"@tauri-apps/api":">=2.0.0-rc.0","@tauri-apps/cli":">=......
  • 批量获取抖音直播间弹幕数据api:实时弹幕评论数据
    抖音作为当下最热门的短视频平台之一,拥有庞大的用户群体和活跃度,为电商行业带来了巨大的商业机会。抖音商品详情接口作为连接抖音平台和电商系统的关键纽带,具有重要的作用。本文将深入探讨抖音商品详情接口在电商行业中的重要性,并介绍如何通过代码实现实时数据获取,帮助电商企业更好......
  • QT QSystemTrayIcon创建系统托盘区图标失败
    前言在开发个人项目时,需要在Windows系统托盘区创建一个图标,在代码中,我使用的是QT的QSystemTrayIcon类进行图标创建,但是在加上图片资源后,一直没有图标显现。我使用的是Qt6,Windows11系统。示例代码QSystemTrayIcon*trayIcon=newQSystemTrayIcon(this);trayIco......