Vue.js实战——封装长按能量条&火箭发射动画组件_17

一、目标

    按照项目需求,需要完成如下几部分的功能:

    1、长按屏幕时,显示能量条动画(类似环形进度条);

    2、当能量条充满时,发射小火箭;

二、实现效果

三、步骤

    按照需求分析,上述目标可以拆解成3个功能:

    1)用Vue实现屏幕长按事件;

    2)实现能量条动画;

    3)实现火箭发射动画;

    1、先说下捕捉屏幕长按事件,在IOS、Android等平台都有API直接可用,但在H5中则需要自己去实现。当然目前也有很多开源的Vue长按组件,但本需求相对简单,没有必要因为一个小功能,引入一个包含了其它复杂功能的组件。长按事件的核心是要添加2个事件监听:touchstart和touchend,本人封装成了一个独立的js,touch.js代码如下(完整功能代码详见git):

export default class Touch {
    constructor(node, continueTime) {
        this.node = node;
        this.continueTime = continueTime * 1000;
        this.touchPoint = {
            x: 0,
            y: 0
        };
        this.startTime = undefined;
        this.endTime = undefined;
        this.timerTag = undefined;
        this.progress = undefined;
    }

    registerTouchStart(param) {
        let self = this;
        self.node.addEventListener("touchstart", (e) => {
            console.log("start touch");
            clearInterval(self.timerTag);
            if (e.preventDefault) {
                e.preventDefault();
            }
            self.touchPoint = {
                x: e.changedTouches[0].pageX,
                y: e.changedTouches[0].pageY
            };
            console.log("start touch point:" + JSON.stringify(self.touchPoint));
            self.startTime = new Date().getTime();
            self.timerTag = setInterval(() => {
                self.endTime = new Date().getTime();
                let continueTime = (self.endTime - self.startTime) % self.continueTime;
                // console.log("continue time:" + continueTime);
                self.progress = (continueTime / self.continueTime).toFixed(2);
                if (self.progress == 0) {
                    self.progress = 1.00;
                }
                // console.log("current progress:" + self.progress);
                param.success(self.progress);
            }, 500);
            console.log("current timer:" + self.timerTag);
        });
    }

    registerTouchEnd(param) {
        let self = this;
        self.node.addEventListener("touchend", (e) => {
            console.log("end touch");
            clearInterval(self.timerTag);
            console.log("clear current timer:" + self.timerTag);
            if (e.preventDefault) {
                e.preventDefault();
            }

            self.endTime = new Date().getTime();
            let deltaX = e.changedTouches[0].pageX - self.touchPoint.x;
            let deltaY = e.changedTouches[0].pageY - self.touchPoint.y;
            console.log("start touch point:" + JSON.stringify(e.changedTouches[0]));

            if (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 100) {
                console.log("no long touch action.");
                return;
            }
            console.log("final progress:" + self.progress);
            param.success(self.progress == 1);
        });
    }
}

    2、在vue页面加载完时,就要注册长按监听事件,即在vue的mounted挂载方法中调用上述js,代码详见第3步:

    3、能量条采用了svg绘制,Rocket.vue页面代码如下:

<template>
	<div id="rocket">
		<div class="rocket" ref="rocket" v-if="shotRocket">
			<img src="assets/rocket.png">
		</div>
		<div ref="rocket-circle" class="rocket-circle" :data-pct="percent">
			<svg
				width="100"
				height="100"
				viewPort="0 0 50 50"
				version="1.1"
				xmlns="http://www.w3.org/2000/svg"
			>
				<circle class="circle-inner" :r="radius" cx="50" cy="50" fill="transparent"></circle>
				<circle ref="circle-bar" class="circle-bar" :r="radius" cx="50" cy="50" fill="transparent"></circle>
			</svg>
		</div>
	</div>
