网站首页 » 前端开发 » 前端工具 » 小程序做一个弹幕效果
上一篇:
下一篇:

小程序做一个弹幕效果

前言

小程序来了,你肯定已经跟它交过手,即使也没交过手,估计你也已经把它列入了你的学习列表中,这篇文章就给大家分享一个在小程序中实现的弹幕效果。

弹幕之旅

要想写出一个弹幕效果,其实也不然,主要是在小程序中我们就不能像平时一样很方便地操作 dom ,我们只能通过变能的方法来实现。下面就是通过数据来驱动 DOM 完成弹幕动画效果。

这个弹幕实现了:

1、可以传一个延迟参数来让弹幕延迟出现

2、弹幕数量(屏幕中最多可同时出现个数)里可以自定义

效果:

小程序做一个弹幕效果

图中看到的效果有些卡顿,但实践是非常地顺滑的,请放心使用。

我把弹幕效果做成了一个组件,下面就把这个组件中的相关文件代码列出来:

barrage.js
import barrageSource from "../../apis/barrageSource"
Component({
  properties:{
    showBarrage:{
     type: Boolean,
     observer:function(newValue,oldValue){
       if (!newValue){
         clearInterval(this.data.barrageTimer);
       }else{
         this.data.barrageTimer = setInterval(this.beginAnimate.bind(this), 20);
       }
     }
   }
  },
  data: {
    sw: 0,
    sh: 0,
    bh: 0,
    delay:0,
    itemPrefix: "barrage",
    dIndex: 2, // 轮播取到数据在数组中的当前下标(默认从2开始,因为初始化时已取了前两项),修改这个值就可以指定可同时显示弹幕的数量
    deviceInfo: null,
    barrageTimer: null,
    barrageEle: [],
    barrageData: [],
    currentBarrageData: [],
  },
  methods: {
    init: async function () {
      const data = this.data;
      const query = wx.createSelectorQuery().in(this); // 创建一个用于获取组件内元素的查询对象
      const box = query.select('#barrage-text-box');
      // 弹幕容器只创建两个,通过替换内容实现轮播
      for (let i = 0; i < data.dIndex; i++) {
        const id = "#" + data.itemPrefix + "-" + i;
        data.barrageEle.push(query.select(id));
      }

      const boxRect = await barrageSource.getRect(box);
      data.bh = boxRect.height;
      this.currentBarrageDataInit();
      await this.getItemWidth(...data.barrageEle);
      this.beginDataInit();
    },

    currentBarrageDataInit: function () { // 初始化等使用的数组对象
      const data = this.data;
      function getInitTemp(speed, content, ele) {
        return {
          x: 0,
          speed: speed,
          content: content,
          styleObj: {},
          realStyle: "",
          ele: ele
        }
      }

      for (let i = 0; i < data.dIndex; i++) {
        const speed = this.getRandom(0.5, 1);
        const content = data.barrageData[i].barrageContent;
        const ele = data.barrageEle[i];
        const tempObj = getInitTemp(speed, content, ele);
        data.currentBarrageData.push(tempObj);
      }
      this.setData({
        currentBarrageData: data.currentBarrageData
      });
    },
    getRandom: function (min, max) { // 获取区间(min到max)随机数
      const result = Math.random() * (max - min) + min;
      return result;
    },
    getItemWidth: async function (...rest) { // 获取/更新弹幕文字对应的元素宽度
      const len = rest.length;
      for (let i = 0; i < len; i++) {
        const item = rest[i];
        const selector = item._selector;
        const matchResult = selector.match(/\d+/);
        const itemIndex = matchResult ? matchResult[0] : 0;
        const itemRect = await barrageSource.getRect(item);
        this.data.currentBarrageData[itemIndex].w = itemRect.width;
        this.data.currentBarrageData[itemIndex].h = itemRect.height;
      }
    },
    setStyle: function (item) {
      item.styleObj.transform = ":translate3d(" + item.x + "px, " + item.y + "px, 0);";
      item.realStyle = "";
      for (let key in item.styleObj) { // 拼接行间样式字符串
        item.realStyle += (key + item.styleObj[key]);
      }
      this.setData({ // 把行间样式应用到元素上
        currentBarrageData: this.data.currentBarrageData
      });
    },
    getDeviceBasicInfo: function () { // 获取设置基本信息
      const data = this.data;
      data.deviceInfo = wx.getSystemInfoSync(); // 设备基本信息对象
      data.sw = data.deviceInfo.screenWidth; // 屏幕宽度
      data.sh = data.deviceInfo.screenHeight; // 屏幕高度
    },
    beginDataInit: async function () {
      const data = this.data;
      const currentBarrageData = data.currentBarrageData;
      for (let item of currentBarrageData) { // 首次设置元素开始位置
        item.x = data.sw;
        item.y = this.getRandom(0, data.bh - item.h);
        item.delay = 0;
        this.setStyle(item);
      }
     
      // data.barrageTimer = setInterval(this.beginAnimate.bind(this), 20);
    },
    beginAnimate: function (){
      const data = this.data;
      const currentBarrageData = data.currentBarrageData;
      for (let item of currentBarrageData) {
        if (item.delay > 0){ // 模拟延迟执行
          item.delay -= 20;
        }else{
          item.x -= item.speed;
        }
        
        if (item.x < -item.w) { // 如果已经滚出屏幕,重置元素基本配置(显示下一条数据)
          item.x = data.sw;
          item.y = this.getRandom(0, data.bh - item.h);
          item.speed = this.getRandom(0.5, 1);
          item.delay = data.delay;
          if (!!!data.barrageData[data.dIndex]) {
            data.dIndex = 0;
          }
          this.updateText(item, data.barrageData[data.dIndex].barrageContent);
          data.dIndex++;
          this.getItemWidth(item.ele); // 生新获取元素的宽度
        }
        this.setStyle(item);
      }
    },
    updateText: function (target, newText) {  // 更新元素文本
      target.content = newText;
      this.setData({ // 先把已替换的内容更新到页面中,方可获取实时元素宽度
        currentBarrageData: this.data.currentBarrageData
      });
    }
  },
  ready: async function () {
    const data = this.data;
    this.getDeviceBasicInfo();
    // 弹幕获取数据
    const response = await barrageSource.queryBarrageList(); // 请求后台接口(返回一个对象,对象有两个属性,一个是弹幕文字对象数组,data,另一个就是延迟 delay)
    if (!response) {
      return;
    }
    if (response.barrageList && response.barrageList.length > 0) {
      data.barrageData = response.barrageList;
    }
    if (response.interval) {
      data.delay = response.interval * 1000; // 秒转毫秒
    }
   
    if (data.barrageData.length === 0) { // 如果没有弹幕数据,直接返回
      return;
    }
    this.init();
  },
  detached: function () {
    clearInterval(this.data.barrageTimer); // 组件被移除时清掉定时器,不然当你点击左上角的返回按钮返回到之前的页面时,这个定时器还是会一直执行
  }
});
barrage.wxml
<view class='barrage-text-box' id='barrage-text-box'>
 <view class='barrage-item' id="{{itemPrefix}}-{{index}}" wx:for="{{currentBarrageData}}" wx:key="{{index}}" style="{{item.realStyle}}">
    <view class='icon-box'><image class='icon' src='{{barrageHornIcon}}'></image></view>
    <text>{{item.content}}</text>
  </view>
