观众核心页面(Android)

最近更新时间:2026-05-13 15:53:53

我的收藏
AudienceView 观众端核心 UI 组件。通过该组件,开发者可以快速搭建基础的观众直播界面。本文档将按照从基础按钮调整到复杂视图替换的顺序,指导开发者完成观众端界面的按需定制。
页面结构示意图
页面结构示意图


准备工作

在开始调整观众端界面前,请先参考 观众观看 完成观众进房的主流程搭建。

功能概览

与主播端直接创建并使用视图的结构不同,观众端由于需要支持上下滑动无缝切换直播间,AudienceView 是一个滑动容器。
在观众端,界面定制不是直接在 AudienceView 上进行的,而是作用于它内部的单房间视图 AudienceLiveView。滑动容器在工作时会动态创建和展示这些单房间视图,开发者需要通过 AudienceViewListener 协议,在以下核心生命周期回调中获取到对应房间的视图实例(即回调参数中的 liveView ),并在正确的时机对其进行定制与资源管理:
方法
描述
onCreateLiveView(audienceView: AudienceLiveView, liveInfo: LiveInfo)
视图创建回调。此回调会向外传递刚刚实例化的 AudienceLiveView
适用于提前确定的静态样式定制。由于容器会提前预加载下个房间,该回调触发时视图可能尚未显示在屏幕上。通常在这里通过 liveView 执行替换内置组件、配置固定按钮等一次性布局操作。
onLiveViewDidAppear(audienceView: AudienceLiveView, liveInfo: LiveInfo)
视图展示回调。此回调会向外传递当前真正显示在屏幕上的 AudienceLiveView
适用于依赖实时状态或信令的动态调整。通常在这里记录当前活跃的 liveView 实例,以便在收到业务信令(如商品上架通知)时,精准定向更新当前屏幕上的 UI;或在此处开启房间专属的定时器与动效。
onLiveViewDidDisappear(audienceView: AudienceLiveView, liveInfo: LiveInfo)
视图隐藏回调。当观众滑出该房间或关闭界面时触发。
适用于状态重置与资源清理。通常在这里清空活跃的 liveView 记录,销毁在该房间内弹出的自定义业务面板、停止动效或清理定时器。
单房间视图 AudienceLiveView 提供的核心自定义接口与属性如下:
方法/属性
描述
topRightItems
用于灵活配置直播间右上角的按钮集合,支持自由添加自定义按钮或调整内置按钮的布局。
bottomItems
用于灵活配置直播间底部的按钮集合,支持自由添加自定义按钮或调整内置按钮的布局。
replace(node: AudienceNode, view: View?)
用于将指定位置的默认组件(如顶部信息区、底部操作栏),替换为开发者自定义的全新视图。
overlayView
专属挂件图层,方便开发者自由添加需要在视频画面上方悬浮展示的全局业务 UI。
perform(action:AudienceAction)
用于在自定义视图中直接触发内置默认逻辑,例如显示默认观众列表,显示默认连麦管理面板等。

快速开始

下面示例快速搭建一个带货直播间观看页。主要流程包括:
隐藏不需要的连麦按钮并添加购物车按钮。
模拟接收商品上架通知并弹出商品卡片。
最后将点击事件路由至商品列表页。
import android.os.Bundle
import android.view.Gravity
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import com.trtc.uikit.livekit.features.audienceview.AudienceLiveView
import com.trtc.uikit.livekit.features.audienceview.AudienceView
import com.trtc.uikit.livekit.features.audienceview.AudienceViewDefine
import com.trtc.uikit.livekit.features.audienceview.AudienceViewDefine.AudienceBottomItem
import io.trtc.tuikit.atomicx.common.util.ScreenUtil.dip2px
import io.trtc.tuikit.atomicxcore.api.live.LiveInfo

class AudienceActivity : AppCompatActivity(), AudienceViewDefine.AudienceViewListener {
private var currentLiveView: AudienceLiveView? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 初始化容器 View
val audienceView = AudienceView(this)
setContentView(audienceView)
// 注册监听并初始化
audienceView.addListener(this)
audienceView.init(this, "your_room_id")
}

