社区编辑申请
注册/登录
HarmonyOS三方开源组件—鸿蒙JS实现仿蚂蚁森林
开源 OpenHarmony
HarmonyOS三方开源组件之鸿蒙JS实现仿蚂蚁森林,自定义仿支付宝蚂蚁森林水滴控件,提供了JAVA UI和JS UI两种实现方式。

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

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 来源: 鸿蒙社区
相关推荐

2022-05-11 15:08:52

驱动开发系统移植

2022-06-22 09:19:55

HDC鸿蒙ADB命令

2022-04-02 20:45:04

Hi3516开发板操作系统鸿蒙

2022-04-07 15:28:16

HarmonyOS鸿蒙操作系统

2022-04-18 10:37:01

鸿蒙操作系统开发工具

2022-04-06 11:27:05

harmonyeTS 开发NAPI开发

2022-06-15 16:16:21

分布式数据库鸿蒙

2022-03-18 15:29:02

Harmony鸿蒙架构

2022-06-15 11:51:14

Vue3开发避坑

2022-06-24 11:14:00

美团开源

2022-05-09 11:52:38

Java卡片服务卡片

2022-06-24 07:08:24

OHOS自定义服务

2022-04-07 14:33:31

操作系统鸿蒙HarmonyOS

2022-07-04 23:16:21

开源技术容器

2022-06-07 10:40:05

蓝牙鸿蒙

2022-06-10 07:45:09

CentOS国产操作系统

2022-06-06 15:18:41

开源GiteaDrone

2022-05-24 15:06:57

AbilityeTS FA鸿蒙

2022-06-28 10:03:56

CentOSLinux

2022-04-13 11:24:18

ETS开发HarmonyOS鸿蒙

同话题下的热门内容

Copilot收费,惹怒软件自由保护协会SFC:停止使用GitHub,时机已到DevOps 工具链管理器 DevStream 还真是神器!2022 年开源技术六大趋势阿里&蚂蚁联合开源的 IDE 研发框架 - OpenSumi2022 年开源技术六大趋势四个用于在云原生环境中运行虚拟机的开源工具软件自由保护协会拒绝Github!代码“借用”界限何在?Kuro:非官方的微软 To-Do Linux 桌面客户端

编辑推荐

十大免费开源云文件共享平台使用Go语言开发必备的5大开源工具开源人脸识别seetaface入门教程(一)Docker不香吗,为啥还要K8s?值得考虑的九大开源ERP系统,看看都有谁
我收藏的内容
点赞
收藏

51CTO技术栈公众号