</view>
barrage.wxss
.barrage-text-box{
  position: fixed;
  top: 0;
  left: 0;
  height: 426rpx;
  width: 750rpx;
  overflow: hidden;
}
.barrage-item{
  background: #4f76ec;
  opacity: 0.95;
  color: #fff;
  font-size: 32rpx;
  position: absolute;
  top: 0;
  height: 80rpx;
  border-radius: 80rpx;
  display: flex;
  align-items: center;
  padding: 0rpx 24rpx 0 10rpx;
  box-sizing: border-box;
  transform: translate(750rpx,0);
}
.barrage-item .icon{
  width: 41rpx;
  height: 35rpx;
}
.barrage-item .icon-box{
  width: 60rpx;
  height: 60rpx;
  background: #fff;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 50%;
  margin-right: 10rpx;
}
barrageSource.js
 const getRect = (obj) => {
   return new Promise((revsolve, reject) => {
     obj.boundingClientRect((rect) => {
         revsolve(rect);
     }).exec();
   })
 }

 export default {
   getRect
 }

为什么要把 boundingClientRect() 方法放到 Promise 中?因为这个方法是异步的,如果不用这种方式的话,直接在页面中使用它,并且也在它的回调函数中执行了相关代码,但是不知道为什么总是会出现一些奇怪的问题,所以,推荐优先使用 Promise 来返回结果。

除了上面的代码外,我们还需要在父页面中添加额外的代码,用来处理通过 wx.navigateTo({url: ‘……’}) 方法实现跳转后,定时器还会继续执行的问题。如果把上面的定时器的时候调大一点,那么闪退的问题就会得到缓解,本来1~2分钟就会出现闪退,而加大了时间后就会更外才出现闪退

父页面中的相关代码

parentPage.js
Page({
    changeBarrageStatus: function (status) {
      this.data.showBarrage = status;
      this.setData({
        showBarrage: this.data.showBarrage
      });
    },
    /*
     * 生命周期函数-监听页面显示
     */
    onShow: async function() {
      this.changeBarrageStatus(true);
    },
    /*
     * 生命周期函数-监听页面隐藏
     */
    onHide: function() {
      this.changeBarrageStatus(false);
    },
})
parentPage.wxml(在页面中调用这个组件,并且向组件传递一个参数)
<view class="home-poster-box">
   <barrage show-barrage="{{showBarrage}}"></barrage>
