用uni-app写一个圆形倒计时组件(包括vue和nvue)

今天来分享一个我自己写的圆形倒计时组件。我在用uni-app做一个APP,里面新增了一个要做倒计时的需求,也就是旁边提示“看视频,得xx奖励”的那种。这个东西在多个页面上都要使用,所以我封装了一个组件,现在详细讲一下其中的过程,因为里面有一些我觉得值得思考的地方。测试环境是在安卓手机的APP,有vue的页面,也有nvue的页面。如果有差不多需要的同学们可以参考,也欢迎给我提出建议。(长文预警,所以这次我做了个目录给大家)

目录

一、成品效果

二、思路和写这个组件的大致过程

三、详细过程

第一步

第二步

第三步

第四步

第五步

总结


一、成品效果

OK,首先来看下效果,我从我自己手机上截了两张图出来,一个是vue的页面,一个是nvue的页面。这个组件我做了两个版本。(因为nvue的一些规定比较特别,我担心nvue这里无法正常使用vue的组件,所以我又做了个nvue的版本。)可以看出来,除了由于字体不同导致效果略有不同(图一是vue的页面,字体是黑体,图二是nvue的页面,字体是我自己在手机上设置的字体,vue的页面在打包前就是固定的黑体),效果是大体相同的,细节上也没有明显的硬伤。

有兴趣的朋友可以看看我写这个组件的过程,现在简单介绍一下我的思路和做组件的大致过程。

二、思路和写这个组件的大致过程

我的思路是这样的:①这个东西应该是一个负责展示的零件,展示的数据就是父组件要传递的prop,我认为要展示的东西是提示文字,如“看视频,得蜜獾”,以及左边的读秒;②另外,组件需要保持在左上角显示,所以根组件需要fixed固定定位,还有,由于有的页面在滚动后下面的其他东西(如菜单)也会置顶,如商城首页这里的“精选 特惠……”这一行,这样,这个组件的上偏移量top的值就不确定了,也应该作为一个输入,具体的值由父组件计算好后传入;③还有,组件在倒计时完毕后要隐藏掉,所以组件是否显示出来也应该由父组件控制;④圆形的进度应该不只是倒计时读秒,弧度应该由百分比决定,所以要知道总时长百分比才可以——也就是说,这个组件的props里面有5个属性:数字类型的读秒值time,字符串类型的提示文字tip,数字类型的top坐标top,布尔类型的是否显示isShow,数字类型的总时长total。写出来就像这样:

props: {
	// 显示的位置
	top: {
		type: Number,
		default: 0
	},
	// 提示文字
	tip: {
		type: String,
		default: '看视频 赚蜜獾'
	},
	// 是否显示,为true显示
	isShow: {
		type: Boolean,
		default: false
	},
	// 还剩多少时间
	time: {
	    type: Number,
	    default: 100
	},
	// 共多少时间
	total: {
		type: Number,
		default: 180
	}
},

大致的过程是这样的:1.最基本的事情,是要做出一个圆形进度条的CSS动画效果,要能正常实现走完一圈一圈的效果。2. 把效果改成设计稿要的样子,包括大小、进度条、轨道、中间圆的颜色。3.把这个效果搬到uni-app组件中,把动画效果改成读秒和总时长是由父组件传入的。 4.在vue的页面上进行测试。5.在nvue的页面上进行测试。

三、详细过程

现在来详细讲一下写这个的过程。

第一步

OK,第一步的代码并不是我亲手写的,但我之前有了解过圆形进度条的大致原理(左右放两个用来旋转的半圆,半圆下方有用来切掉超出半圆超出部分的矩形,底部放一个作为进度条颜色的大圆,半圆旋转后露出大圆的一部分,即扇形,最上层再使用小圆把扇形的中间挡住,露出环形的一部分,然后控制前半部分和后半部分哪个半圆旋转就行了),懒得自己凹细节,网上类似的代码很多,我挑了一个带圆角的案例,拷过来自己改了。我使用的是这篇博客里的代码,非常感谢原作者,不太明白的同学可以去看看——https://blog.csdn.net/qq_42565994/article/details/85228451

第二步

第二步是改细节,我拿过来以后,颜色和大小并不是像设计稿那样的,中间也没有数字,然后我进行了相应的修改。贴一下到这一步,我修改后的代码:


