Kotlin+协程+FLow+Channel+Compose 实现一个直播多个弹幕效果,原来如此简单

一、前言
相信做过直播的朋友,都对弹幕有一点了解。弹幕一般分两块:
- 第一块是直播服务器推送过来的弹幕,这些弹幕是其他人发送到直播服务器,直播服务器推送到客户端的,客户端需要展示。
- 第二块是本地发送弹幕,只要发送成功了,本地立马可以展示发送的这条弹幕,本地自己发送的弹幕是可以单独处理样式以来区分的。
本文我们来简单介绍下: 通过Kotlin+协程+FLow+Channel+Compose 实现一个直播多个弹幕效果的简单实现逻辑。
实现逻辑总共分为4个核心模块:
- 数据模型定义。
- 数据仓库管理
- 视图逻辑处理
- Compose实现UI渲染
下面我们来看怎么具体实现:
二、数据模型定义
如下代码:
- 数据模型定义主要包含:
- 弹幕ID
- 弹幕文字内容
- 弹幕颜色
- 弹幕字体大小
- 弹幕移动速度
- 弹幕初始在右侧X需要减去的偏移值
- 弹幕在Y坐标下显示的位置
- 用来区分本地弹幕和网络弹幕
kotlin
data class DanmuItem(
val id: Long = System.currentTimeMillis(),//弹幕ID
val text: String,//弹幕文字内容
val color: Color = randomColor(), //弹幕颜色随机
val fontSize: TextUnit = (18 + Random.nextInt(16)).sp,//弹幕字体大小
val speed: Float = 1f + Random.nextFloat() * 2f,
val offsetX: Float = 1f, // 初始在右侧
val offsetY: Float = Random.nextFloat() * 0.8f,
val isLocal: Boolean = false
) {
companion object {
fun randomColor() = Color(
red = Random.nextFloat(), green = Random.nextFloat(), blue = Random.nextFloat(), alpha = 1f
)
}
}
三、数据仓库管理
为了区分服务端弹幕和本地弹幕采用双Channel分别处理服务端和本地消息,合并流实现统一数据管道
kotlin
class DanmuRepository {
private val serverChannel = Channel<DanmuItem>(Channel.UNLIMITED)
private val localChannel = Channel<DanmuItem>(Channel.UNLIMITED)
suspend fun emitServerDanmu(item: DanmuItem) {
serverChannel.send(item.copy(isLocal = false))
}
suspend fun emitLocalDanmu(item: DanmuItem) {
localChannel.send(item.copy(isLocal = true))
}
fun getDanmuStream(): Flow<DanmuItem> {
return merge(serverChannel.consumeAsFlow(), localChannel.consumeAsFlow())
}
}
四、视图逻辑处理
这个在ViewModel层:
- 本Demo通过模拟服务端推送实现弹幕,本地消息通过用户输入触发。所有消息弹幕都具有随机位置、大小、颜色和速度特性,然后通过合并数据源通过Flow进行收集 ,通过StateFlow更新UI界面数据
- 采用定时60fps动画(
协程里面: delay(16)
) 引擎保证平滑移动
kotlin
class DanmuViewModel : ViewModel() {
private val repo = DanmuRepository()
private val _danmus = MutableStateFlow<List<DanmuItem>>(emptyList())
val danmus: StateFlow<List<DanmuItem>> = _danmus
init {
// 模拟服务端推送
viewModelScope.launch {
while (true) {
delay((300 + Random.nextInt(100)).toLong())
repo.emitServerDanmu(DanmuItem(text = randomDanmuText()))
}
}
// 合并数据源
viewModelScope.launch {
repo.getDanmuStream().collect { newDanmu ->
_danmus.update { it + newDanmu }
}
}
// 弹幕动画
viewModelScope.launch {
while (true) {
delay(16)
_danmus.update { current ->
current.map {
it.copy(offsetX = it.offsetX - 0.002f * it.speed)
}.filter { it.offsetX > -0.5f }
}
}
}
}
//发送本地弹幕
fun sendLocalDanmu(text: String) {
viewModelScope.launch {
repo.emitLocalDanmu(DanmuItem(text = text))
}
}
//模拟服务端弹幕集
fun randomDanmuText(): String {
val greetings = listOf("来了来了", "前排", "打卡", "签到", "第一!", "强到没朋友")
val reactions = listOf("666", "太强了", "哈哈哈", "awsl", "好活", "泪目")
val questions = listOf("有人吗?", "这是哪?", "主播多大了?", "几点开播?")
val emotes = listOf("(≧∇≦)ノ", "(╯‵□′)╯", "╮(╯▽╰)╭", "(❤ ω ❤)", "( ̄▽ ̄*)ゞ")
val memes = listOf("一键三连", "下次一定", "白嫖", "老板大气", "感谢飞机")
return when ((1..5).random()) {
1 -> greetings.random()
2 -> reactions.random() + listOf("", "!!", "~").random()
3 -> questions.random()
4 -> emotes.random() + " " + reactions.random()
else -> memes.random() + listOf("", "!", "!!!").random()
}
}
}
五、Compose实现UI渲染
- UI渲染,采用
Compose的Canvas的drawText方法来 绘制弹幕
, - 弹幕左右平滑移动动画主要使用到drawText中Offset中的X值偏移,从绘制弹幕控件
宽度乘以动态偏移值比例(item.offsetX)
,这个值item.offsetX
从1逐渐变到0,那么弹幕移动也就从右边逐渐移动到左边。 - 这里,如果我们想要实现
视频加弹幕
,或者直播画面加弹幕
,只需要把直播或者视频显示的控件放到BoxWithConstraints
里面就可以了。从它的名字上也可以看出,它内部实现的就是一个Box控件
,然后把其他控件包含在里面就可以了。其实我们不用它,自己来用Box控件
来包起来也可以。
ini
@Composable
fun DanmuScreen(viewModel: DanmuViewModel = DanmuViewModel()) {
val danmus by viewModel.danmus.collectAsState()
var inputText by remember { mutableStateOf("") }
val textLayout = rememberTextMeasurer()
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
Canvas(modifier = Modifier.fillMaxSize()) {
danmus.forEach { item ->
val text = textLayout.measure(
text = AnnotatedString(item.text), style = TextStyle(
color = item.color,
fontSize = item.fontSize,
)
)
drawText(text, topLeft = Offset(size.width * item.offsetX, size.height * item.offsetY))
}
}
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(16.dp)
) {
OutlinedTextField(
value = inputText, onValueChange = { inputText = it }, modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
viewModel.sendLocalDanmu(inputText)
inputText = ""
}) {
Text("发送弹幕")
}
}
}
}
六、总结
本文简单通过Kotlin+协程+FLow+Channel+Compose 实现一个直播多个弹幕效果:该实现包含四个关键设计
- 采用双Channel分别处理服务端和本地消息;
- 通过Koltin中的Flow来合并流实现统一数据管道;
- 通过
协程里面: delay(16)
来保证60fps动画引擎保证平滑移动3; - 通过Compose的Canvas绘制来保证高性能渲染支持大量弹幕
- 其实很简单,没有什么复杂逻辑