</template>
<script>
	import Touch from "../commons/touch";
	export default {
		name: "Rocket",
		data() {
			return {
				percent: 0,
				radius: 45,
				showRocket: false
			};
		},
		mounted: function() {
			let self = this;
			let node = self.$refs["rocket-circle"];
			let touch = new Touch(node, 5);
			touch.registerTouchStart({
				success: percent => {
					console.log("current percent:" + percent);
					self.percentChange(percent);
				}
			});
			touch.registerTouchEnd({
				success: result => {
					console.log("current result:" + result);
					if (result) {
						self.shotRocket();
					}
				}
			});
		},
		methods: {
			shotRocket: function() {
				let self = this;
				let rocket = self.$refs["rocket"];
				let maxHight = window.screen.availHeight;
				let timer = setInterval(() => {
					let bottom = parseInt(rocket.style.bottom);
					if (!bottom) {
						bottom = 15;
					}
					bottom += 18;
					console.log("start move to:" + bottom);
					rocket.style.bottom = bottom + "px";
					if (bottom >= maxHight) {
						clearInterval(timer);
						console.log("finish move:" + bottom);
						self.showRocket = false;
					}
				}, 100);
			},
			percentChange: function(percent) {
				let self = this;
				let input = this.$refs["percent"];
				let cirle = self.$refs["circle-bar"];
				let rocket = self.$refs["rocket"];
				if (isNaN(percent)) {
					percent = 0;
				} else {
					let r = self.radius;
					let c = Math.PI * (r * 2);

					if (percent < 0) {
						percent = 0;
					}
					if (percent > 1) {
						percent = 1;
					}
					if (percent > 0 && !self.showRocket) {
						self.showRocket = true;
						rocket.style.bottom = "15px";
					}
					let pct = percent * c;
					cirle.style.strokeDashoffset = c - pct;
					cirle.style.strokeDashArray = pct + " " + (c - pct);
					cirle.style.stroke = "#ff9f1e";
					self.percent = percent * 100;
				}
			}
		}
	};
</script>

<style  scoped>
	#rocket {
		font-family: "Avenir", Helvetica, Arial, sans-serif;
		-webkit-font-smoothing: antialiased;
		-moz-osx-font-smoothing: grayscale;
		color: #2c3e50;
	}

	.rocket {
		height: 100px;
		width: 100px;
		position: absolute;
		z-index: 1000;
		bottom: 15px;
		left: 50%;
		margin-left: -50px;
		display: flex;
		justify-content: center;
		align-items: center;
	}

	.rocket img {
		height: 60px;
		width: 60px;
	}

	svg {
		transform: rotate(-90deg);
	}
	svg circle {
		stroke-dashoffset: 0px;
		stroke: #666;
		stroke-width: 10px;
	}

	svg .circle-inner {
		stroke-dasharray: 282.74px;
		stroke: #666;
	}

	svg .circle-bar {
		stroke-dasharray: 282.74px;
		transition: stroke-dasharray 0.5s linear;
	}

	.rocket-circle {
		height: 100px;
		width: 100px;
		box-shadow: 0 0 5px black;
		border-radius: 100%;
		position: absolute;
		z-index: 1000;
		bottom: 15px;
		left: 50%;
		margin-left: -50px;
	}

	.rocket-circle:after {
		height: 80px;
		width: 80px;
		box-shadow: inset 0 0 5px black;
		content: attr(data-pct) "%";
		border-radius: 100%;
		line-height: 80px;
		font-size: 10px;
		text-shadow: 0 0 5px black;
		margin-top: -40px;
		margin-left: -40px;
		position: absolute;
		z-index: 1000;
		bottom: 15px;
		left: 50%;
		top: 50%;
		display: flex;
		justify-content: center;
	}

	.circle-input {
		width: 100px;
		position: absolute;
		z-index: 1000;
		bottom: 155px;
		left: 50%;
		margin-left: -50px;
	}
</style>

    4、根据上述的touch.js中设置的长按时长,换算成能量环的充值百分比返回给能量环,能量环通过改变svg circle的stroke-dasharray和stroke-offset来实现充值过程的动画(代码详见第3步)。

    注意:

    1)svg的transform:rotate用来控制circle的起始原点,起始原点默认在水平X轴的圆环上,现在反向旋转90度(逆时针),即圆的起始原点变成了Y轴;

   2)stroke-dasharray表示虚线,通过一组数字来表示实现、虚线(空格隔开,先实后虚,且该数组为偶数,如果为奇数自动X2),比如上述代码中就同时设置了实线和虚线;stroke-offset来表示虚线的起始位置;

    5、火箭发射动画则比较简单,在能量环充满且长按结束时,就设置一个定时器,不停的修改小火箭的bottom值,当bottom值超过屏幕时,停止定时器;

四、总结

    1、vue长按事件相对较容易,在网上找到相关的原理就很好实现;

    2、svg绘图理解起来比较痛苦,尤其是刚开始的时候,动画是根据stroke-offset来的,导致圆环绘制不规则,后面改成了按照stroke-dasharray来,且同时设置了stroke-dasharray的实线和虚线部分,动画才规律;后面把圆环动画的起始位置从水平轴改到垂直轴又花了不少时间,主要还是不了解;

   3、火箭发射比较简单,但是组合完成这2个动画,足足耗费了我3天时间,希望读者可以少走弯路;

五、参考资料

[1]https://www.jb51.net/article/152307.htm

[2]https://www.cnblogs.com/padding1015/p/9277922.html

 

上一篇:Vue.js实战——单独封装echarts时间轴高级篇_16

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值