// 业务方法:展示商品列表页
private fun showProductListPanel() {
println("展示商品列表页...")
}

// 业务方法:模拟收到 IM 商品推送通知
fun onReceiveProductPushMessage() {
// 必须作用于当前正在展示的 active 视图
val liveView = currentLiveView ?: return

val productCard = AudienceProductCardView(this)
productCard.setOnClickListener { showProductListPanel() }

val params = FrameLayout.LayoutParams(dip2px(150f), FrameLayout.LayoutParams.WRAP_CONTENT).apply {
gravity = Gravity.BOTTOM or Gravity.END
rightMargin = dip2px(12f)
bottomMargin = dip2px(100f)
}
liveView.overlayView.addView(productCard, params)
}

// --- AudienceViewListener 回调实现 ---
override fun onCreateLiveView(liveView: AudienceLiveView, liveInfo: LiveInfo) {
// 配置底部操作栏:隐藏连麦功能,并添加自定义“购物车”按钮
val shopCartBtn = ImageView(this).apply {
setOnClickListener { showProductListPanel() }
}

liveView.bottomItems = listOf(
AudienceBottomItem.Gift,
AudienceBottomItem.Like,
AudienceBottomItem.Custom(shopCartBtn)
)
}

override fun onLiveViewDidAppear(liveView: AudienceLiveView, liveInfo: LiveInfo) {
currentLiveView = liveView
}

override fun onLiveViewDidDisappear(liveView: AudienceLiveView, liveInfo: LiveInfo) {
if (currentLiveView === liveView) {
currentLiveView = null
}
}
override fun onLiveEnded(roomId: String, ownerName: String, ownerAvatarUrl: String) {}
override fun onClickFloatWindow() {}
}

class AudienceProductCardView(context: android.content.Context) : android.view.View(context) {
// 自定义的商品卡片
}

调整底部操作按钮

底部工具栏是观众进行互动的核心区域。目前默认提供礼物、连麦、点赞、更多等按钮。开发者可通过 bottomItems 属性灵活增删内置功能,或通过 AudienceBottomItem.Custom(View) 插入自定义按钮。


实现步骤

步骤 1:准备自定义按钮视图。按需创建自定义按钮视图对象。
步骤 2:更新底部按钮数组。实现 AudienceViewListener 回调,在相应的回调方法中把组装好的包含 AudienceBottomItem 枚举的数组重新赋值给视图属性。
override fun onCreateLiveView(audienceView: AudienceLiveView, liveInfo: LiveInfo) {
// 步骤 1:准备自定义按钮视图
val shopButton = ImageView(audienceView.context).apply {
setImageResource(R.drawable.shop_cart)
}
// 步骤 2:更新底部按钮数组,保留送礼,隐藏连麦,新增商品按钮
audienceView.bottomItems = listOf(
AudienceBottomItem.Gift,
AudienceBottomItem.Custom(shopButton)
)
}

调整顶部操作按钮

顶部区域展示房间信息与关键操作。默认提供观众人数、悬浮窗、退出三个按钮。开发者可通过 topRightItems 属性精简或增加控制按钮。


实现步骤

步骤 1:准备自定义按钮视图。按需创建自定义按钮视图对象。
步骤 2:更新顶部按钮数组。实现 AudienceViewListener 回调,在相应的回调方法中将枚举项或自定义视图赋值给 topRightItems 属性。
override fun onCreateLiveView(audienceView: AudienceLiveView, liveInfo: LiveInfo) {
// 步骤 1:准备自定义按钮视图
val reportButton = ImageView(audienceView.context).apply {
setImageResource(R.drawable.report_btn)
}

// 步骤 2: 更新顶部按钮数组(保留人数和关闭,新增举报)
audienceView.topRightItems = listOf(
AudienceViewDefine.AudienceTopRightItem.AudienceCount,
AudienceViewDefine.AudienceTopRightItem.Custom(reportButton),
AudienceViewDefine.AudienceTopRightItem.Close
)
}

替换界面指定区域视图

