HarmonyOS三方開源組件—鴻蒙JS實現(xiàn)仿螞蟻森林
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
實現(xiàn)的效果圖:
分析實現(xiàn)過程:
1、接收外部傳遞給組件的一個數(shù)組(小球能量列表),及收集能量動畫結(jié)束的位置
- <!-- waterFlake.js -->
- props: {
- //后臺返回的小球信息
- ballList: {
- default: [10, 11, 12, 13, 14],
- },
- // 收集能量動畫結(jié)束的X坐標(biāo)
- collDestinationX: {
- default: 350
- },
- // 收集能量動畫結(jié)束的Y坐標(biāo)
- collDestinationY: {
- default: 400
- }
- },
2、根據(jù)小球的數(shù)量,生成小球的隨機(jī)位置坐標(biāo)。
- // 生成小球的x坐標(biāo)數(shù)組
- let xRandom = this.randomCommon(1, 8, this.ballList.length)
- let all_x = xRandom.map(item => {
- return item * width * 0.10
- });
- //生成小球的y坐標(biāo)數(shù)組
- let yRandom = this.randomCommon(1, 8, this.ballList.length);
- let all_y = yRandom.map(item => {
- return item * height * 0.08
- })
- /**
- * 隨機(jī)指定范圍內(nèi)N個不重復(fù)的數(shù)
- * 最簡單最基本的方法
- *
- * @param min 指定范圍最小值
- * @param max 指定范圍最大值
- * @param n 隨機(jī)數(shù)個數(shù)
- * @return 隨機(jī)數(shù)列表
- */
- 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、根據(jù)傳遞進(jìn)來的能量列表及生成的小球坐標(biāo),組裝成我們需要的小球數(shù)據(jù)列表ballDataList[]
- /**
- * ballDataList的每個對象包括以下屬性:
- * content(小球顯示的文本信息)
- * x(橫坐標(biāo))、
- * y(縱坐標(biāo))
- */
- 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; // 觸發(fā)視圖更新
4、繪制小球隨機(jī)顯示界面
- <!-- 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給小球指定動畫。
小球移動軌跡為上下浮動的簡單動畫,可有兩種思路實現(xiàn):
方式一:為每個小球設(shè)置連續(xù)無限次數(shù)動畫
- 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 // 動畫執(zhí)行至一半時
- },
- {
- transform: {
- translate: '0px 0px'
- },
- offset: 1.0 // 動畫結(jié)束時
- },
- ];
- let animation = el.animate(frames, options);
- return animation
- },
方式二:每個小球設(shè)置為單向動畫,只執(zhí)行一次,監(jiān)聽動畫結(jié)束時,調(diào)用reverse()方法執(zhí)行反轉(zhuǎn)動畫
- 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
執(zhí)行浮動動畫
- <!-- 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 執(zhí)行動畫 -->
- 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、為小球設(shè)置點擊事件及收集能量動畫
- onBallClick(index, item) {
- // 發(fā)送事件給父組件 并將小球信息作為參數(shù)傳遞出去
- this.$emit('ballClick', item);
- let el = this.$element(`ball${index}`)
- this.playCollectionAnimate(el, index)
- },
- /**
- * 執(zhí)行收集的動畫
- * @param el
- * @param index
- * @return
- */
- playCollectionAnimate(el, index) {
- if (this.isCollect) { // 正在執(zhí)行收集動畫則直接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));
- // 調(diào)用splice方法后,原index位置的小球不再執(zhí)行動畫,故手動再創(chuàng)建動畫
- 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'); //注冊數(shù)據(jù)變化監(jiān)聽
- },
- onBallListChange(newV) { // 外部數(shù)據(jù)發(fā)生變化 重新渲染組件
- 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],
- },
- // 收集能量動畫結(jié)束的X坐標(biāo)
- collDestinationX: {
- default: 0
- },
- // 收集能量動畫結(jié)束的Y坐標(biāo)
- collDestinationY: {
- default: 600
- }
- },
- data() {
- return {
- /**
- * ballDataList的每個對象包括以下屬性:
- * content(小球顯示的文本信息)
- * x(橫坐標(biāo))、
- * y(縱坐標(biāo))、
- */
- ballDataList: [],
- isCollect: false // 是否正在執(zhí)行收集能量動畫
- };
- },
- onInit() {
- this.$watch('ballList', 'onBallListChange'); //注冊數(shù)據(jù)變化監(jiān)聽
- },
- onReady() {
- let width = 720 //組件的款第
- let height = 600 //組件的高度
- // 生成小球的x坐標(biāo)數(shù)組
- let xRandom = this.randomCommon(1, 8, this.ballList.length)
- let all_x = xRandom.map(item => {
- return item * width * 0.10
- });
- //生成小球的y坐標(biāo)數(shù)組
- 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; // 觸發(fā)視圖更新
- console.info('onReady ballDataList = ' + JSON.stringify(this.ballDataList));
- this.playShakeAnimate() // 開始執(zhí)行抖動動畫
- },
- 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)
- },
- /**
- * 執(zhí)行收集的動畫
- * @param el
- * @param index
- * @return
- */
- playCollectionAnimate(el, index) {
- if (this.isCollect) { // 正在執(zhí)行收集動畫則直接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));
- // 調(diào)用splice方法后,原index位置的小球不再執(zhí)行動畫,故手動再創(chuàng)建動畫
- 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)
- },
- /**
- * 隨機(jī)指定范圍內(nèi)N個不重復(fù)的數(shù)
- * 最簡單最基本的方法
- *
- * @param min 指定范圍最小值
- * @param max 指定范圍最大值
- * @param n 隨機(jī)數(shù)個數(shù)
- * @return 隨機(jī)數(shù)列表
- */
- 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) { // 外部數(shù)據(jù)發(fā)生變化 重新渲染組件
- 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;
- }
- }
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)