美文网首页
uniapp的动画运用(一)旋转动画《抽奖大转盘》

uniapp的动画运用(一)旋转动画《抽奖大转盘》

作者: 周星星的学习笔记 | 来源:发表于2022-03-15 16:49 被阅读0次

今天简单记录一下如何使用uni.createAnimation(OBJECT)来构建一个抽奖大转盘。效果如下图所示。

效果

一、大转盘布局分解

1.整体圆盘分为5层,从外到内的顺序依次是:

  • 1.黄色层 > 2.红色层 > 3.白色层 > 4.粉色层 > 5.红色层(启动按钮)。


    层分解

二、霓虹灯排布

  1. 小灯的排布是基于父级(第2层)进行绝对定位的。
  2. 小灯是按照一定角度围绕中心点旋转排布的。
  3. 小灯是闪烁的,可以使用定时器构建出交替闪烁的效果。

1.模板代码

<!-- 圆盘周围的圆点 -->
          <view
            class="dot-wrap"
            v-for="(degree, index) in dotList"
            :key="getItemKey('dot-', index)"
            :style="{ transform: 'rotate(' + degree + ')' }"
          >
            <view
              class="dot"
              :class="{
                'dot-light':
                  (lightMode == 1 && index % 2 != 0) ||
                  (lightMode == 2 && index % 2 == 0)
              }"
            ></view>
          </view>
          <!-- 圆盘周围的圆点 -->

2.使用计算属性构建点列表

 computed: {
    //圆点列表
    dotList() {
      let dList = []
      let dotNum = 20
      let rotateDeg = (1 / dotNum) * 360 // 每个灯旋转的角度
      for (let index = 0; index < dotNum; index++) {
        dList.push(index * rotateDeg + 'deg')
      }
      return dList
    },
   ...
}

3.开启定时器

 data() {
    return {
      //亮灯模式
      lightMode: 1,
      ...
    }
  },
created() {
    //一进来开启跑马灯
    setInterval(() => {
      this.lightMode = this.lightMode === 1 ? 2 : 1
    }, 500)
  },

4.小灯的样式

.play-circle-a {
        width: 100%;
        height: 100%;
        border-radius: 50%;
        border: 4rpx solid #dc3a1f;
        background-color: #ff7a01;
        display: flex;
        justify-content: center;
        align-items: center;
        position: relative;
        .dot-wrap {
          position: absolute;
          width: 100%;
          display: flex;
          align-items: center;
          z-index: 998;
          padding-left: 10rpx;
          .dot {
            width: 12rpx;
            height: 12rpx;
            background: #edc225;
            box-shadow: 0rpx 0rpx 12rpx 0rpx #ffdb2c;
            border-radius: 50%;
            //设置好旋转中心点
            transform-origin: 50% 330rpx;
          }
 }

//圆点亮灯的颜色
.dot-light {
  background-color: #fef0ab !important;
}

三、奖品区域分隔线绘制

  1. 分割线实际是一个宽度很小的矩形。
  2. 线条的个数实际是等于奖品的数量的。
  3. 线条是基于父级(第3层)进行绝对定位,并且按照父级的中心点旋转排布的。

1. 模板代码

 <!-- 渲染分割线区域 -->
            <view
              class="sep-line-item"
              :class="{
                'sep-line-color-pink': styleThemeType === 'pink',
                'sep-line-color-purple': styleThemeType === 'purple'
              }"
              v-for="(item, index) in prizeList"
              :key="getItemKey('sep-', index)"
              :style="{ transform: 'rotate(' + item.lineTurn + ')' }"
            ></view>
<!-- 渲染分割线区域 -->

2.构建好奖品列表

 computed: {
//整合奖品列表
    prizeList() {
       let pList = []
      中间略
      ...
      //计算出旋转角度
      let turnNum = (1 / pList.length) * 360
      pList.map((item, index) => {
        returnList.push({
          ...item,
          //奖品区域需要旋转的角度
          turn: index * turnNum + 'deg',
          //分隔线需要旋转的角度
          lineTurn: index * turnNum + turnNum / 2 + 'deg'
        })
      })
      return returnList
    },
   ...
}

