HarmonyOS三方开源组件—鸿蒙JS实现仿蚂蚁森林

开源 OpenHarmony
HarmonyOS三方开源组件之鸿蒙JS实现仿蚂蚁森林,自定义仿支付宝蚂蚁森林水滴控件,提供了JAVA UI和JS UI两种实现方式。

[[415355]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

实现的效果图:

分析实现过程:

1、接收外部传递给组件的一个数组(小球能量列表),及收集能量动画结束的位置

  1. <!-- waterFlake.js --> 
  2.  props: { 
  3.     //后台返回的小球信息 
  4.         ballList: { 
  5.             default: [10, 11, 12, 13, 14], 
  6.         }, 
  7.     // 收集能量动画结束的X坐标 
  8.         collDestinationX: { 
  9.             default: 350 
  10.         }, 
  11.     // 收集能量动画结束的Y坐标 
  12.         collDestinationY: { 
  13.             default: 400 
  14.         } 
  15.     }, 

2、根据小球的数量,生成小球的随机位置坐标。

  1. // 生成小球的x坐标数组 
  2.  let xRandom = this.randomCommon(1, 8, this.ballList.length) 
  3.  let all_x = xRandom.map(item => { 
  4.      return item * width * 0.10 
  5.  }); 
  6.  //生成小球的y坐标数组 
  7.  let yRandom = this.randomCommon(1, 8, this.ballList.length); 
  8.  let all_y = yRandom.map(item => { 
  9.      return item * height * 0.08 
  10.  }) 
  1. /** 
  2.      * 随机指定范围内N个不重复的数 
  3.      * 最简单最基本的方法 
  4.      * 
  5.      * @param min 指定范围最小值 
  6.      * @param max 指定范围最大值 
  7.      * @param n   随机数个数 
  8.      * @return 随机数列表 
  9.      */ 
  10.     randomCommon(minmax, n) { 
  11.         if (n > (max - min + 1) || max < min) { 
  12.             return null
  13.         } 
  14.         let result = []; 
  15.         let count = 0; 
  16.         while (count < n) { 
  17.             let num = parseInt((Math.random() * (max - min)) + min); 
  18.             let flag = true
  19.             for (let j = 0; j < n; j++) { 
  20.                 if (num == result[j]) { 
  21.                     flag = false
  22.                     break; 
  23.                 } 
  24.             } 
  25.             if (flag) { 
  26.                 result[count] = num; 
  27.                 count++; 
  28.             } 
  29.         } 
  30.         return result; 
  31.     }, 

3、根据传递进来的能量列表及生成的小球坐标,组装成我们需要的小球数据列表ballDataList[]

  1. /** 
  2.  * ballDataList的每个对象包括以下属性: 
  3.  * content(小球显示的文本信息) 
  4.  * x(横坐标)、 
  5.  * y(纵坐标) 
  6.  */ 
  7. ballDataList: [], 
  1. let dataList = [] 
  2.  for (let index = 0; index < this.ballList.length; index++) { 
  3.      dataList.push({ 
  4.          content: this.ballList[index] + 'g'
  5.          x: all_x[index], 
  6.          y: all_y[index
  7.      }) 
  8.  } 
  9.  this.ballDataList = dataList; // 触发视图更新 

4、绘制小球随机显示界面

  1. <!-- waterFlake.hml --> 
  2. <div class="main_contain" ref="main_contain" id="main_contain"
  3.     <text for="{{ ballDataList }}" 
  4.           style="top : {{ $item.y }} px; 
  5.                   left : {{ $item.x }} px;" 
  6.             >{{ $item.content }}</text> 
  7.  
  8. </div> 
  1. .main_contain { 
  2.     width: 100%; 
  3.     position: relative
  4.  
  5. .ball { 
  6.     width: 120px; 
  7.     height: 120px; 
  8.     background-color: #c3f593; 
  9.     background-size: 100%; 
  10.     border-radius: 60px; 
  11.     border: #69c78e; 
  12.     border-bottom-style: solid; 
  13.     border-width: 1px; 
  14.     position: absolute
  15.     text-align: center; 

5、给小球添加动画:

由于鸿蒙JSUI框架@keyframes 动画只能指定动画初始样式(from属性)和终止样式(to属性),故只能采用JS给小球指定动画。

小球移动轨迹为上下浮动的简单动画,可有两种思路实现:

方式一:为每个小球设置连续无限次数动画

  1. createShakeAnimate(el) { 
  2.         if (el == null || el == undefined) { 
  3.             return 
  4.         } 
  5.         var options = { 
  6.             duration: 2000, 
  7.             easing: 'friction'
  8.             fill: 'forwards'
  9.             iterations: "Infinity"
  10.         }; 
  11.         var frames = [ 
  12.             { 
  13.                 transform: { 
  14.                     translate: '0px 0px' 
  15.                 }, 
  16.                 offset: 0.0 // 动画起始时 
  17.             }, 
  18.             { 
  19.                 transform: { 
  20.                     translate: '0px 20px' 
  21.                 }, 
  22.                 offset: 0.5 // 动画执行至一半时 
  23.             }, 
  24.             { 
  25.                 transform: { 
  26.                     translate: '0px 0px' 
  27.                 }, 
  28.                 offset: 1.0 // 动画结束时 
  29.             }, 
  30.  
  31.         ]; 
  32.         let animation = el.animate(frames, options); 
  33.         return animation 
  34.     }, 

方式二:每个小球设置为单向动画,只执行一次,监听动画结束时,调用reverse()方法执行反转动画

  1. createShakeAnimate(el) { 
  2.       if (el == null || el == undefined) { 
  3.           return 
  4.       } 
  5.       var options = { 
  6.           duration: 2000, 
  7.           easing: 'friction'
  8.           fill: 'forwards'
  9.           iterations: 1, 
  10.       }; 
  11.       var frames = [ 
  12.           { 
  13.               transform: { 
  14.                   translate: '0px 0px' 
  15.               }, 
  16.               offset: 0.0 
  17.           }, 
  18.           { 
  19.               transform: { 
  20.                   translate: '0px 20px' 
  21.               }, 
  22.               offset: 1.0 
  23.           }, 
  24.       ]; 
  25.       let animation = el.animate(frames, options); 
  26.        animation.onfinish = function () { 
  27.           animation.reverse() 
  28.       }; 
  29.       return animation 

执行浮动动画

  1. <!-- waterFlake.hml 为每个小球指定id --> 
  2.   <text for="{{ ballDataList }}" 
  3.           class="ball" 
  4.           id="ball{{ $idx }}" 
  5.           onclick="onBallClick($idx,$item)" 
  6.           style="top : {{ $item.y }} px; 
  7.                   left : {{ $item.x }} px;" 
  8.             >{{ $item.content }}</text> 
  1. <!-- waterFlake.js  执行动画 --> 
  2.  playShakeAnimate() { 
  3.       setTimeout(() => { 
  4.           console.info('xwg playShakeAnimate '); 
  5.           for (var index = 0; index < this.ballDataList.length; index++) { 
  6.               let el = this.$element(`ball${index}`) 
  7.               let animate = this.createShakeAnimate(el) 
  8.               animate.play() 
  9.           } 
  10.       }, 50) 
  11.     }, 

6、为小球设置点击事件及收集能量动画

  1. onBallClick(index, item) { 
  2.    // 发送事件给父组件 并将小球信息作为参数传递出去 
  3.    this.$emit('ballClick', item); 
  4.  
  5.    let el = this.$element(`ball${index}`) 
  6.    this.playCollectionAnimate(el, index
  7. }, 
  1. /** 
  2.  * 执行收集的动画 
  3.  * @param el 
  4.  * @param index 
  5.  * @return 
  6.  */ 
  7. playCollectionAnimate(el, index) { 
  8.     if (this.isCollect) { // 正在执行收集动画则直接return 
  9.         return 
  10.     } 
  11.     var options = { 
  12.         duration: 1500, 
  13.         easing: 'ease-in-out'
  14.         fill: 'forwards'
  15.     }; 
  16.     let offsetX = this.collDestinationX - this.ballDataList[index].x 
  17.     let offsetY = this.collDestinationY - this.ballDataList[index].y 
  18.     var frames = [ 
  19.         { 
  20.             transform: { 
  21.                 translate: '0px 0px' 
  22.             }, 
  23.             opacity: 1 
  24.         }, 
  25.         { 
  26.             transform: { 
  27.                 translate: `${offsetX}px ${offsetY}px` 
  28.             }, 
  29.             opacity: 0 
  30.         } 
  31.     ]; 
  32.     let animation = el.animate(frames, options); 
  33.     let _t = this 
  34.     animation.onfinish = function () { 
  35.         console.info('onBallClick collection animation onFinish'); 
  36.         _t.isCollect = false
  37.         _t.ballDataList.splice(index, 1); 
  38.         console.info(JSON.stringify(_t.ballDataList)); 
  39.  
  40.         // 调用splice方法后,原index位置的小球不再执行动画,故手动再创建动画 
  41.         if (index <= _t.ballDataList.length) { 
  42.             setTimeout(() => { 
  43.                 let animate = _t.createShakeAnimate(el) 
  44.                 animate.play() 
  45.             }, 5) 
  46.         } 
  47.     }; 
  48.     this.isCollect = true 
  49.     animation.play() 
  50. }, 

7、父组件点击重置时,更新界面

  1.  onInit() { 
  2.         this.$watch('ballList''onBallListChange'); //注册数据变化监听 
  3. }, 
  4. onBallListChange(newV) { // 外部数据发生变化 重新渲染组件 
  5.         console.log('onBallListChange newV = ' + JSON.stringify(newV)) 
  6.         this.onReady() 
  7.     } 

完整代码如下:

子组件:

  1. <!-- waterFlake.css --> 
  2. .main_contain { 
  3.     width: 100%; 
  4.     position: relative
  5.  
  6. .ball { 
  7.     width: 100px; 
  8.     height: 100px; 
  9.     background-color: #c3f593; 
  10.     background-size: 100%; 
  11.     border-radius: 60px; 
  12.     border: #69c78e; 
  13.     border-bottom-style: solid; 
  14.     border-width: 1px; 
  15.     position: absolute
  16.     text-align: center; 
  17.  
  18. @keyframes Wave { 
  19.     from { 
  20.         transform: translateY(0px); 
  21.     } 
  22.  
  23.     to { 
  24.         transform: translateY(10px); 
  25.     } 
  1. <!-- waterFlake.hml --> 
  2. <div class="main_contain" ref="main_contain" id="main_contain"
  3.     <text for="{{ ballDataList }}" 
  4.           ref="ball{{ $idx }}" class="ball" 
  5.           id="ball{{ $idx }}" 
  6.           tid="ball{{ $idx }}" 
  7.           onclick="onBallClick($idx,$item)" 
  8.           style="top : {{ $item.y }} px; 
  9.                   left : {{ $item.x }} px;" 
  10.             >{{ $item.content }}</text> 
  11.  
  12. </div> 
  1. <!-- waterFlake.js --> 
  2. export default { 
  3.     props: { 
  4.     //后台返回的小球信息 
  5.         ballList: { 
  6.             default: [10, 11, 12, 13, 14], 
  7.         }, 
  8.     // 收集能量动画结束的X坐标 
  9.         collDestinationX: { 
  10.             default: 0 
  11.         }, 
  12.     // 收集能量动画结束的Y坐标 
  13.         collDestinationY: { 
  14.             default: 600 
  15.         } 
  16.     }, 
  17.     data() { 
  18.         return { 
  19.         /** 
  20.              * ballDataList的每个对象包括以下属性: 
  21.              * content(小球显示的文本信息) 
  22.              * x(横坐标)、 
  23.              * y(纵坐标)、 
  24.              */ 
  25.             ballDataList: [], 
  26.             isCollect: false // 是否正在执行收集能量动画 
  27.         }; 
  28.     }, 
  29.     onInit() { 
  30.         this.$watch('ballList''onBallListChange'); //注册数据变化监听 
  31.     }, 
  32.     onReady() { 
  33.         let width = 720 //组件的款第 
  34.         let height = 600 //组件的高度 
  35.         // 生成小球的x坐标数组 
  36.         let xRandom = this.randomCommon(1, 8, this.ballList.length) 
  37.         let all_x = xRandom.map(item => { 
  38.             return item * width * 0.10 
  39.         }); 
  40.         //生成小球的y坐标数组 
  41.         let yRandom = this.randomCommon(1, 8, this.ballList.length); 
  42.         let all_y = yRandom.map(item => { 
  43.             return item * height * 0.08 
  44.         }) 
  45.         if (xRandom == null || yRandom == null) { 
  46.             return 
  47.         } 
  48.         let dataList = [] 
  49.         for (let index = 0; index < this.ballList.length; index++) { 
  50.             dataList.push({ 
  51.                 content: this.ballList[index] + 'g'
  52.                 x: all_x[index], 
  53.                 y: all_y[index
  54.             }) 
  55.         } 
  56.         this.ballDataList = dataList; // 触发视图更新 
  57.         console.info('onReady ballDataList = ' + JSON.stringify(this.ballDataList)); 
  58.  
  59.         this.playShakeAnimate() // 开始执行抖动动画 
  60.     }, 
  61.     onBallClick(index, item) { 
  62.         console.info('onBallClick index = ' + index); 
  63.         console.info('onBallClick item = ' + JSON.stringify(item)); 
  64.         this.$emit('ballClick', item); 
  65.         let el = this.$element(`ball${index}`) 
  66.         this.playCollectionAnimate(el, index
  67.     }, 
  68. /** 
  69.      * 执行收集的动画 
  70.      * @param el 
  71.      * @param index 
  72.      * @return 
  73.      */ 
  74.     playCollectionAnimate(el, index) { 
  75.         if (this.isCollect) { // 正在执行收集动画则直接return 
  76.             return 
  77.         } 
  78.         var options = { 
  79.             duration: 1500, 
  80.             easing: 'ease-in-out'
  81.             fill: 'forwards'
  82.         }; 
  83.         let offsetX = this.collDestinationX - this.ballDataList[index].x 
  84.         let offsetY = this.collDestinationY - this.ballDataList[index].y 
  85.         var frames = [ 
  86.             { 
  87.                 transform: { 
  88.                     translate: '0px 0px' 
  89.                 }, 
  90.                 opacity: 1 
  91.             }, 
  92.             { 
  93.                 transform: { 
  94.                     translate: `${offsetX}px ${offsetY}px` 
  95.                 }, 
  96.                 opacity: 0 
  97.             } 
  98.         ]; 
  99.         let animation = el.animate(frames, options); 
  100.         let _t = this 
  101.         animation.onfinish = function () { 
  102.             console.info('onBallClick collection animation onFinish'); 
  103.             _t.isCollect = false
  104.             _t.ballDataList.splice(index, 1); 
  105.             console.info(JSON.stringify(_t.ballDataList)); 
  106.  
  107.             // 调用splice方法后,原index位置的小球不再执行动画,故手动再创建动画 
  108.             if (index <= _t.ballDataList.length) { 
  109.                 setTimeout(() => { 
  110.                     let animate = _t.createShakeAnimate(el) 
  111.                     animate.play() 
  112.                 }, 5) 
  113.             } 
  114.         }; 
  115.         this.isCollect = true 
  116.         animation.play() 
  117.     }, 
  118.     createShakeAnimate(el) { 
  119.         if (el == null || el == undefined) { 
  120.             return 
  121.         } 
  122.         var options = { 
  123.             duration: 2000, 
  124.             easing: 'friction'
  125.             fill: 'forwards'
  126.             iterations: "Infinity"
  127.         }; 
  128.         var frames = [ 
  129.             { 
  130.                 transform: { 
  131.                     translate: '0px 0px' 
  132.                 }, 
  133.                 offset: 0.0 
  134.             }, 
  135.             { 
  136.                 transform: { 
  137.                     translate: '0px 20px' 
  138.                 }, 
  139.                 offset: 0.5 
  140.             }, 
  141.             { 
  142.                 transform: { 
  143.                     translate: '0px 0px' 
  144.                 }, 
  145.                 offset: 1.0 
  146.             }, 
  147.  
  148.         ]; 
  149.         let animation = el.animate(frames, options); 
  150.         return animation 
  151.     }, 
  152.     playShakeAnimate() { 
  153.         setTimeout(() => { 
  154.             console.info('xwg playShakeAnimate '); 
  155.             for (var index = 0; index < this.ballDataList.length; index++) { 
  156.                 let el = this.$element(`ball${index}`) 
  157.                 let animate = this.createShakeAnimate(el) 
  158.                 animate.play() 
  159.             } 
  160.         }, 50) 
  161.     }, 
  162. /** 
  163.      * 随机指定范围内N个不重复的数 
  164.      * 最简单最基本的方法 
  165.      * 
  166.      * @param min 指定范围最小值 
  167.      * @param max 指定范围最大值 
  168.      * @param n   随机数个数 
  169.      * @return 随机数列表 
  170.      */ 
  171.     randomCommon(minmax, n) { 
  172.         if (n > (max - min + 1) || max < min) { 
  173.             return null
  174.         } 
  175.         let result = []; 
  176.         let count = 0; 
  177.         while (count < n) { 
  178.             let num = parseInt((Math.random() * (max - min)) + min); 
  179.             let flag = true
  180.             for (let j = 0; j < n; j++) { 
  181.                 if (num == result[j]) { 
  182.                     flag = false
  183.                     break; 
  184.                 } 
  185.             } 
  186.             if (flag) { 
  187.                 result[count] = num; 
  188.                 count++; 
  189.             } 
  190.         } 
  191.         return result; 
  192.     }, 
  193.     onBallListChange(newV) { // 外部数据发生变化 重新渲染组件 
  194.         console.log('onBallListChange newV = ' + JSON.stringify(newV)) 
  195.         this.onReady() 
  196.     } 

父组件:

  1. <!-- index.css --> 
  2. .container { 
  3.     flex-direction: column
  4.     align-items: flex-start; 
  5.  
  6. .title { 
  7.     font-size: 100px; 
  8.  
  9. .forestContainer { 
  10.     width: 100%; 
  11.     height: 750px; 
  12.     background-image: url("/common/bg.jpg"); 
  13.     background-size: 100%; 
  14.     background-repeat: no-repeat; 
  1. <!-- index.hml --> 
  2. <element name='waterFlake' src='../../../default/common/component/waterflake/waterFlake.hml'></element> 
  3. <div class="container"
  4.     <div class="forestContainer"
  5.         <waterFlake ball-list="{{ ballList }}" @ball-click="onBallClick"></waterFlake> 
  6.     </div> 
  7.     <button style="padding : 20px; align-content : center; background-color : #222222;" 
  8.             onclick="reset">重置 
  9.     </button> 
  10.  
  11. </div> 
  1. <!-- index.js --> 
  2. import prompt from '@system.prompt'
  3. export default { 
  4.     data() { 
  5.         return { 
  6.             ballList: [] 
  7.         } 
  8.     }, 
  9.     onInit() { 
  10.         this.ballList = this.genRandomArray(5); 
  11.     }, 
  12.     onBallClick(info) { 
  13.         console.info('xwg parent  onBallClick item = ' + JSON.stringify(info.detail)); 
  14.         let content = info.detail.content 
  15.         prompt.showToast({message:`点击了${content}`,duration:1500}) 
  16.     }, 
  17.     reset() { 
  18.         console.info("xwg reset clicked "
  19.         this.ballList = this.genRandomArray(6); 
  20.         console.info("xwg reset  ballList = " + JSON.stringify(this.ballList)) 
  21.     }, 
  22.     genRandomArray(count) { 
  23.         let ballArray = [] 
  24.         for (var index = 0; index < countindex++) { 
  25.             let v = this.random(1, 60) 
  26.             ballArray.push(parseInt(v)) 
  27.         } 
  28.         return ballArray 
  29.     }, 
  30.     random(minmax) { 
  31.         return Math.floor(Math.random() * (max - min)) + min
  32.     } 

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-08-02 14:54:50

鸿蒙HarmonyOS应用

2021-08-26 16:07:46

鸿蒙HarmonyOS应用

2021-03-24 09:30:49

鸿蒙HarmonyOS应用

2021-08-03 12:47:58

鸿蒙HarmonyOS应用

2021-04-28 15:07:06

鸿蒙HarmonyOS应用

2021-08-09 10:24:49

鸿蒙HarmonyOS应用

2021-01-12 12:04:40

鸿蒙HarmonyOS应用开发

2021-01-20 09:54:56

鸿蒙HarmonyOS开发

2021-01-21 13:21:18

鸿蒙HarmonyOSPhotoview组件

2021-03-01 14:00:11

鸿蒙HarmonyOS应用

2021-03-10 15:03:40

鸿蒙HarmonyOS应用

2021-04-29 14:32:24

鸿蒙HarmonyOS应用

2021-01-18 09:52:20

鸿蒙HarmonyOS开发

2021-02-04 09:45:19

鸿蒙HarmonyOS应用开发

2021-06-28 14:48:03

鸿蒙HarmonyOS应用

2021-07-30 14:54:54

鸿蒙HarmonyOS应用

2021-08-03 10:07:41

鸿蒙HarmonyOS应用

2021-04-20 15:06:42

鸿蒙HarmonyOS应用

2021-04-08 14:57:52

鸿蒙HarmonyOS应用

2021-07-06 18:21:31

鸿蒙HarmonyOS应用
点赞
收藏

51CTO技术栈公众号