<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>环形进度条</title>
    <style>
        .wrapper {
            position: absolute;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            width: 104px;
            height: 104px;
            margin: auto;
			/* 加上背景色和圆角 */
			border-radius: 50%;
			/* background-color: #1A1A1A; */
			background: #FFD600;
        }
        .container {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 52px;
            overflow: hidden;
        }
        .halfCir {
            width: 52px;
            height: 104px;
			background-color: #1A1A1A;
			/* background: #FFD600; */
        }
        .container1 {
            left: 52px;
        }
        .container1 .halfCir {
            left: 0;
            border-radius: 0 104px 104px 0;
            transform-origin: 0 50%;
            animation: halfCir1 4s infinite linear;           
        }
        .container2 {
            left: 0;
        }
        .container2 .halfCir {
            border-radius: 104px 0 0 104px;
            transform-origin: 52px 52px;
            animation: halfCir2 4s infinite linear;
        }
        @keyframes halfCir1 {
            50%, 100% {
                transform: rotateZ(180deg);
            }
        }
        @keyframes halfCir2 {
            0%, 50% {
                transform: rotateZ(0);
            }
            100% {
                transform: rotateZ(180deg);
            }
        }
        .wrapper::after {
            position: absolute;
            top: 8px;
            left: 8px;
            width: 88px;
            height: 88px;
            border-radius: 50%;
            content: "";    
			background-color: #000;
        }
        .cir {
            position: absolute;
            top: 0;
            right: 0;
            left: 0;
            width: 8px;
            height: 8px;
            margin: auto;
			/* background: #1a1a1a; */
			background: #FFD600;
            border-radius: 50%;
        }
        .cir2 {
            transform-origin: 50% 52px;
            animation: cir2 4s infinite linear;
        }
        @keyframes cir2 {
            100% {
                transform: rotateZ(360deg);
            }
        }
		
		.number {
			position: absolute;
			top: 50%;
			left: 50%;
			z-index: 10;
			transform: translate(-50%, -50%);
			font-size: 36px;
			color: #FFD600;
		}
    </style>
</head>
<body>
	<p>我做的改动如下:1.最外层加上圆角和背景色。
	2. 给最里面的小圆,也就是遮盖层,也就是外圆的after伪元素,加上背景色,这里的背景色是中间的颜色,即黑色。
	3. 往里面加上了数字的显示,定位到最中间,z-index最大。
	4. 设置里面的半圆的背景颜色,因为黄色要越来越大,所以黄色是最底层的颜色,灰黑是上层半圆的颜色色</p>
    <div class="wrapper"> 
        <div class="container container1"> 
            <div class="halfCir"></div> 
        </div> 
        <div class="container container2"> 
            <div class="halfCir"></div> 
        </div>
		<!-- 这两个是控制圆角的小圆 -->
        <div class="cir cir1"></div> 
        <div class="cir cir2"></div>
		<span class="number">58</span>
    </div>
</body>
</html>

效果如下:(我这边要求的旋转方向与那篇博客里写的不一样,是顺时针的。黄色是随着转动越来越大的颜色,所以转动的半圆应该是看起来是轨道色的灰黑色,最底层的半圆才是黄色。然后进度条的圆角效果是用两个小圆来做的,小圆的宽高等于黄色弧形的宽度,border-radius为50%,被固定定位到最上方水平居中的位置,其中一个跟着进行着360%的旋转,正好转到黄色的那一头。这里的哪个颜色在上层,哪个颜色在下层是困扰我的第一个难点,我曾一度为此苦恼,直到我突然发现,颜色互换一下就迎刃而解了)

第三步

第三步是把它拿到uni-app的项目中来用,改写成真正的组件的样子。这是非常重要的一步。我可以细分成几个小步骤

1.在项目的components文件夹新建countdown-tip.vue文件,然后把上面这个HTML文件的主要部分DOM结构和CSS样式搬到这个vue文件中来,记得把<div>改成<view>。这个vue文件的根组件要包含两个view,左边是这个倒计时圆形,右边是文字提示,右边的文字比较简单。因为右边文字的左边圆角并没有完全显示出来,所以父级并不是用flex来并排显示的,而是要让左边的圆使用绝对定位,盖住右边文字提示的左半部分。

2.既然是组件,那么它怎么转,就不能是CSS动画,而应该由父组件来决定它当前转到哪里,转得多快了。所以现在让我们把CSS里面的animation的代码全部删掉,然后把原本CSS要做的动画效果用style动态写到元素上。我假设这一刻右边的半圆转动了half1Turn度(deg),左边的半圆转动了half2Turn度,黄色的小圆(圆弧终点的圆角)转动了ratio个turn(一个trun是360度,0.5turn就是半圈的意思,这也是一个CSS转动角度的值)

(这里的效果是倒计时到0秒,黄色进度条才走满一圈的效果,并不是一秒走一圈的效果)

所以组件左半边的代码变成这样:(注意前半圈转动的是右边的半圆而不是左边的,所以右边的半圆写在前面)

