Kotlin+协程+FLow+Channel+Compose 实现一个直播多个弹幕效果

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

一、前言

相信做过直播的朋友,都对弹幕有一点了解。弹幕一般分两块:

  1. 第一块是直播服务器推送过来的弹幕,这些弹幕是其他人发送到直播服务器,直播服务器推送到客户端的,客户端需要展示。
  2. 第二块是本地发送弹幕,只要发送成功了,本地立马可以展示发送的这条弹幕,本地自己发送的弹幕是可以单独处理样式以来区分的。

本文我们来简单介绍下: 通过Kotlin+协程+FLow+Channel+Compose 实现一个直播多个弹幕效果的简单实现逻辑。 实现逻辑总共分为4个核心模块:

  1. 数据模型定义。
  2. 数据仓库管理
  3. 视图逻辑处理
  4. Compose实现UI渲染

下面我们来看怎么具体实现:

二、数据模型定义

如下代码:

  1. 数据模型定义主要包含:
  2. 弹幕ID
  3. 弹幕文字内容
  4. 弹幕颜色
  5. 弹幕字体大小
  6. 弹幕移动速度
  7. 弹幕初始在右侧X需要减去的偏移值
  8. 弹幕在Y坐标下显示的位置
  9. 用来区分本地弹幕和网络弹幕
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层:

  1. 本Demo通过模拟服务端推送实现弹幕,本地消息通过用户输入触发。所有消息弹幕都具有随机位置、大小、颜色和速度特性,然后通过合并数据源通过Flow进行收集 ,通过StateFlow更新UI界面数据
  2. 采用定时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渲染

  1. UI渲染,采用 Compose的Canvas的drawText方法来 绘制弹幕
  2. 弹幕左右平滑移动动画主要使用到drawText中Offset中的X值偏移,从绘制弹幕控件 宽度乘以动态偏移值比例(item.offsetX),这个值item.offsetX从1逐渐变到0,那么弹幕移动也就从右边逐渐移动到左边。
  3. 这里,如果我们想要实现视频加弹幕,或者直播画面加弹幕,只需要把直播或者视频显示的控件放到 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 实现一个直播多个弹幕效果:该实现包含四个关键设计

  1. 采用双Channel分别处理服务端和本地消息;
  2. 通过Koltin中的Flow来合并流实现统一数据管道;
  3. 通过 协程里面: delay(16) 来保证60fps动画引擎保证平滑移动3;
  4. 通过Compose的Canvas绘制来保证高性能渲染支持大量弹幕
  5. 其实很简单,没有什么复杂逻辑

感谢阅读:

欢迎用你发财的小手 关注,点赞、收藏

这里你会学到不一样的东西

相关推荐
SimonKing1 小时前
拯救大文件上传:一文彻底彻底搞懂秒传、断点续传以及分片上传
java·后端·架构
数据智能老司机1 小时前
Linux内核编程——网络驱动程序
linux·架构·操作系统
vivo互联网技术1 小时前
号码生成系统的创新实践:游戏周周乐幸运码设计
redis·后端·架构
移动开发者1号1 小时前
ReLinker优化So库加载指南
android·kotlin
刘龙超1 小时前
如何应对 Android 面试官 -> 玩转 JetPack LiveData
android jetpack
山野万里__1 小时前
C++与Java内存共享技术:跨平台与跨语言实现指南
android·java·c++·笔记
Huckings1 小时前
Android 性能问题
android
Java技术小馆1 小时前
POST为什么发送两次请求
java·面试·架构
百度Geek说2 小时前
搜索数据建设系列之数据架构重构
数据仓库·重构·架构·spark·dubbo
DemonAvenger2 小时前
Go内存压力测试:模拟与应对高负载的技术文章
性能优化·架构·go