最终效果图如下:
1. 弹幕从弹幕区域外的右边滚动到左边,那么每条弹幕的实际滚动路径长度为当前弹幕的实际宽度+整个弹幕区域的宽度
组件代码如下:
<template>
<view class="l-barrage">
<block v-for="(item,index) in items" :key="index">
<view
class="barrage-item"
:id="item.id"
:data-duration="item.duration"
:data-trackindex="item.trackIndex"
:style="{
top: `${item.top}`,
'animation-duration': `${Number(item.duration)}s`}"
@animationend="handleEnd(item.id, item.trackIndex)">
{{item.text}}
</view>
</block>
<!-- 计算每条弹幕实际的宽度 -->
<view class="bullet-container">{{currentBullet}}</view>
</view>
</template>
2. 本例中弹幕区域的宽度为当前屏幕的宽度。弹幕的滚动效果使用css3的animation实现,使用translateX来实现水平位移的变化。弹幕使用绝对定位初始时在屏幕的最左边(left: 0),动画开始时弹幕在屏幕外的右边,那么相对于弹幕区域的水平位移为100vw,动画结束时要滚动到屏幕外的左边,水平位移正好为负的当前弹幕的实际宽度。
样式代码如下:
<style lang="scss">
.l-barrage {
position: relative;
z-index: 3;
width: 100vw; // 弹幕区域的宽度为屏幕的宽度
height: 26vh; // 这里的高度可以根据实际需要显示弹幕的行数来设定,如每行高度为24px,那么我们可以设置大于
.barrage-item {
position: absolute;
left: 0;
padding: 0 16rpx;
white-space: nowrap;
color: #fff;
font-size: 30rpx;
background-color: rgba(#fff, .16);
border-radius: 20rpx;
animation: mymove 10s linear forwards;
}
.bullet-container {
position: absolute;
right: 9999rpx;
visibility: hidden;
white-space: nowrap;
}
}
@keyframes mymove
{
from{transform: translateX(100vw);}
to{transform: translateX(-100%);}
}
@-moz-keyframes mymove /* Firefox */
{
from{transform: translateX(100vw);}
to{transform: translateX(-100%);}
}
@-webkit-keyframes mymove /* Safari and Chrome */
{
from{transform: translateX(100vw);}
to{transform: translateX(-100%);}
}
@-o-keyframes mymove /* Opera */
{
from{transform: translateX(100vw);}
to{transform: translateX(-100%);}
}
</style>
3. 首先弹幕可以有多行,且每行的弹幕之间不能相互重叠,那么我们在插入弹幕的时候需要判断下一条弹幕可以插入到哪一行,同时需要考虑在某一行插入的时候会不会与前一条弹幕之间有追击问题。
- 首先根据弹幕区域的实际高度及每条弹幕轨道的高度,获取可以填充弹幕的轨道数
- 根据轨道数初始化一个数组tracks放置每条轨道状态,初始时每条轨道都设置为idle空闲状态
- 同时初始化一个二维数组bullets放置在当前轨道上运行的弹幕的id,方便后面获取轨道上弹幕的滚动距离
- 弹幕插入之前我们需要判断可以插入的轨道,如果没有找到可以插入的轨道,需要等待有空余的轨道的时候插入。
- 那么可以设置一个定时器,每隔一段时间插入一次(注意这个间隔时间最好设置得大一点,否则在最开始所有的轨道都为空闲的情况下,可能出现多条弹幕被放置在了相同位置或者同一条弹幕被重复放置在了多条轨道)
- 在某一条轨道插入弹幕后,将此条轨道的状态设置为running,每次插入的时候会优先查找空闲的轨道,如果没有空闲轨道的情况下,会在running的轨道中检测是否可以插入下一条弹幕
在running的轨道中我们需要考虑弹幕的追击问题,插入的新弹幕和当前轨道的最后一条弹幕之间的追击问题,如果两条弹幕不发生重叠,那么需要满足下面三个条件,假设弹幕A在前,弹幕B在后
- 弹幕A出发后,如果还没有完全进入轨道(滚动的距离小于A本身的宽度),那么弹幕B就不能出发
- 弹幕A先出发,弹幕B后出发,如果弹幕A和弹幕B的速度相同或者弹幕B的速度小于弹幕A,那么必然不会重叠
- 如果弹幕B的速度大于弹幕A,那么就要看弹幕B能不能追上弹幕A,如果弹幕B跑完全程(不包含B本身的宽度)的时间大于弹幕A跑完剩余全程的时间,则表示两弹幕直到跑出轨道两者都不会重叠,反之会重叠。详解看下图:
如图所示,假设B插入的时候,A已经达到图中所在的位置,那么只要保证在到达图中临界位置时两条弹幕没有重叠即可。
如何判断是否重叠呢?只需要比较两条弹幕在到达临界位置的时间即可。如果A到达的时间小于B,那么说明A到达的时候B还没到达,那么不会重叠。如果A到达的时间大于B,那么说明B到达的时候A还没到达,那么A和B会有重叠的时候。
自定义rpx2px方法:文章来源:https://www.toymoban.com/news/detail-517571.html
export function rpx2px(rpx) {
return rpx / 750 * wx.getSystemInfoSync().windowWidth
}
核心代码:文章来源地址https://www.toymoban.com/news/detail-517571.html
import { rpx2px } from '@/utils/utils.js'
let cycle; // 定时器
export default {
name: 'barrage',
props: {
barrageData: {
type: Array,
default: () => {
return [
{blessContent: '祝你幸福'},
{blessContent: '你还好吗'},
{blessContent: 'wwwwwwww'},
{blessContent: 'yyyyyyy'},
{blessContent: 'wuwuwuwuwuwuwuwu'},
{blessContent: 'yiyiyyyyy'}
]
}
}
},
data() {
return {
items: [],
currentBullet: '',
speed: 100,
duration: 10,
trackHeight: 48, // 单位为rpx
targetRight: 0,
tracks: [],
bulletInfo: {}
}
},
mounted() {
!!this.barrageData.length && this.start(this.barrageData)
const query = uni.createSelectorQuery().in(this)
query.select('.l-barrage').boundingClientRect(data => {
this.targetRight = data.right
this.targetW = data.width
this.targetH = data.height
const trackNum = Math.floor(this.targetH / rpx2px(this.trackHeight))
// 初始时设置轨道为空闲
this.tracks = new Array(trackNum).fill('idle')
this.bullets = new Array(trackNum).fill([])
}).exec()
},
destroyed() {
cycle && (clearInterval(cycle))
this.barrageData = []
},
watch: {
barrageData() {
!!this.barrageData.length && this.start(this.barrageData)
}
},
methods: {
start(items = []) {
this.items = [];
this.i = 0
cycle && (clearInterval(cycle));
let i = 0, len = items.length;
cycle = setInterval(()=> {
if(this.i < len) {
this.push(items[this.i])
} else {
clearInterval(cycle)
}
}, 500)
},
// 插入一条弹幕数据
push(item) {
this.currentBullet = item.blessContent
this.$nextTick(() => {
let duration // 计算在相同速度下每条弹幕动画需要的时间
const query = uni.createSelectorQuery().in(this)
query.select('.bullet-container').boundingClientRect(async data => {
if(this.speed) {
duration = (this.targetW + data.width) / this.speed;
} else {
duration = this.duration;
}
// 记录下当前弹幕的宽度和运动时间
this.bulletInfo = {
width: data.width,
duration
}
const trackIndex = await this.getTrackIndex();
// 计算弹幕的top, 10px为行间距
const trackTop = trackIndex * rpx2px(this.trackHeight) + 10 * (trackIndex + 1) + 'px'
const id = 's' + Math.random().toString(36).substring(2)
if (trackIndex > -1) {
if(this.bullets[trackIndex].length) {
this.bullets[trackIndex].push({id})
} else {
this.bullets[trackIndex] = [{id}]
}
this.items.push({
id,
text: item.blessContent,
top: trackTop,
trackIndex,
duration
})
// push成功了才继续插入下一个
this.i++
}
}).exec();
})
},
// 获取空闲轨道
getTrackIndex() {
return new Promise(resolve => {
let readyIdxs = [];
let index = -1;
// 优先去 idle 状态
this.tracks.forEach((v, idx) => v === 'idle' && readyIdxs.push(idx))
if (readyIdxs.length) {
// 可以插入任意一条空闲的轨道,也可以每次都直接取空闲轨道中的第一条
//const random = parseInt(Math.random() * (readyIdxs.length))
// index = readyIdxs[random];
index = readyIdxs[0]
this.tracks[index] = 'running'
resolve(index);
}
// 没有轨道空闲,丛上到下巡检各轨道,选出可执行弹幕轨道
for(let i = 0; i < this.bullets.length; i++) {
const len = this.bullets[i].length;
if (len) {
const item = this.bullets[i][len - 1];
this.checkTrack(item.id, (flag) => {
if(item && flag) {
resolve(i)
} else if(i == (this.bullets.length - 1)){
resolve(-1)
}
})
}
}
})
},
checkTrack(id, cb) {
const query = uni.createSelectorQuery().in(this)
query.select('#' + id).boundingClientRect(data => {
let itemPos = data
// 轨道中最后一个元素尚未完全进入展示区域,直接跳出
if (itemPos.right > this.targetRight) {
cb(false)
} else if(itemPos.right < this.targetRight) {
// 轨道内最后一条弹幕和新弹幕的追及问题
// 轨道中最后一个元素已完全进去展示区域
// 速度相同,只要初始条件满足即可,不用去考虑追及问题
if (this.speed) {
cb(true)
} else {
// this.compare(itemPos, index, len)
// 原弹幕速度
const v1 = (this.targetW + itemPos.width) / +itemPos.dataset.duration;
/**
* 新弹幕
* s2:全程距离
* t2:全程时间
* v2:速度
*/
const s2 = this.targetW + this.bulletInfo.width;
const t2 = this.bulletInfo.duration;
const v2 = s2 / t2
if (v2 <= v1) {
cb(true)
} else {
// t = s / v 比较时间:t1, t2
// 原弹幕跑完剩余全程所需要的时间
const t1 = (itemPos.right - 0) / v1;
// 新弹幕头部跑完全程所需要的时间
const t2 = this.targetW / v2;
// console.log('前面的--->', t1, t2, '后面的时间', v1)
if (t2 < t1) {
cb(false)
}
}
}
}
}).exec()
},
handleEnd(id, trackIndex) {
// 从轨道中剔除已经结束动画的弹幕
this.bullets[trackIndex] = this.bullets[trackIndex].filter(v => v.id !== id)
if (!this.bullets[trackIndex].length) {
this.tracks[trackIndex] = 'idle'
}
}
}
}
</script>
到了这里,关于微信小程序简易弹幕组件(uniapp)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!