<!-- 左边,倒计时的部分 -->
<view class="time-warpper" :style="{'transform': 'scale('+ rpxRatio +')'}">
	<!-- 右半边圆 -->
	<view class="container container1">
		<!-- 里面的半圆 -->
		<view class="half half-circle1" :style="{'transform': 'rotate('+ half1Turn +'deg)'}"></view>
	</view>
	<!-- 左半边圆 -->
	<view class="container container2">
		<!-- 里面的半圆 -->
		<view class="half half-circle2" :style="{'transform': 'rotate('+ half2Turn +'deg)'}"></view>
	</view>
	<!-- 这两个是控制圆角的小圆 -->
	<view class="cir cir1"></view> 
	<view class="cir cir2" :style="{'transform': 'rotate('+ ratio +'turn)'}"></view>
	<!-- 当前秒数 -->
	<text class="time-text">{{time}}</text>
</view>

这些值是怎么计算出来的呢?明明父组件只传了数字类型的读秒值time,字符串类型的提示文字tip,数字类型的top坐标top,布尔类型的是否显示isShow,数字类型的总时长total来啊。①这里可以使用计算属性,把读秒值time和总时长total两个数据拿来加工,读秒值意味着倒计时还剩下多少秒,总时长减去这个值就得到已经过了多少秒,再除以总时长就是已经过了这段时间的多少比例

前半段时间转动的是右边的半圆,后半段转动的是左边的半圆,所以判断现在的比例是否大于0.5,是就让右边的半圆固定转180度,减去0.5的部分除以0.5,再乘以180,看看左边的半圆需要转动多少度,否就让右边的半圆转动相应的度数,左边的半圆不动,保持在0即可。

③小圆转动的角度就直接是ratio * 1turn,因为这个小圆在一整圈都在转。

computed: {
	// 计算这段时间已经过去了百分之多少
	ratio() {
		return (this.total - this.time) / this.total;
	},
	// 右边的半圆转的度数
	half1Turn() {
		// 大于50%则不转,停留在180deg这里
		if(this.ratio > 0.5) {
			return 180;
		} else {
			// 小于50%则判断现在占50%的多少,根据这个比例乘180
			return (this.ratio / 0.5) * 180;
		}
	},
	// 左边边的半圆转的度数
	half2Turn() {
		// 小于50%则不转,停留在0这里
		if(this.ratio < 0.5) {
			return 0;
		} else {
			// 大于50%则判断大于50%的部分占50%的多少,根据这个比例乘180
			return ((this.ratio - 0.5) / 0.5) * 180;
		}
	}
}

3.那么父组件,也就是vue页面要怎么使用这个组件呢——首先当然是导入组件,指定为自己的子组件,使用对应的标签,并为子组件准备并传递数据。这些都是基本的操作,就不细谈了。要制造读秒的动态效果,那就还需要在onLoad生命周期里启动定时器:

// 让倒计时组件动起来的测试代码
this.t = setInterval(() => {
	if(this.currentTime > 0) {
		this.currentTime--
	}
}, 1000)

currentTime就是当前传给子组件time属性的数据,这里让它每一秒都自减,这样就会让子组件里的time越来越小,于是子组件的数字改变,进度条也动起来了。(小提示:如果为了测试,可以把这里的1000调小一点,这样变化快,测试起来也方便一些,我测试的时候用的是200)这里使用t来存储计时器的id也是为了在组件卸载的时候可以通过clearInterval(this.t)来清理掉这个定时器,防止内存泄漏。

第四步

第四步,在真机上调试vue组件。到第三部结束后,前面的理论整理完毕,那么来看看在vue页面真机上的效果吧——我们都知道uni-app让页面的宽度都为750rpx来进行适配的,那么我这里的样式的单位改成rpx肯定能正常适配各个屏幕吧,而且改起来很方便。诶,实际效果怎么翻车了?这里是困扰我的第二个难点。

我来详细说一下当时的情况吧。UI给的图是一倍图,圆的宽度是52px,根据我的经验,拿到uni-app上来,肯定是数字乘以2后改成rpx单位啊,所以我把52px改成了104rpx。结果放到手机上的效果一言难尽,可以说是大致效果都有,就是细节上差强人意——如整体看起来不够圆;右边的半圆和左边的半圆之间有一条黑色的缝,并没有完全合在一起;黄色的圆环在边缘不够圆润,粗细不均匀;最明显的是,黄色的小圆位置偏离了圆环,好像从圆环这里脱落了一样。在细节上多多少少有偏差,难道是我计算的不对?我仔细排查了多遍都没有用,最后才明白,解铃还须系铃人,问题就出在rpx这个单位上——不同的设备,屏幕的逻辑像素宽度五花八门,却始终以750rpx为100%的宽度,可想而知rpx计算出来有多少位小数,而我之前测试的圆润效果,是在HTML文件里用px单位做的,所以这里也必须用px来做,否则用非整数像素的rpx没办法做的那么精确

