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

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.length
					this.danmuList = list
					this.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, // 字体大小,单位px
				width: 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, // 移动距离, 单位px
				systemInfo: {},
				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 = true
						this.last = -1
						this.sending = false
						this.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 = -1
						this.sending = false
						this.bulletStatus = new Array(this.maxNum).fill(0)
						this.bullets.forEach(bullet => bullet.removeContent())
					}

					getIdleBulletIdx() {
						return this.bulletStatus.indexOf(0)
					}

					getIdleBulletNum() {
						let count = 0
						this.bulletStatus.forEach(status => {
							if (status === 0) count++
						})
						return count
					}

					addBullet(opt) {
						if (this.disabled) return
						const idx = this.getIdleBulletIdx()
						if (idx >= 0) {
							this.bulletStatus[idx] = 1
							this.bullets[idx].addContent(opt)
						}
					}

					removeBullet(bulletId) {
						if (this.disabled) return
						this.bulletStatus[bulletId] = 0
						const 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.windowWidth
					this.height = res.height || 300
					this.last = -1
					this.$emit('reloadDanmu')
				}).exec()
			},
			// 设置显示范围 range: [0,1]
			setRange(range) {
				range = range || this.range
				const top = range[0] * this.tunnelNum
				const 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.disabled
						tunnel.enable()
						enableTunnels.add(tunnelId)

						if (disabled || this.idleTunnels.has(tunnelId)) {
							idleTunnels.add(tunnelId)
						}
					} else {
						tunnel.disable()
					}
				})
				this.idleTunnels = idleTunnels
				this.enableTunnels = enableTunnels
				this.range = range
			},
			setFont(font) {
				this.font = font
			},
			setAlpha(alpha) {
				if (typeof alpha !== 'number') return
				this.alpha = alpha
			},
			setDuration(duration) {
				if (typeof duration !== 'number') return
				this.duration = duration
				this.clear()
			},
			// 开启弹幕
			open() {
				this._isActive = true
			},
			// 关闭弹幕,清除所有数据
			close(cb) {
				this._isActive = false
				this.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) return
				data.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) + 60
						item.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) return

				const tunnelId = tunnel.tunnelId
				tunnel.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 = bIndex
							const 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 = -1
						this.$emit('reloadDanmu')
					} else {
						this.$emit('reloadDanmu', 'addDanmu')
					}
					clearTimeout(reloadDanmuTimer)
				}, lastPassTime)
			},
			tunnelAnimate(tunnel) {
				if (tunnel.disabled || tunnel.sending) return

				const next = (tunnel.last + 1) % tunnel.maxNum
				const bullet = tunnel.bullets[next]

				if (!bullet) return

				if (bullet.content || bullet.image) {
					tunnel.sending = true
					tunnel.last = next
					const duration = this.distance * this.duration / (this.distance + bullet.width)
					const passDistance = bullet.width + tunnel.safeGap
					bullet.duration = this.duration
					// 等上一条通过右边界
					bullet.passtime = Math.ceil(passDistance * bullet.duration * 1000 / this.distance)
					let sendTimer = setTimeout(() => {
						tunnel.sending = false
						this.tunnelAnimate(tunnel)
						clearTimeout(sendTimer)
					}, bullet.passtime)
				}
			},
			// 从还有余量的轨道中随机挑选一个
			getIdleTunnel(addIndex) {
				if (!this.idleTunnels || this.idleTunnels.size === 0) return null
				const idleTunnels = Array.from(this.idleTunnels)
				let index = -1
				if (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
				} = opt
				const tunnel = this.tunnels[tunnelId]
				const bullet = tunnel && tunnel.bullets && tunnel.bullets[bulletId]

				if (!tunnel || !bullet) return

				tunnel.removeBullet(bulletId)
				this.idleTunnels.add(tunnelId)
			},
			tapBullet(opt) {
				if (!this.enableTap) return

				const {
					tunnelId,
					bulletId
				} = opt
				const 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.windowWidth
					this.height = res.height || 80
					this.init()
					this.open()
					this.addData(this.danmuList)
				}).exec()
			},
			onAnimationend(e) {
				const {
					tunnelid,
					bulletid
				} = e.currentTarget.dataset
				this.animationend({
					tunnelId: tunnelid,
					bulletId: bulletid
				})
			},
			onTapBullet(e) {
				const {
					tunnelid,
					bulletid
				} = e.currentTarget.dataset
				this.tapBullet({
					tunnelId: tunnelid,
					bulletId: bulletid
				})
			},
			// 获取字节长度,中文算2个字节
			getStrLen(str) {
				// eslint-disable-next-line no-control-regex
				return str.replace(/[^\x00-\xff]/g, 'aa').length
			},
			// 截取指定字节长度的子串
			substring(str, n) {
				if (!str) return ''

				const len = this.getStrLen(str)
				if (n >= len) return str

				let l = 0
				let result = ''
				for (let i = 0; i < str.length; i++) {
					const ch = str.charAt(i)
					// eslint-disable-next-line no-control-regex
					l = /[^\x00-\xff]/i.test(ch) ? l + 2 : l + 1
					result += ch
					if (l >= n) break
				}
				return result
			},
			getRandom(max = 10, min = 0) {
				return Math.floor(Math.random() * (max - min) + min)
			},
			getFontSize(font) {
				const reg = /(\d+)(px)/i
				const 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.kler.cn/a/454385.html

相关文章:

  • ES 磁盘使用率检查及处理方法
  • js版本之ES6特性简述【Proxy、Reflect、Iterator、Generator】(五)
  • LaTeX 是一种基于标记的排版系统,广泛用于创建高质量的文档,特别是在需要复杂数学公式、表格、文献引用等的场景中
  • JVM的详细介绍
  • python+requests接口自动化测试框架实例详解
  • MacOS下TestHubo安装配置指南
  • 游戏引擎学习第61天
  • Idea 将多个module显示在同一个project
  • Java+Vue 断点续传功能实现
  • 【Java 数据结构】链表的中间结点
  • 【华为OD-E卷-租车骑绿道 100分(python、java、c++、js、c)】
  • C++ 最小栈 - 力扣(LeetCode)
  • 杂项记录一些笔记
  • linux服务器上CentOS的yum和Ubuntu包管理工具apt区别与使用实战
  • AIOps平台的功能对比:如何选择适合的解决方案?
  • 简单贪吃蛇小游戏的设计与实现
  • es创建的索引状态一直是red
  • Effective C++ 条款 09:绝不在构造和析构过程中调用 virtual 函数
  • python操作Elasticsearch执行增删改查
  • 十二月第23讲:.NET 9 New features-AOT相关的改进
  • ubuntu搭建redis cluster集群三主三从(从0搭建,小白也会,不啰嗦)
  • (十)Ubuntu 20.04+akiaaa大神 Stable Diffusion整合包 AI绘画教程-外挂VAE模型等快捷设置教程
  • HarmonyOS NEXT 实战之元服务:静态案例效果---电动车电池健康状况
  • DPO(Direct Preference Optimization)算法解释:中英双语
  • 嵌入式学习-QT-Day11
  • .NET Core 中使用 C# 获取Windows 和 Linux 环境兼容路径合并