3. css代码

.play-circle-inner {
          position: relative;
          width: 550rpx;
          height: 550rpx;
          border-radius: 50%;
          border: 6rpx solid #dc381f;
          background-color: #ffffff;
          //分隔线
          .sep-line-item {
            position: absolute;
            top: -4rpx;
            left: 269rpx;
            width: 3rpx;
            height: 275rpx;
            overflow: hidden;
            background-color: #dc381f;
            //设置好旋转中心点
            transform-origin: 50% 275rpx;
          }
      ....
}

四、奖品区域排布

  1. 奖品区域实际是有一个个的矩形(如图中紫色线条的矩形)。
  2. 奖品区域是基于父级(第3层)进行绝对定位,并且按照父级的中心点旋转排布的。

1.模板代码

<!-- 奖品显示区域 -->
            <view
              class="prize-item"
              v-for="(item, index) in prizeList"
              :key="getItemKey('prize-', index)"
              :style="{ transform: 'rotate(' + item.turn + ')' }"
            >
              <text
                class="prize-title ellipsis"
                :class="{
                  'w-60': prizeList.length > 7 && prizeList.length <= 11,
                  'w-50': prizeList.length > 11,
                  'prize-title-color-pink': styleThemeType === 'pink',
                  'prize-title-color-purple': styleThemeType === 'purple'
                }"
                >{{ item.title }}</text
              >
              <image
                v-if="item.indexpic"
                :src="item.indexpic"
                :lazy-load="true"
                class="prize-img"
                :class="{
                  round: prizeList.length > 7,
                  'small-image': prizeList.length > 9
                }"
              ></image>
            </view>
<!-- 奖品显示区域 -->

2.css代码

.play-circle-inner {
          position: relative;
          width: 550rpx;
          height: 550rpx;
          border-radius: 50%;
          border: 6rpx solid #dc381f;
          background-color: #ffffff;
          //奖品
          .prize-item {
            width: 100%;
            position: absolute;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding-top: 30rpx;
            transform-origin: 50% 275rpx;
            .prize-title {
              max-width: 190rpx;
              color: #a34033;
              margin-bottom: 24rpx;
              text-align: center;
            }
            .prize-img {
              width: 76rpx;
              height: 76rpx;
            }
          }
 }

五、转盘动画

  1. 点击中间按钮之后去服务端请求抽奖接口(可以额外做一些判断,次数等等进行防刷)。
  2. 然后根据服务端返回的抽奖结果定位到中奖所在的奖品列表中的索引。
  3. 根据中奖奖品所在的索引计算出转盘最后需要转的角度,以确保最终小箭头能够刚好指向所中的奖品,然后开始启动转盘动画。
  4. 转盘动画旋转过程中需要设置正在旋转的状态,防止连续重复点击,当转盘停止之后重置状态,就可以再一次启动转盘了。

1.旋转动画

//旋转圆盘
    rotate() {
      let runNum = 8 //旋转圈数
      let duration = 4000 //旋转的时长
      // 根据中奖的索引值计算出旋转角度
      this.rotateDeg = this.rotateDeg || 0
      //每次旋转的角度需要累加上之前已经旋转过的角度,不然的话会出现只能旋转一次
      this.rotateDeg =
        this.rotateDeg +
        (360 - (this.rotateDeg % 360)) +
        (360 * runNum - this.winPrizeIndex * (360 / this.prizeList.length))
      //创建动画
      let animationRun = uni.createAnimation({
        duration: duration,
        timingFunction: 'ease'
      })
      animationRun.rotate(this.rotateDeg).step()
      //导出动画(开启动画)
      this.animationData = animationRun.export()
      this.rotateStatus = true
      setTimeout(() => {
        //停止旋转重置状态
        this.stopRotate()
      }, 4100)
    },

