微信小程序实现拼团成功动画

477 阅读3分钟

小程序拼团.gif

微信小程序实现如上效果。

分为两个组件:

  • PinTuan: 整体组件
  • PinTuanHead: 每个头像组件

代码如下:

PinTuan.js

Component({
  attached:function() {
    // 获取头像地址数据,返回一个头像地址数组
    notification.addObserver('你自己的消息事件', (config) => {
      this._getServeHeadData(config);
    }, this);
  },

  detached: function() {
    this._stopInterval();
    this._stopTimeOut();
  },

  ready: function() {
    // 初始化适配rpx
    this._initRpx();
  },
  
  /**
   * 组件的属性列表
   */
  properties: {

  },

  /**
   * 组件的初始数据
   */
  data: {
    personNum: 1,
    successedImg: 'XXX.png',
    joinText: "即将成团",
    clockText: "00:00:00",  // 倒计时文字初始值
    headsArr: [],  // 所有头像url地址库
    headUrls: [],  // 头像url数组
    moveHead: '',  // 头像飞动图片
    headAniData: {},  // 头像动画animation
    successAni: {},  // 成功动画animation
    pinTuanAni: {},  // 拼团整体上滚刷新animation
    headAniOrigin: true,  // 头像飞动动画是否归位
    successAniOrigin: true,  // 成功动画是否归位
    pinTuanAniOrigin: true,  // 拼团组件动画是否归位
    pinTuanSuccess: false,  // 拼团成功图标显示
    countDownSeedTimer: 10,  // 倒计时最小随机数,也是拼团成功最大时间随机数
    pinTuanMinSeedTimer: 3,  // 拼团最小随机时间
    maxHour: 8,  // 倒计时随机最大小时
  },

  /**
   * 组件的方法列表
   */
  methods: {
    _getServeHeadData: function(config) {
      console.log('check config = ', config)
      // 初始化服务器配置
      if (!config.recent) return;
      const recent = config.recent;
      
      this.setData({
        headsArr: recent,
      });

      this._updateHeadAni();
      this._updateCountDown();
    },

    _initRpx: function() {
      // 适配屏幕移动距离
      wx.getSystemInfo({
        success: res => {
          let rpx = 1 * (res.windowWidth * res.pixelRatio) / (375 * res.pixelRatio);
          this.setData({
            rpx: rpx
          });
        },
      })
    },

    _randomCountDownTime: function(time) {
      if (time < 0) {
        this._stopInterval();
        return;
      }
      let hour = Math.floor(time / 3600) % 10;
      let minute = Math.floor((time / 60) % 60);
      let second = Math.floor(time % 60);
      let timeStr = '0' + hour + ':' + (minute < 10 ? ('0' + minute) : minute) + ':' + (second < 10 ? ('0' + second) : second);
      console.log('check _randomCountDownTime  timeStr = ', timeStr);
      this.setData({
        clockText: timeStr,
      });
    },

    _updateCountDown: function() {
      this._stopInterval();
      let min = this.data.countDownSeedTimer;
      let maxH = this.data.maxHour;
      let time = Math.ceil(min + Math.random() * (maxH * 60 * 60 - min));
      this.timeInterval = setInterval(res => {
        this._randomCountDownTime(time);
        time--;
      }, 1000);
    },

    _stopTimeOut: function() {
      if (this.timeTimeout) clearTimeout(this.timeTimeout);
    },

    _stopInterval: function() {
      if (this.timeInterval) clearInterval(this.timeInterval);
    },

    _updateHeadAni: function() {
      let recent = this.data.headsArr;
      let startIndex = Math.ceil(Math.random() * (recent.length - 3));
      let urls = recent.slice(startIndex, startIndex + 3);
      
      this.setData({
        headsArr: recent,
        headUrls: urls,
        moveHead: urls[2].avatar
      });
      let min = this.data.pinTuanMinSeedTimer;
      let num = Math.ceil(min + Math.random() * (this.data.countDownSeedTimer - min));
      console.log('check updateHeadAni num = ', num);
      this._headAnimation(num);
    },

    // 头像飞动
    _headAnimation: function(timeOut) {
      // 创建动画
      let animation = wx.createAnimation({
        delay: 0,
        duration: 1300,
        timingFunction: "ease"
      });

      // 设置循环
      this.timeTimeout = setTimeout(() => {
        animation.opacity(1).translateX(-123 * this.data.rpx).step();
        this.setData({
          headAniData: animation.export(),
          headAniOrigin: false,
          joinText: "拼团成功!"
        });
      }, timeOut * 1000);
    },

    // 动画结束回掉
    headAniEnd: function(res) {
      if (this.data.headAniOrigin) {
        return;
      }
      // 头像动画归位
      let aniOrigin = wx.createAnimation();
      aniOrigin.opacity(0).translateX(0).step( {duration: 0} );
      this.setData({
        headAniData: aniOrigin.export(),
        headAniOrigin: true,
        pinTuanSuccess: true
      });

      this.selectComponent('.pin-tuan').setData({
        person3Show: true
      });

      // 显示拼团成功动画
      let aniSuccess = wx.createAnimation();
      aniSuccess.scale(1.2).step( {duration: 300, delay: 0, timingFunction: 'ease-in-out'} );
      aniSuccess.scale(1).translateY(0).step( {duration: 100, delay: 0, timingFunction: 'ease-in-out'} );
      this.setData({
        successAni: aniSuccess.export(),
        successAniOrigin: false
      });
    },

    // 拼团成功动画结束回调
    successAniEnd: function() {
      setTimeout(() => {
        let aniEnd = wx.createAnimation();
        if (!this.data.successAniOrigin) {
          aniEnd.scale(0.3).translateY(0).step( {duration: 0, delay: 0} );
          this.setData({
            successAni: aniEnd.export(),
            successAniOrigin: true,
            pinTuanSuccess: false,
            joinText: "即将成团"
          });

          this._pinTuanPlayAni();
        }
      }, 1500);
    },

    _pinTuanPlayAni: function() {
      let ani = wx.createAnimation({
        delay: 0,
        duration: 500,
        timingFunction: 'ease'
      });

      ani.opacity(0).translateY(-30).step();
      this.setData({
        pinTuanAniOrigin: false,
        pinTuanAni: ani.export()
      });
    },

    pinTuanAniEnd: function() {
      if (this.data.pinTuanAniOrigin) {
        return;
      }

      this.selectComponent('.pin-tuan').setData({
        person3Show: false
      });

      let ani = wx.createAnimation();

      ani.opacity(1).translateY(0).step({ duration: 0});
      this.setData({
        pinTuanAniOrigin: true,
        pinTuanAni: ani.export()
      });
      // 刷新下一轮拼团倒计时
      this._updateHeadAni();
      this._updateCountDown();
    },

  }
})

