当前位置:   article > 正文

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

uniapp 圆形倒计时

今天来分享一个我自己写的圆形倒计时组件。我在用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。写出来就像这样:

  1. props: {
  2. // 显示的位置
  3. top: {
  4. type: Number,
  5. default: 0
  6. },
  7. // 提示文字
  8. tip: {
  9. type: String,
  10. default: '看视频 赚蜜獾'
  11. },
  12. // 是否显示,为true显示
  13. isShow: {
  14. type: Boolean,
  15. default: false
  16. },
  17. // 还剩多少时间
  18. time: {
  19. type: Number,
  20. default: 100
  21. },
  22. // 共多少时间
  23. total: {
  24. type: Number,
  25. default: 180
  26. }
  27. },

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

三、详细过程

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

第一步

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

第二步

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

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>环形进度条</title>
  6. <style>
  7. .wrapper {
  8. position: absolute;
  9. top: 0;
  10. right: 0;
  11. bottom: 0;
  12. left: 0;
  13. width: 104px;
  14. height: 104px;
  15. margin: auto;
  16. /* 加上背景色和圆角 */
  17. border-radius: 50%;
  18. /* background-color: #1A1A1A; */
  19. background: #FFD600;
  20. }
  21. .container {
  22. position: absolute;
  23. top: 0;
  24. bottom: 0;
  25. width: 52px;
  26. overflow: hidden;
  27. }
  28. .halfCir {
  29. width: 52px;
  30. height: 104px;
  31. background-color: #1A1A1A;
  32. /* background: #FFD600; */
  33. }
  34. .container1 {
  35. left: 52px;
  36. }
  37. .container1 .halfCir {
  38. left: 0;
  39. border-radius: 0 104px 104px 0;
  40. transform-origin: 0 50%;
  41. animation: halfCir1 4s infinite linear;
  42. }
  43. .container2 {
  44. left: 0;
  45. }
  46. .container2 .halfCir {
  47. border-radius: 104px 0 0 104px;
  48. transform-origin: 52px 52px;
  49. animation: halfCir2 4s infinite linear;
  50. }
  51. @keyframes halfCir1 {
  52. 50%, 100% {
  53. transform: rotateZ(180deg);
  54. }
  55. }
  56. @keyframes halfCir2 {
  57. 0%, 50% {
  58. transform: rotateZ(0);
  59. }
  60. 100% {
  61. transform: rotateZ(180deg);
  62. }
  63. }
  64. .wrapper::after {
  65. position: absolute;
  66. top: 8px;
  67. left: 8px;
  68. width: 88px;
  69. height: 88px;
  70. border-radius: 50%;
  71. content: "";
  72. background-color: #000;
  73. }
  74. .cir {
  75. position: absolute;
  76. top: 0;
  77. right: 0;
  78. left: 0;
  79. width: 8px;
  80. height: 8px;
  81. margin: auto;
  82. /* background: #1a1a1a; */
  83. background: #FFD600;
  84. border-radius: 50%;
  85. }
  86. .cir2 {
  87. transform-origin: 50% 52px;
  88. animation: cir2 4s infinite linear;
  89. }
  90. @keyframes cir2 {
  91. 100% {
  92. transform: rotateZ(360deg);
  93. }
  94. }
  95. .number {
  96. position: absolute;
  97. top: 50%;
  98. left: 50%;
  99. z-index: 10;
  100. transform: translate(-50%, -50%);
  101. font-size: 36px;
  102. color: #FFD600;
  103. }
  104. </style>
  105. </head>
  106. <body>
  107. <p>我做的改动如下:1.最外层加上圆角和背景色。
  108. 2. 给最里面的小圆,也就是遮盖层,也就是外圆的after伪元素,加上背景色,这里的背景色是中间的颜色,即黑色。
  109. 3. 往里面加上了数字的显示,定位到最中间,z-index最大。
  110. 4. 设置里面的半圆的背景颜色,因为黄色要越来越大,所以黄色是最底层的颜色,灰黑是上层半圆的颜色色</p>
  111. <div class="wrapper">
  112. <div class="container container1">
  113. <div class="halfCir"></div>
  114. </div>
  115. <div class="container container2">
  116. <div class="halfCir"></div>
  117. </div>
  118. <!-- 这两个是控制圆角的小圆 -->
  119. <div class="cir cir1"></div>
  120. <div class="cir cir2"></div>
  121. <span class="number">58</span>
  122. </div>
  123. </body>
  124. </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秒,黄色进度条才走满一圈的效果,并不是一秒走一圈的效果)

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

  1. <!-- 左边,倒计时的部分 -->
  2. <view class="time-warpper" :style="{'transform': 'scale('+ rpxRatio +')'}">
  3. <!-- 右半边圆 -->
  4. <view class="container container1">
  5. <!-- 里面的半圆 -->
  6. <view class="half half-circle1" :style="{'transform': 'rotate('+ half1Turn +'deg)'}"></view>
  7. </view>
  8. <!-- 左半边圆 -->
  9. <view class="container container2">
  10. <!-- 里面的半圆 -->
  11. <view class="half half-circle2" :style="{'transform': 'rotate('+ half2Turn +'deg)'}"></view>
  12. </view>
  13. <!-- 这两个是控制圆角的小圆 -->
  14. <view class="cir cir1"></view>
  15. <view class="cir cir2" :style="{'transform': 'rotate('+ ratio +'turn)'}"></view>
  16. <!-- 当前秒数 -->
  17. <text class="time-text">{{time}}</text>
  18. </view>

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

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

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

  1. computed: {
  2. // 计算这段时间已经过去了百分之多少
  3. ratio() {
  4. return (this.total - this.time) / this.total;
  5. },
  6. // 右边的半圆转的度数
  7. half1Turn() {
  8. // 大于50%则不转,停留在180deg这里
  9. if(this.ratio > 0.5) {
  10. return 180;
  11. } else {
  12. // 小于50%则判断现在占50%的多少,根据这个比例乘180
  13. return (this.ratio / 0.5) * 180;
  14. }
  15. },
  16. // 左边边的半圆转的度数
  17. half2Turn() {
  18. // 小于50%则不转,停留在0这里
  19. if(this.ratio < 0.5) {
  20. return 0;
  21. } else {
  22. // 大于50%则判断大于50%的部分占50%的多少,根据这个比例乘180
  23. return ((this.ratio - 0.5) / 0.5) * 180;
  24. }
  25. }
  26. }

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

  1. // 让倒计时组件动起来的测试代码
  2. this.t = setInterval(() => {
  3. if(this.currentTime > 0) {
  4. this.currentTime--
  5. }
  6. }, 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 )。我们在组件被挂载后马上开始计算它。

  1. mounted() {
  2. uni.getSystemInfo({
  3. success: (res) => {
  4. console.log(res.windowWidth)
  5. this.rpxRatio = (res.windowWidth / 750)
  6. }
  7. })
  8. },

我准备了一个数据较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文档再决定什么样式可以用,遇到不起作用的样式不要慌,多想想曲线救国的备用方案。

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/117053?site
推荐阅读
相关标签
  

闽ICP备14008679号