HarmonyOS三方開源組件—鴻蒙JS實現仿螞蟻森林
實現的效果圖:
分析實現過程:
1、接收外部傳遞給組件的一個數組(小球能量列表),及收集能量動畫結束的位置
- <!-- waterFlake.js -->
- props: {
- //后臺返回的小球信息
- ballList: {
- default: [10, 11, 12, 13, 14],
- },
- // 收集能量動畫結束的X坐標
- collDestinationX: {
- default: 350
- },
- // 收集能量動畫結束的Y坐標
- collDestinationY: {
- default: 400
- }
- },
2、根據小球的數量,生成小球的隨機位置坐標。
- // 生成小球的x坐標數組
- let xRandom = this.randomCommon(1, 8, this.ballList.length)
- let all_x = xRandom.map(item => {
- return item * width * 0.10
- });
- //生成小球的y坐標數組
- let yRandom = this.randomCommon(1, 8, this.ballList.length);
- let all_y = yRandom.map(item => {
- return item * height * 0.08
- })
- /**
- * 隨機指定范圍內N個不重復的數
- * 最簡單最基本的方法
- *
- * @param min 指定范圍最小值
- * @param max 指定范圍最大值
- * @param n 隨機數個數
- * @return 隨機數列表
- */
- randomCommon(min, max, n) {
- if (n > (max - min + 1) || max < min) {
- return null;
- }
- let result = [];
- let count = 0;
- while (count < n) {
- let num = parseInt((Math.random() * (max - min)) + min);
- let flag = true;
- for (let j = 0; j < n; j++) {
- if (num == result[j]) {
- flag = false;
- break;
- }
- }
- if (flag) {
- result[count] = num;
- count++;
- }
- }
- return result;
- },
3、根據傳遞進來的能量列表及生成的小球坐標,組裝成我們需要的小球數據列表ballDataList[]
- /**
- * ballDataList的每個對象包括以下屬性:
- * content(小球顯示的文本信息)
- * x(橫坐標)、
- * y(縱坐標)
- */
- ballDataList: [],
- let dataList = []
- for (let index = 0; index < this.ballList.length; index++) {
- dataList.push({
- content: this.ballList[index] + 'g',
- x: all_x[index],
- y: all_y[index]
- })
- }
- this.ballDataList = dataList; // 觸發視圖更新
4、繪制小球隨機顯示界面
- <!-- waterFlake.hml -->
- <div class="main_contain" ref="main_contain" id="main_contain">
- <text for="{{ ballDataList }}"
- style="top : {{ $item.y }} px;
- left : {{ $item.x }} px;"
- >{{ $item.content }}</text>
- </div>
- .main_contain {
- width: 100%;
- position: relative;
- }
- .ball {
- width: 120px;
- height: 120px;
- background-color: #c3f593;
- background-size: 100%;
- border-radius: 60px;
- border: #69c78e;
- border-bottom-style: solid;
- border-width: 1px;
- position: absolute;
- text-align: center;
- }
5、給小球添加動畫:
由于鴻蒙JSUI框架@keyframes 動畫只能指定動畫初始樣式(from屬性)和終止樣式(to屬性),故只能采用JS給小球指定動畫。
小球移動軌跡為上下浮動的簡單動畫,可有兩種思路實現:
方式一:為每個小球設置連續無限次數動畫
- createShakeAnimate(el) {
- if (el == null || el == undefined) {
- return
- }
- var options = {
- duration: 2000,
- easing: 'friction',
- fill: 'forwards',
- iterations: "Infinity",
- };
- var frames = [
- {
- transform: {
- translate: '0px 0px'
- },
- offset: 0.0 // 動畫起始時
- },
- {
- transform: {
- translate: '0px 20px'
- },
- offset: 0.5 // 動畫執行至一半時
- },
- {
- transform: {
- translate: '0px 0px'
- },
- offset: 1.0 // 動畫結束時
- },
- ];
- let animation = el.animate(frames, options);
- return animation
- },
方式二:每個小球設置為單向動畫,只執行一次,監聽動畫結束時,調用reverse()方法執行反轉動畫
- createShakeAnimate(el) {
- if (el == null || el == undefined) {
- return
- }
- var options = {
- duration: 2000,
- easing: 'friction',
- fill: 'forwards',
- iterations: 1,
- };
- var frames = [
- {
- transform: {
- translate: '0px 0px'
- },
- offset: 0.0
- },
- {
- transform: {
- translate: '0px 20px'
- },
- offset: 1.0
- },
- ];
- let animation = el.animate(frames, options);
- animation.onfinish = function () {
- animation.reverse()
- };
- return animation
執行浮動動畫
- <!-- waterFlake.hml 為每個小球指定id -->
- <text for="{{ ballDataList }}"
- class="ball"
- id="ball{{ $idx }}"
- onclick="onBallClick($idx,$item)"
- style="top : {{ $item.y }} px;
- left : {{ $item.x }} px;"
- >{{ $item.content }}</text>
- <!-- waterFlake.js 執行動畫 -->
- playShakeAnimate() {
- setTimeout(() => {
- console.info('xwg playShakeAnimate ');
- for (var index = 0; index < this.ballDataList.length; index++) {
- let el = this.$element(`ball${index}`)
- let animate = this.createShakeAnimate(el)
- animate.play()
- }
- }, 50)
- },
6、為小球設置點擊事件及收集能量動畫
- onBallClick(index, item) {
- // 發送事件給父組件 并將小球信息作為參數傳遞出去
- this.$emit('ballClick', item);
- let el = this.$element(`ball${index}`)
- this.playCollectionAnimate(el, index)
- },
- /**
- * 執行收集的動畫
- * @param el
- * @param index
- * @return
- */
- playCollectionAnimate(el, index) {
- if (this.isCollect) { // 正在執行收集動畫則直接return
- return
- }
- var options = {
- duration: 1500,
- easing: 'ease-in-out',
- fill: 'forwards',
- };
- let offsetX = this.collDestinationX - this.ballDataList[index].x
- let offsetY = this.collDestinationY - this.ballDataList[index].y
- var frames = [
- {
- transform: {
- translate: '0px 0px'
- },
- opacity: 1
- },
- {
- transform: {
- translate: `${offsetX}px ${offsetY}px`
- },
- opacity: 0
- }
- ];
- let animation = el.animate(frames, options);
- let _t = this
- animation.onfinish = function () {
- console.info('onBallClick collection animation onFinish');
- _t.isCollect = false;
- _t.ballDataList.splice(index, 1);
- console.info(JSON.stringify(_t.ballDataList));
- // 調用splice方法后,原index位置的小球不再執行動畫,故手動再創建動畫
- if (index <= _t.ballDataList.length) {
- setTimeout(() => {
- let animate = _t.createShakeAnimate(el)
- animate.play()
- }, 5)
- }
- };
- this.isCollect = true
- animation.play()
- },
7、父組件點擊重置時,更新界面
- onInit() {
- this.$watch('ballList', 'onBallListChange'); //注冊數據變化監聽
- },
- onBallListChange(newV) { // 外部數據發生變化 重新渲染組件
- console.log('onBallListChange newV = ' + JSON.stringify(newV))
- this.onReady()
- }
完整代碼如下:
子組件:
- <!-- waterFlake.css -->
- .main_contain {
- width: 100%;
- position: relative;
- }
- .ball {
- width: 100px;
- height: 100px;
- background-color: #c3f593;
- background-size: 100%;
- border-radius: 60px;
- border: #69c78e;
- border-bottom-style: solid;
- border-width: 1px;
- position: absolute;
- text-align: center;
- }
- @keyframes Wave {
- from {
- transform: translateY(0px);
- }
- to {
- transform: translateY(10px);
- }
- }
- <!-- waterFlake.hml -->
- <div class="main_contain" ref="main_contain" id="main_contain">
- <text for="{{ ballDataList }}"
- ref="ball{{ $idx }}" class="ball"
- id="ball{{ $idx }}"
- tid="ball{{ $idx }}"
- onclick="onBallClick($idx,$item)"
- style="top : {{ $item.y }} px;
- left : {{ $item.x }} px;"
- >{{ $item.content }}</text>
- </div>
- <!-- waterFlake.js -->
- export default {
- props: {
- //后臺返回的小球信息
- ballList: {
- default: [10, 11, 12, 13, 14],
- },
- // 收集能量動畫結束的X坐標
- collDestinationX: {
- default: 0
- },
- // 收集能量動畫結束的Y坐標
- collDestinationY: {
- default: 600
- }
- },
- data() {
- return {
- /**
- * ballDataList的每個對象包括以下屬性:
- * content(小球顯示的文本信息)
- * x(橫坐標)、
- * y(縱坐標)、
- */
- ballDataList: [],
- isCollect: false // 是否正在執行收集能量動畫
- };
- },
- onInit() {
- this.$watch('ballList', 'onBallListChange'); //注冊數據變化監聽
- },
- onReady() {
- let width = 720 //組件的款第
- let height = 600 //組件的高度
- // 生成小球的x坐標數組
- let xRandom = this.randomCommon(1, 8, this.ballList.length)
- let all_x = xRandom.map(item => {
- return item * width * 0.10
- });
- //生成小球的y坐標數組
- let yRandom = this.randomCommon(1, 8, this.ballList.length);
- let all_y = yRandom.map(item => {
- return item * height * 0.08
- })
- if (xRandom == null || yRandom == null) {
- return
- }
- let dataList = []
- for (let index = 0; index < this.ballList.length; index++) {
- dataList.push({
- content: this.ballList[index] + 'g',
- x: all_x[index],
- y: all_y[index]
- })
- }
- this.ballDataList = dataList; // 觸發視圖更新
- console.info('onReady ballDataList = ' + JSON.stringify(this.ballDataList));
- this.playShakeAnimate() // 開始執行抖動動畫
- },
- onBallClick(index, item) {
- console.info('onBallClick index = ' + index);
- console.info('onBallClick item = ' + JSON.stringify(item));
- this.$emit('ballClick', item);
- let el = this.$element(`ball${index}`)
- this.playCollectionAnimate(el, index)
- },
- /**
- * 執行收集的動畫
- * @param el
- * @param index
- * @return
- */
- playCollectionAnimate(el, index) {
- if (this.isCollect) { // 正在執行收集動畫則直接return
- return
- }
- var options = {
- duration: 1500,
- easing: 'ease-in-out',
- fill: 'forwards',
- };
- let offsetX = this.collDestinationX - this.ballDataList[index].x
- let offsetY = this.collDestinationY - this.ballDataList[index].y
- var frames = [
- {
- transform: {
- translate: '0px 0px'
- },
- opacity: 1
- },
- {
- transform: {
- translate: `${offsetX}px ${offsetY}px`
- },
- opacity: 0
- }
- ];
- let animation = el.animate(frames, options);
- let _t = this
- animation.onfinish = function () {
- console.info('onBallClick collection animation onFinish');
- _t.isCollect = false;
- _t.ballDataList.splice(index, 1);
- console.info(JSON.stringify(_t.ballDataList));
- // 調用splice方法后,原index位置的小球不再執行動畫,故手動再創建動畫
- if (index <= _t.ballDataList.length) {
- setTimeout(() => {
- let animate = _t.createShakeAnimate(el)
- animate.play()
- }, 5)
- }
- };
- this.isCollect = true
- animation.play()
- },
- createShakeAnimate(el) {
- if (el == null || el == undefined) {
- return
- }
- var options = {
- duration: 2000,
- easing: 'friction',
- fill: 'forwards',
- iterations: "Infinity",
- };
- var frames = [
- {
- transform: {
- translate: '0px 0px'
- },
- offset: 0.0
- },
- {
- transform: {
- translate: '0px 20px'
- },
- offset: 0.5
- },
- {
- transform: {
- translate: '0px 0px'
- },
- offset: 1.0
- },
- ];
- let animation = el.animate(frames, options);
- return animation
- },
- playShakeAnimate() {
- setTimeout(() => {
- console.info('xwg playShakeAnimate ');
- for (var index = 0; index < this.ballDataList.length; index++) {
- let el = this.$element(`ball${index}`)
- let animate = this.createShakeAnimate(el)
- animate.play()
- }
- }, 50)
- },
- /**
- * 隨機指定范圍內N個不重復的數
- * 最簡單最基本的方法
- *
- * @param min 指定范圍最小值
- * @param max 指定范圍最大值
- * @param n 隨機數個數
- * @return 隨機數列表
- */
- randomCommon(min, max, n) {
- if (n > (max - min + 1) || max < min) {
- return null;
- }
- let result = [];
- let count = 0;
- while (count < n) {
- let num = parseInt((Math.random() * (max - min)) + min);
- let flag = true;
- for (let j = 0; j < n; j++) {
- if (num == result[j]) {
- flag = false;
- break;
- }
- }
- if (flag) {
- result[count] = num;
- count++;
- }
- }
- return result;
- },
- onBallListChange(newV) { // 外部數據發生變化 重新渲染組件
- console.log('onBallListChange newV = ' + JSON.stringify(newV))
- this.onReady()
- }
- }
父組件:
- <!-- index.css -->
- .container {
- flex-direction: column;
- align-items: flex-start;
- }
- .title {
- font-size: 100px;
- }
- .forestContainer {
- width: 100%;
- height: 750px;
- background-image: url("/common/bg.jpg");
- background-size: 100%;
- background-repeat: no-repeat;
- }
- <!-- index.hml -->
- <element name='waterFlake' src='../../../default/common/component/waterflake/waterFlake.hml'></element>
- <div class="container">
- <div class="forestContainer">
- <waterFlake ball-list="{{ ballList }}" @ball-click="onBallClick"></waterFlake>
- </div>
- <button style="padding : 20px; align-content : center; background-color : #222222;"
- onclick="reset">重置
- </button>
- </div>
- <!-- index.js -->
- import prompt from '@system.prompt';
- export default {
- data() {
- return {
- ballList: []
- }
- },
- onInit() {
- this.ballList = this.genRandomArray(5);
- },
- onBallClick(info) {
- console.info('xwg parent onBallClick item = ' + JSON.stringify(info.detail));
- let content = info.detail.content
- prompt.showToast({message:`點擊了${content}`,duration:1500})
- },
- reset() {
- console.info("xwg reset clicked ")
- this.ballList = this.genRandomArray(6);
- console.info("xwg reset ballList = " + JSON.stringify(this.ballList))
- },
- genRandomArray(count) {
- let ballArray = []
- for (var index = 0; index < count; index++) {
- let v = this.random(1, 60)
- ballArray.push(parseInt(v))
- }
- return ballArray
- },
- random(min, max) {
- return Math.floor(Math.random() * (max - min)) + min;
- }
- }