当前位置: 首页 > news >正文

uniapp小程序实现弹幕不重叠

uniapp小程序实现弹幕不重叠

1、在父组件中引入弹幕组件

<template><!-- 弹幕 --><barrage ref="barrage" class="barrage-content" @reloadDanmu="reloadDanmu"></barrage>
</template>
<script>import barrage from './components/barrage.vue'import {getBarrageListApi} from '@/api/voteApi.js'export default {components: {barrage},data() {return {danmuList: [], // 弹幕列表danmuContion: { // 弹幕查询条件page: 1,size: 200}},onLoad(){this.getBarrageList()},methods: {async getBarrageList(isInit) {try {let res = await getBarrageListApi(this.danmuContion)let resData = (res && res.data) || {}let list = Array.isArray(resData.records) ? resData.records : []list.map((item) => {item.color = '#fff'item.timestampt = new Date().getTime()item.image = {head: {src: item.avatarUrl,width: 44,height: 44}, // 弹幕头部添加图片gap: 8 // 图片与文本间隔}item.content = `{${item.nickname}} 已为《${item.voteName}》投下宝贵的一票`})let danmuLength = this.danmuList.lengththis.danmuList = listthis.addBarrage(isInit || danmuLength === 0)} catch (e) {uni.showToast({title: (e && e.message) || '查询弹幕列表失败',icon: 'none',during: 2000})}},addBarrage(isInit) {if (!isInit || !this.danmuList.length) {return}const barrageComp = this.$refs && this.$refs.barrage || {}barrageComp.getBarrageInstance({duration: 15, // 弹幕动画时长 (移动 1500px 所需时长)lineHeight: 2.4, // 弹幕行高padding: [0, 0, 0, 0], // 弹幕区四周留白alpha: 1, // 全局透明度font: '10px PingFang SC', // 全局字体range: [0, 1], // 弹幕显示的垂直范围,支持两个值。[0,1]表示弹幕整个随机分布,tunnelShow: false, // 显示轨道线tunnelMaxNum: 200, // 隧道最大缓冲长度maxLength: 5000, // 弹幕最大字节长度,汉字算双字节safeGap: 20, // 发送时的安全间隔enableTap: false, // 点击弹幕停止动画高亮显示danmuList: this.danmuList})},async reloadDanmu(type) {const barrageComp = this.$refs && this.$refs.barrage || {}if(type === 'addDanmu') {await this.getBarrageList(false)barrageComp.open()barrageComp.addData(this.danmuList)return}await this.getBarrageList(true)}, }
</script><style lang="less" scoped>.barrage-conten {width: 100%;height: 156rpx;position: absolute;top: 192rpx;box-sizing: border-box;}
</style>

2、弹幕组件

 <template><view class="barrage-area" :style="{'opacity': alpha, 'font-size': fontSize*2 + 'rpx', 'padding': padding}"><block v-for="(tunnel, tunnelId) in tunnels" :key="tunnelId"><view class="barrage-tunnel":style="{'height': tunnel.height*2 + 'rpx', 'border-top-width': (tunnelShow ? 1 : 0) + 'px'}"><view class="tunnel-tips" :style="{'display': !tunnelShow ? 'none' : 'block'}">轨道{{tunnelId}}</view><block v-for="(bullet, bulletId) in tunnel.bullets" :key="bullet.timestampt + bulletId"><view :data-tunnelid="{tunnelId}" :data-bulletid="{bulletId}":class="['bullet-item', bullet.duration > 0 ? 'bullet-move' : '', bullet.paused ? 'paused' : '']":style="{'color': bullet.paused ? '#fff' : bullet.color, 'line-height': tunnel.height*2 + 'rpx', 'animation-duration': bullet.duration + 's', 'animation-play-state': bullet.paused ? 'paused' : 'running'}"@animationend="onAnimationend" @tap="onTapBullet"><image class="bullet-item_img" v-if="bullet.image && bullet.image.head":style="{'width': bullet.image.head.width + 'rpx', 'height': bullet.image.head.height + 'rpx'}"mode="aspectFill" :src="bullet.image.head.src"></image><view class="bullet-item_text":style="{'margin':'0 ' + (bullet.image && bullet.image.gap || 0) + 'rpx', opacity: 1}"><text>{{bullet.content}}</text></view></view></block></view></block></view>
</template><script>export default {data() {return {fontSize: 10, // 字体大小,单位pxwidth: 375, // 弹幕区域宽度height: 80, // 弹幕区域高度duration: 15, // 弹幕动画时长lineHeight: 3, // 弹幕行高padding: [0, 0, 0, 0], // 弹幕区四周留白alpha: 1, // 全局透明度font: '10px PingFang SC', // 全局字体range: [0, 1], // 弹幕显示的垂直范围,支持两个值。[0,1]表示弹幕整个随机分布,tunnelShow: false, // 显示轨道线tunnelMaxNum: 200, // 轨道最大缓冲长度maxLength: 5000, // 弹幕最大字节长度,汉字算双字节safeGap: 20, // 发送时的安全间隔enableTap: false, // 点击弹幕停止动画高亮显示tunnelHeight: 0,tunnelNum: 0,tunnels: [],idleTunnels: null,enableTunnels: {},distance: 1500, // 移动距离, 单位pxsystemInfo: {},danmuList: []};},methods: {init() {this.fontSize = this.getFontSize(this.font)this.idleTunnels = new Set()this.enableTunnels = new Set()this.tunnels = []this.availableHeight = (this.height - this.padding[0] - this.padding[2])this.tunnelHeight = this.fontSize * this.lineHeight// 轨道行数 = 弹幕区域高度/(单个弹幕高度+下边距)this.tunnelNum = Math.floor(this.availableHeight / (this.tunnelHeight + 15))// tunnel(轨道)class Tunnel {constructor(opt = {}) {const defaultTunnelOpt = {tunnelId: 0,height: 0, // 轨道高度width: 0, // 轨道宽度safeGap: 4, // 相邻弹幕安全间隔maxNum: 10, // 缓冲队列长度bullets: [], // 弹幕last: -1, // 上一条发送的弹幕序号bulletStatus: [], // 0 空闲,1 占用中disabled: false, // 禁用中sending: false, // 弹幕正在发送}Object.assign(this, defaultTunnelOpt, opt)this.bulletStatus = new Array(this.maxNum).fill(0)class Bullet {constructor(opt = {}) {this.bulletId = opt.bulletId}/*** image 结构* {*   head: {src, width, height},*   gap: 4 // 图片与文本间隔* }*/addContent(opt = {}) {const defaultBulletOpt = {duration: 0, // 动画时长passtime: 0, // 弹幕穿越右边界耗时content: '', // 文本color: '#000000', // 默认黑色width: 0, // 弹幕宽度height: 0, // 弹幕高度image: {}, // 图片paused: false // 是否暂停}Object.assign(this, defaultBulletOpt, opt)}removeContent() {this.addContent({})}}for (let i = 0; i < this.maxNum; i++) {this.bullets.push(new Bullet({bulletId: i,}))}}disable() {this.disabled = truethis.last = -1this.sending = falsethis.bulletStatus = new Array(this.maxNum).fill(1)this.bullets.forEach(bullet => bullet.removeContent())}enable() {if (this.disabled) {this.bulletStatus = new Array(this.maxNum).fill(0)}this.disabled = false}clear() {this.last = -1this.sending = falsethis.bulletStatus = new Array(this.maxNum).fill(0)this.bullets.forEach(bullet => bullet.removeContent())}getIdleBulletIdx() {return this.bulletStatus.indexOf(0)}getIdleBulletNum() {let count = 0this.bulletStatus.forEach(status => {if (status === 0) count++})return count}addBullet(opt) {if (this.disabled) returnconst idx = this.getIdleBulletIdx()if (idx >= 0) {this.bulletStatus[idx] = 1this.bullets[idx].addContent(opt)}}removeBullet(bulletId) {if (this.disabled) returnthis.bulletStatus[bulletId] = 0const bullet = this.bullets[bulletId]bullet.removeContent()}}for (let i = 0; i < this.tunnelNum; i++) {this.idleTunnels.add(i) // 空闲的轨道id集合this.enableTunnels.add(i) // 可用的轨道id集合this.tunnels.push(new Tunnel({ // 轨道集合width: this.width,height: this.tunnelHeight,safeGap: this.safeGap,maxNum: this.tunnelMaxNum,tunnelId: i,}))}// 筛选符合范围的轨道this.setRange()},resize() {const query = uni.createSelectorQuery().in(this)query.select('.barrage-area').boundingClientRect((res) => {res = res || {}let systemInfo = uni.getSystemInfoSync()this.systemInfo = systemInfo || {}this.width = res.width || systemInfo.windowWidththis.height = res.height || 300const isActive = this._isActivethis.close(() => {this.init()if (isActive) {setTimeout(() => {this.open()}, 2000)}})}).exec()},// 设置显示范围 range: [0,1]setRange(range) {range = range || this.rangeconst top = range[0] * this.tunnelNumconst bottom = range[1] * this.tunnelNum// 释放符合要求的轨道// 找到目前空闲的轨道const idleTunnels = new Set()const enableTunnels = new Set()this.tunnels.forEach((tunnel, tunnelId) => {if (tunnelId >= top && tunnelId < bottom) {const disabled = tunnel.disabledtunnel.enable()enableTunnels.add(tunnelId)if (disabled || this.idleTunnels.has(tunnelId)) {idleTunnels.add(tunnelId)}} else {tunnel.disable()}})this.idleTunnels = idleTunnelsthis.enableTunnels = enableTunnelsthis.range = range},setFont(font) {this.font = font},setAlpha(alpha) {if (typeof alpha !== 'number') returnthis.alpha = alpha},setDuration(duration) {if (typeof duration !== 'number') returnthis.duration = durationthis.clear()},// 开启弹幕open() {this._isActive = true},// 关闭弹幕,清除所有数据close(cb) {this._isActive = falsethis.clear(cb)},clear(cb) {this.tunnels.forEach(tunnel => tunnel.clear())this.idleTunnels = new Set(this.enableTunnels)if (typeof cb === 'function') {cb()}},// 添加一批弹幕,轨道满时会被丢弃addData(data = []) {if (!this._isActive || !data || !data.length) returndata.forEach((item, index) => {item.timestampt = new Date().getTime()item.content = item.content || ''item.content = this.substring(item.content, this.maxLength)if (!item.width) {// 一个弹幕总长度=头像(包含边框)+文本+内边距+外边距item.width = (44 + 4) + item.content.length * this.fontSize * 2 + (8 + 20) + 60item.width = Math.ceil(((this.systemInfo.windowWidth || 375) / 375) * (item.width / 2))}this.addBullet2Tunnel(item, index)})// 更新弹幕this.updateBullets()},// 添加至轨道addBullet2Tunnel(opt = {}, index) {const tunnel = this.getIdleTunnel(index)if (tunnel === null) returnconst tunnelId = tunnel.tunnelIdtunnel.addBullet(opt)if (tunnel.getIdleBulletNum() === 0) {this.idleTunnels.delete(tunnelId)}},updateBullets() {if (!this.tunnels || !this.tunnels.length) {return}this.tunnels.map((a) => {a.batchTime = 0 // 通过一批弹幕花费(即一次addData添加的所有弹幕)的时间a.lastBulletIndex = a.lastBulletIndex >= 0 ? a.lastBulletIndex : -1 // 轨道最后通过的弹幕下标a.bullets && a.bullets.map((b, bIndex) => {if ((a.lastBulletIndex === -1 || bIndex > a.lastBulletIndex) && b.content) {a.lastBulletIndex = bIndexconst duration = this.distance * this.duration / (this.distance + b.width)const passDistance = b.width + a.safeGap// 等上一条通过右边界b.passtime1 = Math.ceil(passDistance * this.duration * 1000 / this.distance)a.batchTime += b.passtime1}})this.tunnelAnimate(a)})let list = JSON.parse(JSON.stringify(this.tunnels))list.sort((a, b) => {return b.batchTime - a.batchTime})let lastBullet = list[0].bullets[list[0].lastBulletIndex]// 最后一条弹幕通过屏幕的时间let lastPassTime = list[0].batchTime + Math.ceil((this.width) * this.duration * 1000 / this.distance)console.log('最后一条弹幕通过屏幕的时间:', lastPassTime)let reloadDanmuTimer = setTimeout(() => {// 轨道已满,重置轨道并重新加载弹幕if (!this.idleTunnels || this.idleTunnels.size === 0) {this.last = -1this.$emit('reloadDanmu')} else {this.$emit('reloadDanmu', 'addDanmu')}clearTimeout(reloadDanmuTimer)}, lastPassTime)},tunnelAnimate(tunnel) {if (tunnel.disabled || tunnel.sending) returnconst next = (tunnel.last + 1) % tunnel.maxNumconst bullet = tunnel.bullets[next]if (!bullet) returnif (bullet.content || bullet.image) {tunnel.sending = truetunnel.last = nextconst duration = this.distance * this.duration / (this.distance + bullet.width)const passDistance = bullet.width + tunnel.safeGapbullet.duration = this.duration// 等上一条通过右边界bullet.passtime = Math.ceil(passDistance * bullet.duration * 1000 / this.distance)let sendTimer = setTimeout(() => {tunnel.sending = falsethis.tunnelAnimate(tunnel)clearTimeout(sendTimer)}, bullet.passtime)}},// 从还有余量的轨道中随机挑选一个getIdleTunnel(addIndex) {if (!this.idleTunnels || this.idleTunnels.size === 0) return nullconst idleTunnels = Array.from(this.idleTunnels)let index = -1if (this.tunnelNum == 2 && (addIndex || addIndex === 0)) { // 只有两个轨道的情况下,优先手动分发轨道index = addIndex % 2 === 0 ? 0 : 1}if (index === -1 || (!idleTunnels[index] && idleTunnels[index] !== 0)) { // 随机选轨道index = this.getRandom(idleTunnels.length)}return this.tunnels[idleTunnels[index]]},animationend(opt) {const {tunnelId,bulletId} = optconst tunnel = this.tunnels[tunnelId]const bullet = tunnel && tunnel.bullets && tunnel.bullets[bulletId]if (!tunnel || !bullet) returntunnel.removeBullet(bulletId)this.idleTunnels.add(tunnelId)},tapBullet(opt) {if (!this.enableTap) returnconst {tunnelId,bulletId} = optconst tunnel = this.tunnels[tunnelId]const bullet = tunnel.bullets[bulletId]bullet.paused = !bullet.paused},// 初始化弹幕组件数据getBarrageInstance(opt) {for (let key in opt) {this[key] = opt[key]}const query = uni.createSelectorQuery().in(this)query.select('.barrage-area').boundingClientRect((res) => {res = res || {}let systemInfo = uni.getSystemInfoSync()this.systemInfo = systemInfo || {}this.width = res.width || systemInfo.windowWidththis.height = res.height || 80this.init()this.open()this.addData(this.danmuList)}).exec()},onAnimationend(e) {const {tunnelid,bulletid} = e.currentTarget.datasetthis.animationend({tunnelId: tunnelid,bulletId: bulletid})},onTapBullet(e) {const {tunnelid,bulletid} = e.currentTarget.datasetthis.tapBullet({tunnelId: tunnelid,bulletId: bulletid})},// 获取字节长度,中文算2个字节getStrLen(str) {// eslint-disable-next-line no-control-regexreturn str.replace(/[^\x00-\xff]/g, 'aa').length},// 截取指定字节长度的子串substring(str, n) {if (!str) return ''const len = this.getStrLen(str)if (n >= len) return strlet l = 0let result = ''for (let i = 0; i < str.length; i++) {const ch = str.charAt(i)// eslint-disable-next-line no-control-regexl = /[^\x00-\xff]/i.test(ch) ? l + 2 : l + 1result += chif (l >= n) break}return result},getRandom(max = 10, min = 0) {return Math.floor(Math.random() * (max - min) + min)},getFontSize(font) {const reg = /(\d+)(px)/iconst match = font.match(reg)return (match && match[1]) || 10},}}
</script><style scoped>.barrage-area {position: relative;box-sizing: border-box;width: 100%;height: 100%;z-index: 2;pointer-events: auto;overflow-x: hidden;}.barrage-tunnel {box-sizing: border-box;position: relative;display: flex;align-items: center;border-top: 1px solid #CCB24D;width: 100%;margin-bottom: 30rpx;}.tunnel-tips {display: inline-block;margin-left: 60px;}.bullet-item {position: absolute;display: flex;align-items: center;top: 0;left: 100%;white-space: nowrap;background: rgba(0, 0, 0, 0.3);border-radius: 80rpx;padding: 0 20rpx 0 0;}.bullet-item.paused {background: #000;opacity: 0.6;padding: 0 10px;z-index: 2;}.bullet-item_img {max-height: 100%;border-radius: 50%;border: 2px solid #FFFFFF;}.bullet-item_text {display: inline-block;margin: 0;}.bullet-move {animation: 0s linear slidein}@keyframes slidein {0% {transform: translate3d(0, 0, 0)}100% {transform: translate3d(-1500px, 0, 0)}}
</style>

http://www.mrgr.cn/news/81657.html

相关文章:

  • DFS【东北大学oj数据结构11-2】C++
  • 在 Sanic 应用中使用内存缓存管理 IP 黑名单
  • 若依plus apifox导入接口显示为空
  • axios 常见的content-type、responseType有哪些?
  • Mac 查询IP配置,网络代理
  • 计算机视觉目标检测-1
  • YOLOv8中间特征层可视化
  • Docker完整技术汇总
  • Windows下C++使用SQLite
  • 计算机网络习题(第1章 概论 第2章 数据通信基础)
  • 音视频入门基础:MPEG2-TS专题(23)——通过FFprobe显示TS流每个packet的信息
  • React 高阶组件(HOC)
  • 贪心算法(常见贪心模型)
  • 设计模式与游戏完美开发(2)
  • MySQL 查询大偏移量(LIMIT)问题分析
  • Go快速开发框架2.6.0版本更新内容快速了解
  • Python的简单爬虫框架
  • 《传染病与人类历史》传染病如何推动人类历史进程
  • 【Spring AI】Spring AI Alibaba的简单使用
  • HTML速查
  • 【QT开发自制小工具】PDF/图片转excel---调用百度OCR API接口
  • 洛谷 P1014:Cantor 表
  • 用友-友数聚科技CPAS审计管理系统V4 getCurserIfAllowLogin存在SQL注入漏洞
  • Unity Dots理论学习-2.ECS有关的模块(1)
  • KVM虚拟机管理脚本
  • 攻防世界web第三题file_include