【中软国际】HarmonyOS三方开源组件——鸿蒙JS实现仿蚂蚁森林 原创 精华
深开鸿开发板
发布于 2021-7-27 17:41
9.2w浏览
7收藏
实现的效果图
:
分析实现过程:
1、接收外部传递给组件的一个数组(小球能量列表),及收集能量动画结束的位置
<!-- waterFlake.js -->
props: {
//后台返回的小球信息
ballList: {
default: [10, 11, 12, 13, 14],
},
// 收集能量动画结束的X坐标
collDestinationX: {
default: 350
},
// 收集能量动画结束的Y坐标
collDestinationY: {
default: 400
}
},
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
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
})
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
/**
* 随机指定范围内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;
},
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
3、根据传递进来的能量列表及生成的小球坐标,组装成我们需要的小球数据列表ballDataList[]
/**
* ballDataList的每个对象包括以下属性:
* content(小球显示的文本信息)
* x(横坐标)、
* y(纵坐标)
*/
ballDataList: [],
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
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; // 触发视图更新
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
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>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
.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;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
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
},
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
方式二:每个小球设置为单向动画,只执行一次,监听动画结束时,调用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
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
执行浮动动画
<!-- 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>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
<!-- 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)
},
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
6、为小球设置点击事件及收集能量动画
onBallClick(index, item) {
// 发送事件给父组件 并将小球信息作为参数传递出去
this.$emit('ballClick', item);
let el = this.$element(`ball${index}`)
this.playCollectionAnimate(el, index)
},
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
/**
* 执行收集的动画
* @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()
},
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
7、父组件点击重置时,更新界面
onInit() {
this.$watch('ballList', 'onBallListChange'); //注册数据变化监听
},
onBallListChange(newV) { // 外部数据发生变化 重新渲染组件
console.log('onBallListChange newV = ' + JSON.stringify(newV))
this.onReady()
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
完整代码如下:
子组件:
<!-- 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);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
<!-- 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>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
<!-- 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()
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
父组件:
<!-- 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;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
<!-- 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>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
<!-- 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;
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
gitee地址:
https://gitee.com/chinasoft4_ohos/CustomWaterView
作者:熊文功
©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
赞
11
收藏 7
回复
分享
微博
QQ
微信
举报
举报
11
1
7
微信扫码分享
删除帖子
删除 取消
相关推荐
学习了,感谢分享