今天来分享一个我自己写的圆形倒计时组件。我在用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文档再决定什么样式可以用,遇到不起作用的样式不要慌,多想想曲线救国的备用方案。
希望能帮助到有类似需求的大家。如果你有好的意见和建议,也希望告诉我呀。