PinTuan.json

{
  "component": true,
  "usingComponents": {
    "PinTuanHead": "../PinTuanHead/PinTuanHead"
  }
}

PintTuan.wxml

<view class="wrap">
  <view class="text-wrap">
    <text>还差</text>
    <text class="persion-num">{{personNum}}人</text>
    <text>成团,可直接参与</text>
  </view>
  <view class="pintuan-content">
    <PinTuanHead class="pin-tuan" headUrls="{{headUrls}}" animation="{{pinTuanAni}}" bindtransitionend="pinTuanAniEnd"></PinTuanHead>
    <view class="join">
      <text class="join-text">{{joinText}}</text>
      <text class="clock">还剩{{clockText}}</text>
    </view>
    <image animation="{{headAniData}}" bindtransitionend="headAniEnd" class="move-head" src="{{moveHead}}"></image>
    <button class="goGroup">去参团</button>
  </view>
  <image wx:if="{{pinTuanSuccess}}" class="successed" src="{{successedImg}}" mode="widthFix"
    animation="{{successAni}}" bindtransitionend="successAniEnd" style="transform: scale(0.3) opacity(0)"></image>
  <view class="bottom-border"></view>
</view>

PinTuan.wxss

.wrap {
  width: 100%;
  height: 216rpx;
  background: #fff;
  font-family: PingFangSC-Semibold,PingFang SC;
  display: block;
}