当调整按钮无法满足结构性修改需求时,可使用 replace 接口整体替换指定区域的视图。内部通过 AudienceNode 枚举定义了 5 个支持替换的区域,可以结合开头的“页面结构示意图”理解:
AudienceNode
说明
LIVE_INFO
左上角的主播与房间信息展示区域。
TOP_RIGHT_BUTTONS
右上角的系统控制按钮区域。
NETWORK_INFO
网络状态指示区域。
BOTTOM_RIGHT_BAR
右下角的业务操作栏区域。
BARRAGE_INPUT
左下角的弹幕输入框触发区域。

布局规则

replace 接口会将自定义视图放入指定的区域。视图的位置由框架控制,开发者无需设置。视图的尺寸由其自身决定,推荐使用以下两种方式之一声明尺寸:

方式 1:使用内部约束链(推荐)

确保子视图的约束形成完整链条,从而自动撑开父视图。
class MyInfoView(context: Context) : FrameLayout(context) {
init {
val label = TextView(context).apply {
text = "直播中"
setPadding(dip2px(12f), dip2px(12f), dip2px(12f), dip2px(12f))
}
val params = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
addView(label, params)
}
}

方式 2:重写 onMeasure 或固定 LayoutParams

通过重写自定义 ViewonMeasure 方法,或者在初始化时赋予明确宽高的 LayoutParams 来决定视图尺寸。
class MyInfoView(context: Context) : FrameLayout(context) {
init {
// 赋予固定的宽高属性
layoutParams = LayoutParams(dip2px(200f), dip2px(44f))
setBackgroundColor(Color.DKGRAY)
}
}

实现步骤

步骤 1:创建自定义视图对象,确保符合上文所述的布局规则。
步骤 2:实现 AudienceViewListener 回调,在相应的回调方法中调用 AudienceLiveView 组件的 replace 接口,传入需要替换的节点枚举和新建的自定义视图。
override fun onCreateLiveView(liveView: AudienceLiveView, liveInfo: LiveInfo) {
// 步骤 1:初始化符合布局规则的自定义视图
val customInfoView = MyInfoView(liveView.context)
// 步骤 2:调用替换接口更新特定节点
liveView.replace(AudienceViewDefine.AudienceNode.LIVE_INFO, customInfoView)
}

绑定事件与触发逻辑

替换节点后,开发者需自行接管该视图的交互事件。可在事件回调中执行专属业务逻辑,也可以使用 perform 方法快速触发内置逻辑。支持的 AudienceAction 包括:展示礼物面板(SHOW_GIFT_PANEL)、展示观众列表(SHOW_AUDIENCE_LIST)等。

实现步骤

步骤 1:为自定义视图绑定事件,使用 setOnClickListener 或手势识别器为视图添加点击事件。
步骤 2:触发内置逻辑或执行业务代码,在事件回调中调用 perform 方法传入 AudienceAction 枚举,或执行其他业务代码。
override fun onCreateLiveView(audienceView: AudienceLiveView, liveInfo: LiveInfo) {
val btn = MyGiftButton(audienceView.context, audienceView)
audienceView.bottomItems = listOf(AudienceBottomItem.Custom(btn))
}

class MyGiftButton(context: Context, private val liveView: AudienceLiveView) : androidx.appcompat.widget.AppCompatImageView(context) {
init {
setImageResource(R.drawable.custom_gift)
setOnClickListener {
// 弹出默认礼物面板
liveView.perform(AudienceViewDefine.AudienceAction.SHOW_GIFT_PANEL)
// 或者触发自定义逻辑
// presentCustomPanel()
}
}
}

深度定制业务弹窗

当使用 perform 触发的内置默认面板无法满足具体的业务需求时,开发者可以彻底接管这部分逻辑,基于底层数据 AtomicXCore 构建全新的业务面板。

核心思路

开发者可使用底层数据接口 AtomicXCore 获取房间、用户及状态数据,完全自主地完成自定义视图的搭建与交互绑定。在完成自定义控制器的构建后,建议使用内部封装的 AtomicPopover 组件将其弹出,以确保您的自定义面板也能获得与 SDK 内置面板一致的丝滑手势拦截与平滑动画。

实现步骤

