实现的效果图:
::: hljs-center
:::
分析实现过程:
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;
}
}
gitee地址:
https://gitee.com/chinasoft4_ohos/CustomWaterView
作者:熊文功
https://harmonyos.51cto.com/#bkwz