实现的效果图:
分析实现过程:
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;
- }
- }