六、整体代码逻辑(做了简化)

<template>
  <!-- 大转盘组件 -->
  <view class="big-wheel-wrap">
    <!-- 大转盘背景区域 -->
    <view class="big-wheel-bg-wrap">
      <!-- 旋转区域 -->
      <view class="big-wheel-play-wrap">
        <view class="play-circle-a">
          <!-- 圆盘周围的圆点 -->
          <view
            class="dot-wrap"
            v-for="(degree, index) in dotList"
            :key="getItemKey('dot-', index)"
            :style="{ transform: 'rotate(' + degree + ')' }"
          >
            <view
              class="dot"
              :class="{
                'dot-light':
                  (lightMode == 1 && index % 2 != 0) ||
                  (lightMode == 2 && index % 2 == 0)
              }"
            ></view>
          </view>
          <!-- 圆盘周围的圆点 -->

          <!-- 最里面的圆盘 -->
          <view class="play-circle-inner" :animation="animationData">
            <!-- 渲染分割线区域 -->
            <view
              class="sep-line-item"
              v-for="(item, index) in prizeList"
              :key="getItemKey('sep-', index)"
              :style="{ transform: 'rotate(' + item.lineTurn + ')' }"
            ></view>
            <!-- 渲染分割线区域 -->

            <!-- 奖品显示区域 -->
            <view
              class="prize-item"
              v-for="(item, index) in prizeList"
              :key="getItemKey('prize-', index)"
              :style="{ transform: 'rotate(' + item.turn + ')' }"
            >
              <text
                class="prize-title ellipsis"
                :class="{
                  'w-60': prizeList.length > 7 && prizeList.length <= 11,
                  'w-50': prizeList.length > 11
                }"
                >{{ item.title }}</text
              >
              <image
                v-if="item.indexpic"
                :src="item.indexpic"
                :lazy-load="true"
                class="prize-img"
                :class="{
                  round: prizeList.length > 7,
                  'small-image': prizeList.length > 9
                }"
              ></image>
            </view>
            <!-- 奖品显示区域 -->
          </view>
          <!-- 最里面的圆盘 -->

          <!-- 启动抽奖按钮 -->
          <view class="start-btn-wrap">
            <!-- 指针 -->
            <view class="indicator"></view>
            <!-- 指针 -->
            <!-- 圆环按钮 -->
            <view class="circle-btn-outer" @click="submitLottery()">
              <view
                class="circle-btn-inner"
                :class="{
                  'forbid-bg': userLotteryTimes === 0
                }"
              >
                <text class="btn-text">启动抽奖</text>
              </view>
            </view>
            <!-- 圆环按钮 -->
          </view>
          <!-- 启动抽奖按钮 -->
        </view>
      </view>
      <!-- 旋转区域 -->
      <!-- 支架 -->
      <image :src="resource.supportImageUrl" class="support-image"></image>
      <!-- 支架 -->
    </view>
    <!-- 大转盘背景区域 -->

    <!-- 弹窗提示组件 -->
    <lottery-toast ref="lotteryToast" :user-lottery-times="userLotteryTimes" />
    <!-- 弹窗提示组件 -->

    <!-- 提示 -->
    <u-toast ref="uToast" />
    <!-- 提示 -->
  </view>
</template>
<script>
//导入公共截图方法
import { getImageUrl } from '@/common/services/utils'
//导入弹窗提示组件
import lotteryToast from '../modal/lottery-toast'