步骤 1:构建自定义业务视图。创建一个独立的 View,并在其内部引入底层核心数据接口,用于拉取或监听业务数据以驱动 UI 更新。
步骤 2:实例化弹窗容器。实例化 AtomicPopover 对象,并指定其弹出位置(BOTTOMCENTER)。
步骤 3:配置并展示自定义视图。设置弹窗的高度模式(如按屏幕比例),将您的自定义视图通过 setContent 传入容器,最后调用 show() 进行展示。
import android.content.Context
import android.graphics.Color
import android.widget.FrameLayout
import io.trtc.tuikit.atomicx.widget.basicwidget.popover.AtomicPopover

// 步骤1:构建自定义业务视图(如:观众列表)
class CustomAudienceListView(context: Context) : FrameLayout(context) {
init {
setBackgroundColor(Color.WHITE)
setupUI()
bindLiveData()
}
private fun setupUI() {
// 在此添加您的自定义 UI 控件,例如展示观众头像、用户等级等列表
}
private fun bindLiveData() {
// 使用 AtomicXCore 提供的核心接口获取当前房间状态或用户数据
// 例如:LiveAudienceStore.create(liveID)...
// 拿到核心数据后刷新上方构建的自定义 UI
}
}

// 示例场景:从屏幕底部向上滑出一个高度占屏幕 50% 的完全自定义面板
fun presentBusinessPanel(context: Context) {
// 步骤2:实例化弹窗容器,指定从底部弹出
val popover = AtomicPopover(context, AtomicPopover.PanelGravity.BOTTOM)
// 步骤3:配置并展示自定义视图
// 设置面板高度为屏幕高度的 50%(也可以使用 WrapContent 自适应高度)
popover.setPanelHeight(AtomicPopover.PanelHeight.Ratio(0.5f))
// 实例化自定义业务视图
val audienceListView = CustomAudienceListView(context)
// 将视图填入 Popover 并展示
popover.setContent(audienceListView)
popover.show()
}
请参考以下文档,使用 AtomicXCore 接口实现自定义功能面板页
功能描述
参考文档
实现观众连线管理面板:连麦申请 / 邀请 / 同意 / 拒绝,连麦成员权限控制(麦克风 / 摄像头),状态同步。
实现主播跨房连线面板:连线主播互动管理,发起 / 接受 / 拒绝连线。
实现观众列表:统计观众数量,监听观众进出事件。
实现音频特效面板:变声(童声 / 男声)、混响(KTV 等)、耳返调节,实时切换特效。
音效

添加自定义悬浮挂件

复杂的直播场景常需要在视频画面上方悬浮展示活动图标或互动贴纸。这类需要定位于视频层之上、且独立于基础布局的视图,应统一添加到 overlayView 图层中。
在实际业务中,悬浮挂件通常作为某个活动面板的入口。建议将其与前文介绍的 AtomicPopover 组件结合使用:为挂件绑定点击事件,并在触发后弹出深度定制的业务弹窗。

挂件生命周期管理

结合视图的生命周期回调,悬浮挂件的添加与管理时机取决于具体的业务需求:
常驻型挂件(例如直播间固定的活动入口):可以直接在 onCreateLiveView 回调中将其添加至 overlayView
动态型挂件(例如天降红包、临时弹出商品卡片):应在接收到业务信令后,获取 onLiveViewDidAppear 记录的当前活跃视图实例,再向其 overlayView 中动态添加控件。
挂件与弹窗销毁:对于动态弹出的业务面板或挂件,可以在 liveViewDidDisappear 回调中执行视图的 removeFromSuperview 操作。这能有效避免用户滑动切房时,因视图复用导致的 UI 状态异常。

实现步骤