</view>

这个参数用于告诉组件内容,父页面是显示还是隐藏来控制弹幕组件内的定时器是否应该被清掉,即停止或者启动动画。弹幕组件通过 properties 中定义的属性showBarrage的observer监听方法来监听父页面中的 showBarrage 的变化(显示或者隐藏),一般情况下 wx.navigateTo({url: ‘……’})  跳转到新页面后,前一个页面没有并销毁,只是隐藏起来了,如果不处理弹幕中的定时器的话,那么这个定时器在“新页面”中也会一直的执行。所以我们通过父页面中的 onShow,onHide 方法钩子来间接处理定时器。当离开弹幕所在的页面时,我们清掉定时器,当回到弹幕页面时,我们又重新启动这个动画定时器。

本例子中还有一个地方要特别地注意,那就是当你更新了其中一个元素中的内容(本例子中的 updateText() 方法)时,一定一定要用 setData({……}) 方法更新一下数据,不然就无法获取到当前字符串所在的元素宽度。

似乎已经大功告成,但后面出现的问题让人无奈,动画在一些手机中动画有明显卡顿,还会有小程序闪退的问题。估计就是资源占用太多导致小程序崩了。

所有就换了种实现方式,不按上面的效果来,改下下面的降级效果:

1、从后面取回来有多少条数据,就生成多少个元素

2、弹幕去掉了循环显示

3、根据数据的先后顺序给第一个数据添加延迟执行以达到随机出现的效果

主要代码
Component({
  properties: {
    barrageData: {
      type: Array,
      value: []
    },
    barrageDelay: {
      type: Number,
      value: 2000
    }
  },
  data: {
    sw: 0,
    bh: 0,
    itemPrefix: "barrage",
    deviceInfo: null,
    currentBarrageData: [],
  },
  methods: {
    init: async function() {
      const data = this.data;
      const query = wx.createSelectorQuery().in(this); // 创建一个用于获取组件内元素的查询对象
      const box = query.select('#barrage-text-box');


      const boxRect = await barrageSource.getRect(box);
      data.bh = boxRect.height;
      this.currentBarrageDataInit();
      this.beginPosition();
    },

    currentBarrageDataInit: function() { // 初始化等使用的数组对象
      const data = this.data;
      function getInitTemp(speed, content, top, delay) {
        return {
          x: 0,
          top: top,
          speed: speed,
          content: content,
          styleObj: {},
          realStyle: "",
          delay: delay
        }
      }

      for (let i = 0; i < data.barrageData.length; i++) {
        const speed = this.getRandom(7, 17);
        const content = data.barrageData[i].barrageContent;
        const top = this.getRandom(0, 160);
        const delay = i * 6 + this.getRandom(0, 2);
        const tempObj = getInitTemp(speed, content, top, delay);
        data.currentBarrageData.push(tempObj);
      }
      this.setData({
        currentBarrageData: data.currentBarrageData
      });
    },

    getRandom: function(min, max) { // 获取区间(min到max)随机数
      const result = Math.random() * (max - min) + min;
      return result;
    },

    setStyle: function() {
      const data = this.data;
      for (let item of data.currentBarrageData) {
        item.styleObj.top = ":" + item.top + "px;";
        item.styleObj.transform = ":translateX(-100%);";
        item.styleObj.transition = ":all " + item.speed + "s ease-in-out " + item.delay + "s;";
        for (let key in item.styleObj) { // 拼接行间样式字符串
          item.realStyle += (key + item.styleObj[key]);
        }
      }

      this.setData({ // 把行间样式应用到元素上
        currentBarrageData: data.currentBarrageData
      });
    },

    getDeviceBasicInfo: function() { // 获取设置基本信息
      const data = this.data;
      data.deviceInfo = wx.getSystemInfoSync(); // 设备基本信息对象
      data.sw = data.deviceInfo.screenWidth; // 屏幕宽度
    },
    beginPosition: function() {
      this.setStyle();
    },
  },
  ready: async function() {
    this.getDeviceBasicInfo();
    this.init();
  }
});

改用上面的这个新方案后,虽然缺少了循环播放弹幕的功能,但是现百弹幕的动画效果,真的是如丝般顺滑。即使在第一种方式中在一些机型中动画出现明显卡顿,用了第二次降级方案后就没问题了。

  • 微信扫一扫,赏我

  • 支付宝扫一扫,赏我

声明

原创文章,不经本站同意,不得以任何形式转载,如有不便,请多多包涵!

本文永久链接:http://yunkus.com/mini-program-animation-barrage/

发表评论

电子邮件地址不会被公开。 必填项已用*标注

评论 END