那么问题又来了——我们这里用的px,是屏幕的逻辑像素,也就是300多到400多点,104px的圆环显得太大了,而且在不同屏幕上效果差别很大,如何让它大小正常呢?我们需要计算px与rpx大小之间的比例了,屏幕宽度可以用uni.getSystemInfo来获取。假设屏幕宽度为a px, 正好等于750 rpx, 所以1 rpx = ( a / 750 )px = 1 px * ( a / 750 )。我们在组件被挂载后马上开始计算它。

mounted() {
    uni.getSystemInfo({
		success: (res) => {
			console.log(res.windowWidth)
			this.rpxRatio = (res.windowWidth / 750)
		}
	})
},

我准备了一个数据较rpxRatio,用于存储rpx与px的比例,然后用来对左边的圆进行整体缩放:<view class="time-warpper" :style="{'transform': 'scale('+ rpxRatio +')'}">,这样就把里面使用到的1px整体转成1rpx了,由于是整体缩小,所以缩小的比例一致,并没有乱掉。transform的缩放是看起来更小,并不影响实际占位空间的,但是因为左边的圆绝对定位了,所以不会影响到右边的文字部分,记得设置左边的圆的transform-origin为left top,让它贴着自己的左上角缩小,否则看起来位置会怪怪的,因为中间的圆心还在原地。至此,在vue页面上显示效果大功告成。

第五步

第五步,在nvue页面上调试。这个项目有需要播放视频的页面,这个用vue页面做不了,只能用nvue的页面,所以也需要做nvue版本的组件。因为vue相对于nvue上写起来更简单更可控,所以我会先做vue页面的版本,然后再把它复制一份,改成nvue的后缀。我们之前的考虑那么周全,现在应该也没什么大问题吧?结果效果让我很不满意,对nvue页面的适配是第三个难点

大体上在功能和计算方便并没有区别,区别主要在样式上,我可以告诉大家有哪些是需要注意的——

1. 在nvue中,写给<view>的border-radius不可以合在一起写,否则看起来没有圆角。这里的半圆要是没有圆角,那简直转了个寂寞。记得border-radius写多个值的顺序是上左,上右,下右,下左,拆开来写的顺序最好跟这个一致,否则容易写错,导致悲剧。

2.nvue中不支持display属性,组件如何显示隐藏?我最常用的显示隐藏方法就是修改display,但是nvue的display是flex,并且不接受修改,怎么办?我之前的一篇博客说过,可以把宽度或高度改成0或者正常大小,这个组件的高度是确定的,所以可以对高度下手——isShow为false的时候高度为0,isShow为true时,高度为104rpx。

3.nvue中不支持z-index。这个问题只能用“先来后到”的办法解决,也就是谁写在后面,谁就盖住别人。这里的圆环要盖住右边文字提示的左半部分,可是我们习惯把右边的东西写在左边后面,所以加了定位没有用,左边的圆环被右边的提示盖住了。那就只能把右边的提示写到左边的圆环前面。

4.nvue中不支持turn这个单位。我测试的时候,看到圆环边缘处不够圆润,也就是黄色的小圆没有随着圆环转动,联想到小圆转动的单位跟半圆的单位不同,我把1 turn当成 360 deg进行换算后,小圆也可以正常转动了

另外还有之前的博客就提到过的一些要点——文字不能写在<view>里,要单独写到<text>里; CSS样式写的时候只能用单类名选择器,其他选择器和复杂的选择器都不能生效,这些是我首先会修改的地方,我是改完了这些再来改以上四个问题的 

解决了以上四个问题,终于,nvue版本的组件在页面上效果也正常了。以上这五步,我折腾了大半天的时间才搞定的。

总结

这是一个比较简单,看起来也比较有趣的倒计时组件。由于文字提示和组件位置可以传入,所以也容易复用。

要做好这个组件,首先要有能正常转动的圆形进度条动画,然后要弄清楚的是有哪些数据应该由父组件传入,还有哪些数据可以从父组件传入的数据中计算出来,要靠父组件传值来实现动画,再就是要注意样式适配的细节,注意不要用rpx单位来做过于精细的效果展示,否则细节上表现不佳,在nvue页面上,注意样式要写得保守一些,最好查阅weex文档再决定什么样式可以用,遇到不起作用的样式不要慌,多想想曲线救国的备用方案。

希望能帮助到有类似需求的大家。如果你有好的意见和建议,也希望告诉我呀。

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值