export default {
  components: {
    lotteryToast
  },
  props: {
    //用户剩余的抽奖次数
    userLotteryTimes: {
      type: Number,
      default: -1
    },
    //用户传递的奖品列表
    userPrizeList: {
      type: Array,
      default() {
        return []
      }
    },
    //活动信息
    activity: {
      type: Object,
      default() {
        return {}
      }
    },
    //提交
    submit: {
      type: Function,
      default() {
        return () => {}
      }
    }
  },
  computed: {
    //谢谢参与数据结构
    thanksObj() {
      return {
        id: -1,
        type: -1,
        indexpic: this.resource.thanksIconUrl,
        title: '谢谢参与'
      }
    },
    //圆点列表
    dotList() {
      let dList = []
      let dotNum = 20
      let rotateDeg = (1 / dotNum) * 360 // 每个灯旋转的角度
      for (let index = 0; index < dotNum; index++) {
        dList.push(index * rotateDeg + 'deg')
      }
      return dList
    },
    //整合奖品列表
    prizeList() {
      let pList = []
      if (this.userPrizeList.length) {
        //首先打乱原始数组
        let oriPrizeList = this.$u.randomArray(this.userPrizeList)
        oriPrizeList.map((item, index) => {
          if (index < 11) {
            let pTitle = item.title
            pList.push({
              //奖项ID
              id: item.id,
              //奖项类型
              type: item.type,
              //奖项图片
              indexpic: this.getPrizeIndexPic(item),
              //选项标题
              title: pTitle
            })
          }
        })
        //如果不足6个奖品则要补足
        if (pList.length < 6) {
          let addNum = 6 - pList.length
          let num = 0
          while (num < addNum) {
            pList.push(this.thanksObj)
            num++
          }
        } else {
          //补一个谢谢参与
          pList.push(this.thanksObj)
        }
      } else {
        let num = 0
        while (num < 6) {
          pList.push(this.thanksObj)
          num++
        }
      }
      //打乱数组
      pList = this.$u.randomArray(pList)
      //打散之后
      let returnList = []
      //计算出旋转角度
      let turnNum = (1 / pList.length) * 360
      pList.map((item, index) => {
        returnList.push({
          ...item,
          //图片&文字需要旋转的角度
          turn: index * turnNum + 'deg',
          //分隔线需要旋转的角度
          lineTurn: index * turnNum + turnNum / 2 + 'deg'
        })
      })
      return returnList
    }
  },
  data() {
    return {
      //图标资源
      resource: {
        //优惠券图标
        couponIconUrl:
          this.$cnf.resDomains.image +
          '/1/20102/2021/1022/617278d8f23e0vyf.png',
        //积分图标
        integralIconUrl:
          this.$cnf.resDomains.image +
          '/1/20102/2022/0126/61f0ee4a19101qwt.png',
        //金币图标
        goldIconUrl:
          this.$cnf.resDomains.image +
          '/1/20102/2021/1014/6167d43ece520oef.png',
        //谢谢参与图片URL
        thanksIconUrl:
          this.$cnf.resDomains.image +
          '/1/20102/2021/0823/61236a0254a5438c.png',
        //支架图片
        supportImageUrl:
          this.$cnf.resDomains.image + '/1/20102/2022/0204/61fd33f839b2ccc5.png'
      },
      //亮灯模式
      lightMode: 1,
      //动画效果参数
      animationData: {},
      //旋转角度
      rotateDeg: 0,
      //是否旋转
      rotateStatus: false,
      //中奖索引
      winPrizeIndex: ''
    }
  },
  created() {
    //一进来开启跑马灯
    setInterval(() => {
      this.lightMode = this.lightMode === 1 ? 2 : 1
    }, 500)
  },
  methods: {
    //获取奖项索引图
    getPrizeIndexPic(item) {
      let indexPic = ''
      //设置中奖奖品图片
      if (item.type === 1) {
        //商品类型
        indexPic = item.goods_indexpic
          ? getImageUrl(JSON.parse(item.goods_indexpic, 100, 100))
          : ''
      } else if (item.type === 2) {
        //优惠券类型
        indexPic = this.resource.couponIconUrl
      } else if (item.type === 4) {
        //积分类型
        indexPic = this.resource.integralIconUrl
      } else if (item.type === 5) {
        //金币类型
        indexPic = this.resource.goldIconUrl
      }
      return indexPic
    },
    //获取key
    getItemKey(key, index) {
      return key + index
    },
    //提交抽奖
    async submitLottery() {
      //正在旋转中不可点击
      if (this.rotateStatus) {
        return
      }
      //判断活动在不在时间范围之内
      if (this.activity.start_state === 2) {
        this.$refs.uToast.show({
          title: '活动未开始',
          type: 'error'
        })
        return
      } else if (this.activity.start_state === 3) {
        this.$refs.uToast.show({
          title: '活动已结束',
          type: 'error'
        })
        return
      }
      //抽之前判断用户的抽奖次数有没有用完
      if (this.userLotteryTimes === 0) {
        return
      }
      //标记已经开始旋转
      this.rotateStatus = true
      //每次点击之前通知父组件
      this.$emit('submit')
      //去服务端请求抽奖接口
      const res = await this.submit(this.activity)
      if (res.code === '0') {
        //说明中奖了,设置中到的奖品的索引位置
        this.setWinPrizeIndex(res.data.id)
        //启动转圈
        this.rotate()
      } else {
        //设置为谢谢参与
        this.setWinPrizeIndex(-1)
        if (res.code !== '1001') {
          //启动转圈
          this.rotate()
        } else {
          //重置状态
          this.rotateStatus = false
        }
      }
    },
    //旋转圆盘
    rotate() {
      let runNum = 8 //旋转圈数
      let duration = 4000 //旋转的时长
      // 旋转角度
      this.rotateDeg = this.rotateDeg || 0
      this.rotateDeg =
        this.rotateDeg +
        (360 - (this.rotateDeg % 360)) +
        (360 * runNum - this.winPrizeIndex * (360 / this.prizeList.length))
      //创建动画
      let animationRun = uni.createAnimation({
        duration: duration,
        timingFunction: 'ease'
      })
      animationRun.rotate(this.rotateDeg).step()
      this.animationData = animationRun.export()
      this.rotateStatus = true
      setTimeout(() => {
        this.stopRotate()
      }, 4100)
    },
    //设置抽中的奖品所在奖品列表的索引
    setWinPrizeIndex(prizeId) {
      this.prizeList.some((item, index) => {
        if (item.id === prizeId) {
          this.winPrizeIndex = index
          return true
        }
      })
    },
    //停止旋转
    stopRotate() {
      this.rotateStatus = false
      //获取配置的提示信息
      const resultTip = this.activity.rule.page_result
      //停止了就给出提示信息
      if (this.winPrizeIndex || this.winPrizeIndex === 0) {
        let prizeInfo = this.prizeList[this.winPrizeIndex]
        if (prizeInfo.type !== -1) {
          setTimeout(() => {
            this.$refs.lotteryToast.show({
              type: 'success',
              title: resultTip.win.tip_title
                ? resultTip.win.tip_title
                : '中奖啦',
              brief: resultTip.win.tip_text
                ? resultTip.win.tip_text
                : '恭喜您获得',
              prizeTitle: prizeInfo.title,
              image: prizeInfo.indexpic
            })
            this.rotateStatus = false
          }, 500)
        } else {
          setTimeout(() => {
            //谢谢参与
            this.$refs.lotteryToast.show({
              type: 'error',
              title: resultTip.no_win.tip_title
                ? resultTip.no_win.tip_title
                : '未中奖',
              brief: resultTip.no_win.tip_text
                ? resultTip.no_win.tip_text
                : '很遗憾,再接再厉啦'
            })
            this.rotateStatus = false
          }, 500)
        }
      } else {
        setTimeout(() => {
          //谢谢参与
          this.$refs.lotteryToast.show({
            type: 'error',
            title: resultTip.no_win.tip_title
              ? resultTip.no_win.tip_title
              : '未中奖',
            brief: resultTip.no_win.tip_text
              ? resultTip.no_win.tip_text
              : '很遗憾,再接再厉啦'
          })
        }, 500)
        this.rotateStatus = false
      }
    }
  }
}
</script>

