一、效果展示
| 连线视频挂件 | PK 视频挂件 |
| | |
二、准备工作
在开始调整视频挂件前,请先参考 主播开播 和 观众观看 完成主流程的搭建。
三、核心原理
LiveCoreView 通过 VideoViewAdapter 代理支持自定义视图渲染。当业务场景发生变化(例如有人上麦、开始 PK )时,LiveCoreView 会调用代理方法询问应该显示的视图。开发者实现对应的接口方法并返回自定义的 View 实例即可。
| 方法 | 描述 | 对应业务场景 |
| createCoGuestView | 创建观众连麦的挂件视图。 | 观众连麦、邀请上麦 |
| createCoHostView | 创建跨房连麦(主播连线)的挂件视图。 | 主播连线 |
| createBattleView | 创建 PK 场景中单个用户的挂件视图(例如头像、分数)。 | 主播 PK |
| createBattleContainerView | 创建 PK 场景的整体容器视图(例如背景、分数 PK 条)。 | 主播 PK |
四、调整连线场景挂件
当观众与主播进行连线或者主播间跨房连线时,直播间界面会从单人模式切换为多人布局。此时,需要在连线的视频窗口上展示特定的用户信息,例如昵称、等级或“静音”状态提示,以便区分不同麦位。
五、适用场景
·主播和观众视频连麦时,修改麦位上的用户信息(例如昵称、等级、静音图标)。
·主播与主播跨房连线时,修改对方主播的显示样式(例如昵称、等级、静音图标)。
·修改无视频画面时的默认背景(例如占位头像)。
·修改空麦位时的视图实现。
七、实现步骤
注意:说明:
注意:你也可以参考 TUILiveKit 开源项目中的 CoGuestWidgets 和 CoHostWidgets 目录下文件来了解完整的实现逻辑。
八、步骤 1. 准备自定义挂件 View
定义三个基础视图类,分别用于展示用户信息、空麦位提示和无视频流时的背景。
创建自定义的用户信息视图:
import android.content.Contextimport android.view.Viewimport android.widget.FrameLayoutimport android.widget.ImageViewimport android.widget.TextViewclass CustomInfoView(context: Context, name: String, isMuted: Boolean) : FrameLayout(context) { private val nameTextView = TextView(context) private val muteIcon = ImageView(context) init { nameTextView.text = name muteIcon.visibility = if (isMuted) View.VISIBLE else View.GONE addView(nameTextView) addView(muteIcon) // 此处省略具体布局参数设置代码 }}
创建自定义的空麦位视图:
import android.content.Contextimport android.widget.FrameLayoutimport android.widget.ImageViewimport android.widget.TextViewclass EmptySeatView(context: Context) : FrameLayout(context) { private val addIcon = ImageView(context) private val addLabel = TextView(context) init { addLabel.text = "邀请连线" // 此处省略图片资源加载和布局参数设置代码 }}
创建自定义无视频流时的头像占位视图:
import android.content.Contextimport android.widget.FrameLayoutimport android.widget.ImageViewclass CustomAvatarView(context: Context, avatarURL: String) : FrameLayout(context) { private val avatarImageView = ImageView(context) init { // 实际项目中可使用 Glide 等图片加载库加载 avatarURL addView(avatarImageView) // 此处省略具体布局参数设置代码 }}
九、步骤 2. 实现 Adapter 逻辑
创建一个适配器类,实现 VideoViewAdapter 接口的方法 createCoGuestView (观众连线)和 createCoHostView (主播连线),返回刚才创建的自定义视图。
实现 VideoViewAdapter 中的 createCoGuestView 方法,返回观众连线视频挂件。
import android.content.Contextimport android.view.Viewimport io.trtc.tuikit.atomicxcore.api.device.DeviceStatusimport io.trtc.tuikit.atomicxcore.api.live.SeatInfoimport io.trtc.tuikit.atomicxcore.api.view.VideoViewAdapterimport io.trtc.tuikit.atomicxcore.api.view.ViewLayerclass VideoWidgetAdapter(private val context: Context) : VideoViewAdapter { override fun createCoGuestView(seatInfo: SeatInfo, viewLayer: ViewLayer): View? { val isUserOnSeat = seatInfo.userInfo.userID.isNotEmpty() return when (viewLayer) { ViewLayer.FOREGROUND -> { if (isUserOnSeat) { // 非空麦位,返回自定义的前景视图 CustomInfoView(context, seatInfo.userInfo.userName, seatInfo.userInfo.microphoneStatus == DeviceStatus.OFF) } else { // 空麦位,返回自定义空麦位视图 EmptySeatView(context) } } ViewLayer.BACKGROUND -> { if (isUserOnSeat) { // 返回自定义的背景视图(未开摄像头时显示) CustomAvatarView(context, seatInfo.userInfo.avatarURL) } else { null } } } }}
实现 VideoViewAdapter 中的 createCoHostView 方法,返回主播连线的视频挂件。
import android.content.Contextimport android.view.Viewimport io.trtc.tuikit.atomicxcore.api.device.DeviceStatusimport io.trtc.tuikit.atomicxcore.api.live.SeatInfoimport io.trtc.tuikit.atomicxcore.api.view.VideoViewAdapterimport io.trtc.tuikit.atomicxcore.api.view.ViewLayerclass VideoWidgetAdapter(private val context: Context) : VideoViewAdapter { override fun createCoHostView(seatInfo: SeatInfo, viewLayer: ViewLayer): View? { val isUserOnSeat = seatInfo.userInfo.userID.isNotEmpty() return when (viewLayer) { ViewLayer.FOREGROUND -> { if (isUserOnSeat) { // 返回自定义的前景视图,可以返回与观众连线不同的样式 CustomInfoView(context, seatInfo.userInfo.userName, seatInfo.userInfo.microphoneStatus == DeviceStatus.OFF) } else { // 返回自定义空麦位视图,可以返回与观众连线不同的样式 EmptySeatView(context) } } ViewLayer.BACKGROUND -> { if (isUserOnSeat) { // 返回自定义的背景视图(未开摄像头时显示),可以返回与观众连线不同的样式 CustomAvatarView(context, seatInfo.userInfo.avatarURL) } else { null } } } }}
十、参数说明:
| 参数 | 类型 | 说明 |
| seatInfo | SeatInfo | 麦位信息对象,包含麦上用户的详细信息。 |
| seatInfo.userInfo.userName | String | 麦上用户的昵称。 |
| seatInfo.userInfo.avatarURL | String | 麦上用户的头像 URL。 |
| seatInfo.userInfo.microphoneStatus | DeviceStatus | 麦上用户的麦克风状态。 |
| seatInfo.userInfo.cameraStatus | DeviceStatus | 麦上用户的摄像头状态。 |
| viewLayer | ViewLayer | 视图层级枚举。.foreground 表示前景挂件视图,始终显示在视频画面的最上层。.background 表示背景挂件视图,位于前景视图下层,仅在对应用户没有视频流(例如未开摄像头)的情况下显示,通常用于展示用户的默认头像或占位图。 |
十一、调整主播 PK 场景挂件
PK 是直播中互动性最强的环节。在 PK 过程中,画面通常根据参与人数被分割为几部分。开发者需要在画面上方或中间区域添加 PK 专用的 UI ,分数 PK 条、每个麦位上的分数显示、倒计时动画或“VS”特效图标,以营造紧张的竞技氛围。
十二、视图层级示意
注意:说明:
注意:PK 功能依赖于连线功能,所以需要先在主播连线的情况下才能发起 PK 请求。
十四、步骤 1. 准备自定义 UI 组件
PK 场景通常需要两类视图:
·单人挂件:显示在每个主播窗口上(例如分数胶囊)。
·全局容器:覆盖在整体画面上(例如 VS 动画、倒计时)。
注意:说明:
注意:你也可以参考 TUILiveKit 开源项目中的 BattleWidgets 目录下文件来了解完整的实现逻辑。
import android.content.Contextimport android.widget.FrameLayoutimport android.widget.ImageView// 示例:单人分数条class MyBattleScoreView(context: Context) : FrameLayout(context) { // 内部实现分数显示逻辑}// 示例:全局 VS 面板class MyBattleContainer(context: Context) : FrameLayout(context) { private val battleTimeView = ImageView(context) // 内部实现倒计时和 VS 动画逻辑}
十五、步骤 2. 实现 Adapter 逻辑
在之前创建的 VideoWidgetAdapter 中,实现剩余的 PK 视图方法。
class VideoWidgetAdapter(private val context: Context) : VideoViewAdapter { // 省略 createCoGuestView / createCoHostView 实现… // 1. 创建【PK 单人信息】挂件 (显示在每个主播视频上方) override fun createBattleView(seatInfo: SeatInfo): View? { return MyBattleScoreView(context) } // 2. 创建【PK 全局容器】挂件 (显示在整个视频区域上方) override fun createBattleContainerView(): View? { return MyBattleContainer(context) }}
十六、集成与生效(重要)
这是最关键的一步。你需要将实现了代理逻辑的 VideoWidgetAdapter 注入到直播核心流程中。
主播端集成:需在初始化外层容器前,先初始化 LiveCoreView 并通过 setVideoViewAdapter 设置适配器。
import android.os.Bundleimport androidx.appcompat.app.AppCompatActivityimport android.view.ViewGroupimport com.trtc.uikit.livekit.features.anchorview.AnchorViewimport io.trtc.tuikit.atomicxcore.api.live.LiveInfoimport io.trtc.tuikit.atomicxcore.api.view.LiveCoreViewclass AnchorActivity : AppCompatActivity() { private lateinit var anchorView: AnchorView private lateinit var widgetAdapter: VideoWidgetAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 初始化适配器对象 widgetAdapter = VideoWidgetAdapter(this) // 假设此处已获取 liveInfo val liveInfo = LiveInfo() // 1. 仅传入 Context 初始化 LiveCoreView val videoView = LiveCoreView(this) // 2. 调用 setVideoViewAdapter 传入自定义适配器 videoView.setVideoViewAdapter(widgetAdapter) // 3. 将核心视频组件传入外层容器 anchorView = AnchorView(this, liveInfo, videoView) setContentView(anchorView, ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT )) }}
观众端集成:观众端通过 AudienceContainerViewDelegate 来回调核心视图的创建时机。
import android.os.Bundleimport androidx.appcompat.app.AppCompatActivityimport io.trtc.tuikit.atomicxcore.api.live.LiveInfoimport io.trtc.tuikit.atomicxcore.api.view.LiveCoreViewclass AudienceActivity : AppCompatActivity(), AudienceContainerViewDelegate { private lateinit var audienceView: AudienceContainerView private lateinit var widgetAdapter: VideoWidgetAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) widgetAdapter = VideoWidgetAdapter(this) val roomId = "your_room_id" audienceView = AudienceContainerView(this, roomId) audienceView.setDelegate(this) setContentView(audienceView) } override fun onCreateCoreView(liveInfo: LiveInfo): LiveCoreView? { val view = LiveCoreView(this) // 注入适配器 view.setVideoViewAdapter(widgetAdapter) return view }}
十七、进阶:获取实时业务数据
在开发 PK 等复杂场景时,你会发现 SeatInfo 只提供了基础麦位信息。如果你需要获取实时倒计时、PK 分数等业务数据,就需要在你的自定义视图中对接 AtomicXCore 中的核心数据。
| Store/Component | 功能描述 | API 文档 |
| CoGuestStore | 观众连线数据:已连线用户列表、邀请列表,申请列表等。 | API 文档 |
| CoHostStore | 主播连线数据:已连线用户列表、邀请列表,申请列表等。 | API 文档 |
| BattleStore | PK 数据:当前 PK 信息、PK 用户列表、PK 分数列表。 | API 文档 |
十九、设置了适配器,但自定义视图没有显示?
请检查设置时机是否太晚。在 Android 的组件装载链路中,LiveCoreView 的适配器赋值必须在它被传入 AnchorView 之前完成。如果 AnchorView 已经完成了初始化,它内部会直接加载 SDK 的默认挂件视图。
解决:请务必遵循严格的初始化顺序:先 new LiveCoreView > 再调用 setVideoViewAdapter 注入自定义 UI > 最后实例化 AnchorView 并传入配置好的 CoreView。
20、我只想修改“连麦”视图,“PK”视图想保留默认的,怎么办?
VideoViewAdapter 是一种完全接管模式。一旦你设置了自定义的代理,SDK 默认的 AnchorView/AudienceView 内部实现的代理逻辑就会完全失效。
解决:你需要在你的代理类中实现所有相关方法。对于你不想修改的部分(例如 PK 视图),你可以:
1. 复制源码:找到 LiveKit 源码中默认的实现类,在你的代理中返回这些默认类的实例。
2. 手动实现:参考默认样式,快速写一个类似的简单视图返回。
21、自定义挂件显示了,但无法点击?
这是 Android 经典的事件分发冲突问题。由于前景视图(.FOREGROUND)叠加在视频渲染层之上:
1. 请确保你返回的 CustomView 设置了 isClickable = true 或注册了 setOnClickListener。
2. 如果你的挂件是一个嵌套的 ViewGroup(例如 FrameLayout),请检查其父容器是否错误地拦截了触摸事件(例如被外层控件触发了 onInterceptTouchEvent 且返回了 true)。