.successed {
  position: absolute;
  width: 180rpx;
  height: 65rpx;
  padding: 0;
  top: 148rpx;
  left: 60rpx;
  z-index: 5;
}

.bottom-border {
  position: absolute;
  padding: 0;
  width: 89.3%;
  height: 0.5rpx;
  background: #D8D8D8;
  left: 5.35%;
  top: 32%;
}

.text-wrap {
  margin: 28rpx 0 0 40rpx;
  padding: 0;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  flex-grow: 1;
  flex-shrink: 0;
  font-size: 30rpx;
  color: #000;
}

.persion-num {
  margin: 0 10rpx;
  color: #F44;
}

.pintuan-content {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  flex-grow: 1;
  flex-shrink: 0;
}

.join {
  margin-top: 44rpx;
  margin-left: 30rpx;
  display: flex;
  flex-direction: column;
  font-family: PingFangSC-Regular;
  font-size: 24rpx;
}

.join-text {
  color: #FF493B;
}

.clock {
  color: #7F7F7F;
}

.goGroup {
  margin-right: 40rpx;
  margin-top: 38rpx;
  padding: 0;
  background: linear-gradient(#FF4444, #FF6E00);
  border-radius: 2em;
  width: 170rpx;
  height: 70rpx;
  font-size: 30rpx;
  color: #fff;
  line-height: 70rpx;
}

.move-head {
  position: absolute;
  width: 84rpx;
  height: 84rpx;
  border: 1rpx solid #E6E6E6;
  border-radius: 50%;
  right: 230rpx;
  top: 100rpx;
  opacity: 0;
  z-index: 5;
}

PinTuanHead.js

Component({
  attached: function () {},
  detached: function () {},
  /**
   * 组件的属性列表
   */
  properties: {
    headUrls: {
      type: Array,
      value: [],
      observer: "_headUpdate"
    },
  },

  /**
   * 组件的初始数据
   */
  data: {
    backImg: 'back.png',
    person1Show: true,
    person2Show: true,
    person3Show: false,
  },

  /**
   * 组件的方法列表
   */
  methods: {
    _headUpdate(newVal, oldVal) {
      console.log(`check headUpdate newVal = ${JSON.stringify(newVal)}`);
      this.setData({
        headUrls: newVal
      });
    },
    
  }
})

PinTuanHead.json

{
  "component": true,
  "usingComponents": {}
}

PinTuanHead.wxml

<view class="wrap">
  <image wx:if="{{person1Show}}" class="icon" src="{{headUrls[0].avatar}}" mode="widthFix"></image>
  <image wx:else class="back" src="{{backImg}}" mode="widthFix"></image>
  
  <image wx:if="{{person2Show}}" class="icon" src="{{headUrls[1].avatar}}" mode="widthFix"></image>
  <image wx:else class="back" src="{{backImg}}" mode="widthFix"></image>
  
  <image wx:if="{{person3Show}}" class="icon" src="{{headUrls[2].avatar}}" mode="widthFix"></image>
  <image wx:else class="back" src="{{backImg}}" mode="widthFix"></image>
</view>

PinTuanHead.wxss

.wrap {
  margin: 34rpx 0 0 40rpx;
  padding: 0;
  display: flex;
  flex-direction: row;
  width: 100%;
  height: 84rpx;
}

.icon, .back {
  width: 84rpx !important;
  height: 84rpx !important;
}

.icon {
  border: 1rpx solid #E6E6E6;
  border-radius: 50%;
}

.back:nth-child(1),
.icon:nth-child(1) {
  margin-left: 0rpx;
  z-index: 1;
}

.back:nth-child(2),
.icon:nth-child(2) {
  margin-left: -12rpx;
  z-index: 2;
}

.back:nth-child(3),
.icon:nth-child(3) {
  margin-left: -12rpx;
  z-index: 3;
}