HarmonyOS三方开源组件——鸿蒙JS实现仿蚂蚁森林

实现的效果图:

::: hljs-center

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;
    }
}

gitee地址:

https://gitee.com/chinasoft4_ohos/CustomWaterView

作者:熊文功

想了解更多关于鸿蒙的内容,请访问:

51CTO和华为官方战略合作共建的鸿蒙技术社区

https://harmonyos.51cto.com/#bkwz

上一篇:JS019. 原生JS使用new Blob()实现带格式导出Excel


下一篇:【JS】原生实现拖拽