步骤 1:创建挂件视图并开启交互。实例化悬浮控件,设置其尺寸与位置。
步骤 2:绑定点击事件。为挂件设置 setOnClickListener ,用于响应观众的点击操作。
步骤 3:添加至覆盖图层并联动弹窗。将挂件添加至 overlayView 中,并在点击回调中调用前文定义的自定义弹窗逻辑。
class LiveRoomActivity : AppCompatActivity() {
// 在 AudienceViewListener 中获取当前的 liveView
private var currentLiveView: AudienceLiveView? = null

// 示例场景:在画面左上角悬浮显示红包挂件,点击后联动弹出业务面板
fun addRedPacketWidget() {
val activeView = currentLiveView ?: return
// 步骤 1 & 2:创建挂件视图并开启交互
val redPacketWidget = ImageView(this).apply {
setImageResource(R.drawable.red_packet_icon)
setOnClickListener { handleRedPacketClick() }
}
val params = FrameLayout.LayoutParams(dip2px(60f), dip2px(60f)).apply {
gravity = Gravity.TOP or Gravity.START
topMargin = dip2px(120f)
leftMargin = dip2px(15f)
}
// 步骤 3:添加至覆盖图层
activeView.overlayView.addView(redPacketWidget, params)
}
private fun handleRedPacketClick() {
// 在此处调用前文写好的 presentBusinessPanel 方法,弹出深度定制的业务视图
// presentBusinessPanel(this)
}
}

常见问题

添加的自定义按钮为何点击无响应?

使用 AudienceBottomItem.Custom() 传入自定义视图后,点击视图却无法触发绑定的事件。
排查建议:
检查点击事件绑定(高频原因)Android 中默认的 View 往往不具备点击特性,请确保对传入的控件调用了 setOnClickListener ,或者在 XML 中/代码中将其 isClickable 设为了 true。
检查视图尺寸与边界:如果您的容器并没有声明具体的宽高(或没有通过子 View 撑起 WRAP_CONTENT),导致父布局尺寸为 0,即便内部子按钮画出了边界外,点击事件也会因为超出父视图 Rect 范围而被系统的 Touch 机制丢弃。
检查父级拦截:确认外部是否有一层包裹图层在 onInterceptTouchEvent 中拦截了事件。

动态隐藏或显示操作按钮?

在实际业务中,可能需要根据房间状态(例如带货期间隐藏连麦按钮),动态调整底部或顶部的工具栏。
实现方案 AudienceLiveViewbottomItemstopRightItems 属性支持响应式更新。只需组装一个新的按钮数组并重新赋值,SDK 内部会自动触发视图刷新,无需手动重绘。需要注意的是在动态更新视图时,务必确保更新的是当前正在展示的视图实例。请使用 onLiveViewDidAppear 回调中记录的活跃实例,切勿误修改处于“预加载”状态的相邻房间视图。

使用 replace 接口替换的视图在滑动时不显示?

在使用 replace(node, customView) 接口替换指定节点后,发现当开始滑动后程序抛出 illegalStateException: The specified child already has a parent... 崩溃,或者屏幕上替换的自定义视图消失了。这通常是因为开发者在不同的直播间视图中,错误地复用了同一个自定义 View 实例。
原因剖析:
由于 AudienceView 是一个滑动容器,为了保证滑动的流畅性,系统会提前预加载接下来的相邻房间。这意味着 onCreateLiveView 代理方法会被提前调用。在 Android 中,一个 View 实例同一时刻只能拥有一个 Parent。如果您传入了一个共享的全局 View 实例,当预加载下一个房间时,将其 addView 到新房间前,如果不手动 removeView 会直接导致崩溃;即使框架底层处理了强制拔出,也会导致该 View 从当前可视房间被剥离,从而引发显示异常。
错误示例(请避免这样使用)
// ❌ 错误做法:在外部持有一个共享的视图实例
val sharedBrandView = MyBrandView(context)

override fun onCreateLiveView(audienceView: AudienceLiveView, liveInfo: LiveInfo) {
// ⚠️ 警告:当预加载下一个 liveView 时,复用同一个 View 实例会导致 Parent 冲突!
audienceView.replace(AudienceNode.LIVE_INFO, sharedBrandView)
}
正确做法
请确保在 onCreateLiveView 回调中,每次都为新的 AudienceLiveView 创建一个全新的自定义视图实例。
override fun onCreateLiveView(audienceView: AudienceLiveView, liveInfo: LiveInfo) {
// ✅ 正确做法:每次触发回调时,都实例化一个全新的自定义视图
val newBrandView = MyBrandView(audienceView.context)
audienceView.replace(AudienceNode.LIVE_INFO, newBrandView)
}