<style lang="scss">
.big-wheel-wrap {
  .big-wheel-bg-wrap {
    height: 724rpx;
    position: relative;
    display: flex;
    justify-content: center;
    .support-image {
      width: 430rpx;
      height: 100%;
      position: absolute;
      top: 0rpx;
      z-index: 1;
    }
    .big-wheel-play-wrap {
      width: 660rpx;
      height: 660rpx;
      box-shadow: 0rpx 0rpx 12rpx 0rpx #ffb901;
      border: 18rpx solid #ffb901;
      border-radius: 50%;
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 900;
      .play-circle-a {
        width: 100%;
        height: 100%;
        border-radius: 50%;
        border: 4rpx solid #dc3a1f;
        background-color: #ff7a01;
        display: flex;
        justify-content: center;
        align-items: center;
        position: relative;
        .dot-wrap {
          position: absolute;
          width: 100%;
          display: flex;
          align-items: center;
          z-index: 998;
          padding-left: 10rpx;
          .dot {
            width: 12rpx;
            height: 12rpx;
            background: #edc225;
            box-shadow: 0rpx 0rpx 12rpx 0rpx #ffdb2c;
            border-radius: 50%;
            transform-origin: 50% 330rpx;
          }
        }
        .play-circle-inner {
          position: relative;
          width: 550rpx;
          height: 550rpx;
          border-radius: 50%;
          border: 6rpx solid #dc381f;
          background-color: #ffffff;
          //分隔线
          .sep-line-item {
            position: absolute;
            top: -4rpx;
            left: 269rpx;
            width: 3rpx;
            height: 275rpx;
            overflow: hidden;
            background-color: #dc381f;
            transform-origin: 50% 275rpx;
          }
          //奖品
          .prize-item {
            width: 100%;
            position: absolute;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding-top: 30rpx;
            transform-origin: 50% 275rpx;
            .prize-title {
              max-width: 190rpx;
              color: #a34033;
              margin-bottom: 24rpx;
              text-align: center;
            }
            .prize-img {
              width: 76rpx;
              height: 76rpx;
            }
          }
        }
        //中奖抽奖按钮
        .start-btn-wrap {
          position: absolute;
          left: 0rpx;
          top: 0rpx;
          width: 100%;
          height: 100%;
          display: flex;
          justify-content: center;
          align-items: center;
          z-index: 999;
          .indicator {
            position: absolute;
            top: 208rpx;
            width: 0;
            height: 0;
            border-left: 14rpx solid transparent;
            border-right: 14rpx solid transparent;
            border-bottom: 40rpx solid #fbc765;
          }
          .circle-btn-outer {
            width: 145rpx;
            height: 145rpx;
            border-radius: 50%;
            background-color: #fbc765;
            display: flex;
            justify-content: center;
            align-items: center;
            .circle-btn-inner {
              width: 116rpx;
              height: 116rpx;
              background-color: #ff7a01;
              border-radius: 50%;
              display: flex;
              justify-content: center;
              align-items: center;
              .btn-text {
                width: 80rpx;
                color: #ffffff;
                font-size: 30rpx;
                font-weight: 500;
                text-align: center;
                line-height: 40rpx;
              }
            }
          }
        }
      }
    }
  }
}

//圆点亮灯的颜色
.dot-light {
  background-color: #fef0ab !important;
}

.small-image {
  width: 60rpx !important;
  height: 60rpx !important;
}

.forbid-bg {
  background-color: #b1b2b3 !important;
}
</style>

相关文章

网友评论

      本文标题:uniapp的动画运用(一)旋转动画《抽奖大转盘》

      本文链接:https://www.haomeiwen.com/subject/txkydrtx.html