前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何实现一个圆弧倒计时进度条

如何实现一个圆弧倒计时进度条

作者头像
WecTeam
发布2020-07-17 11:25:14
2.4K0
发布2020-07-17 11:25:14
举报
文章被收录于专栏:WecTeamWecTeamWecTeam

一、前言

最近的项目中,需要实现一个圆弧形倒计时进度条,对于本来 css 知识薄弱的我当场就懵逼,脑海里总是不断思考如何实现,不幸的是脑袋里没能蹦出半个想法。然后立马百度查看网上是否有相似的解决方案,百度下来初步知道如何来实现了,那我们就一步一步从 0 到有开始这段旅程。

首先展示一下最终的成果,最终效果图如下:

实现要点:浅色圆弧需要分成左右两边,左右两边都需要用一个同心原来实现,亮色圆弧也需要左右分开,各自用一个同心圆来实现。让我们开始吧!

二、实现步骤

添加容器

让整个容器是 position: fixed 方便可以在整个页面上随意放置 html 代码:

<div class="task-container"></div>

css 代码:

.task-container {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
    width: 65px;
    height: 65px;
    display: flex;
    justify-content: center;
    align-items: center;
}

画底盘

加点阴影,让它看起来有点立体的感觉 html 代码:

<div class="task-container">
    <div class="task-cicle"></div>
</div>

css 代码:

.task-container {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
    width: 65px;
    height: 65px;
    display: flex;
    justify-content: center;
    align-items: center;
 
    .task-cicle {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 53px;
        height: 53px;
        border-radius: 50%;
        background: #FFFFFF;
        box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
    }
}

效果:

重点来了,接下来实现圆弧

我们先画右圆弧,我们用右半边矩形来实现,右半圆只设置上方和右边的边框颜色 html 代码:

<div class="task-container">
    <div class="task-cicle">
        <div class="task-inner">
            <div class="right-cicle">
                <div class="cicle-progress cicle1-inner"></div>
            </div>
        </div>
    </div>
</div>

css 代码:

.task-container {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
    width: 65px;
    height: 65px;
    display: flex;
    justify-content: center;
    align-items: center;


    .task-cicle {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 53px;
        height: 53px;
        border-radius: 50%;
        background: #FFFFFF;
        box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
    }

    .task-inner {
        position: relative;
        width: 46px;
        height: 46px;
    }

    .right-cicle {
        width: 23px;
        height: 46px;
        position: absolute;
        top: 0;
        right: 0;
        overflow: hidden;
    }

    .cicle-progress {
        position: absolute;
        top: 0;
        width: 46px;
        height: 46px;
        border: 3px solid transparent;
        box-sizing: border-box;
        border-radius: 50%;
    }

    .cicle1-inner {
        left: -23px;
        border-right: 3px solid #e0e0e0;
        border-top: 3px solid #e0e0e0;
        transform: rotate(-15deg);
    }
}

right-cicle 需要设置 overflow: hidden;对子元素超出的部分进行裁剪。cicle1-inner 中的旋转-15 度,其实可以根据设计稿来调整你需要展示的弧度 如果父节点,没有进行裁剪,右半圆就会延伸到左边

裁剪之后的效果

画左边的弧

接下来根据同样的原理画左边的弧。左边的圆,只设置上方和左边的边框颜色 html 代码:

<div class="task-container">
    <div class="task-cicle">
        <div class="task-inner">
            <div class="right-cicle">
                <div class="cicle-progress cicle1-inner"></div>
            </div>
            <div class="left-cicle">
                <div class="cicle-progress cicle2-inner"></div>
            </div>
        </div>
    </div>
</div>

css 代码:

.left-cicle {
    width: 23px;
    height: 46px;
    position: absolute;
    top: 0;
    left: 0;
    overflow: hidden;
}

.cicle2-inner {
    left: 0;
    border-left: 3px solid #e0e0e0;
    border-top: 3px solid #e0e0e0;
    transform: rotate(15deg);
}

效果如下:

ok,圆弧的基本轮廓已经完成,接下来实现亮色进度条,进度条也是分左右边各自实现

画右半边进度条

右半边圆只设置上方和右边的边框颜色 html 代码:

<div class="task-container">
    <div class="task-cicle">
        <div class="task-inner">
            <div class="right-cicle">
                <div class="cicle-progress cicle1-inner"></div>
            </div>
            <div class="left-cicle">
                <div class="cicle-progress cicle2-inner"></div>
            </div>
            <div class="right-cicle">
                <div class="cicle-progress cicle3-inner" id="rightCicle"></div>
            </div>
        </div>
    </div>
</div>

css 代码:

.cicle3-inner {
    left: -23px;
    border-right: 3px solid #feca02;
    border-top: 3px solid #feca02;
    transform: rotate(-135deg);
}

效果如下:

为什么是旋转-135 度?进度条是从左边蔓延到右边的,让亮色进度条旋转到左右两边的临界点,也就是初始角度是-135 度,随着时间推移增加旋转角度,进度条就蔓延到右边了

转到哪个角度为止呢?转到亮色边框和右边灰色边框重合,也就是-15 度,那么右边亮色进度条的旋转角度范围就是-135 度到-15 度,共 120 度的。

右半边进度条已经完成,初始角度是-135 度,随着时间的推移,慢慢旋转到-15 度的位置

画左半边的进度条

左半圆只设置上方和左边的边框颜色 html 代码:

<div class="task-container">
    <div class="task-cicle">
        <div class="task-inner">
            <div class="right-cicle">
                <div class="cicle-progress cicle1-inner"></div>
            </div>
            <div class="left-cicle">
                <div class="cicle-progress cicle2-inner"></div>
            </div>
            <div class="right-cicle">
                <div class="cicle-progress cicle3-inner" id="rightCicle"></div>
            </div>
            <div class="left-cicle">
                <div class="cicle-progress cicle4-inner" id="leftCicle"></div>
            </div>
        </div>
    </div>
</div>

css 代码:

.cicle4-inner {
    left: 0;
    border-left: 3px solid #feca02;
    border-top: 3px solid #feca02;
    transform: rotate(195deg);
}

效果如下(为了演示,父节点为设置了 overflow: inherit;不裁剪,能更清楚来龙去脉):

为什么要旋转 195 度?进度条是从左边开始由无到有的,我们让亮色进度条旋转到左边灰色圆弧起始点的临界点位置,随着时间的推移增加旋转角度。左边进度条要转 120 度,所以左边进度条旋转角度范围:195 到 315 度 我们把父节点的 overflow 设置回原来的 hidden,对子节点超出的部分进行裁剪。

what?裁剪之后还露出了一个小尾巴,如何把这个小尾巴给掩盖掉?这时候我们需要在左边再画一个同心圆来遮盖掉它

画遮盖圆

注意:遮罩圆边框宽度要比左边亮色进度条圆的边框宽度要大,不然会遮盖不完全,会出现金色余晖,且要和亮色进度条是同心圆 html 代码:

<div class="task-container">
    <div class="task-cicle">
        <div class="task-inner">
            <div class="right-cicle">
                <div class="cicle-progress cicle1-inner"></div>
            </div>
            <div class="left-cicle">
                <div class="cicle-progress cicle2-inner"></div>
            </div>
            <div class="right-cicle">
                <div class="cicle-progress cicle3-inner" id="rightCicle"></div>
            </div>
            <div class="left-cicle">
                <div class="cicle-progress cicle4-inner" id="leftCicle"></div>
            </div>
            <div class="left-cicle">
                <div class="mask-inner"></div>
            </div>
        </div>
    </div>
</div>

css 代码(为了展示遮罩圆是完全覆盖的,我把父节点的 overflow: inherit;不裁剪,圆的边框颜色设置为蓝色):

.mask-inner {
    position: absolute;
    left: 0;
    top: 0;
    width: 39px;
    height: 39px;
    // border: 4px solid transparent;
    border: 4px solid blue;
    border-radius: 50%;
    // border-left: 4px solid #FFFFFF;
    // border-top: 4px solid #FFFFFF;
    // transform: rotate(195deg);
}

看,我们的遮罩圆已经完全遮罩了其他圆,遮盖圆和左边进度条圆一样,都是旋转 195 度,只设置上方和左边的边框颜色,边框颜色是和底盘颜色一样,我们把父节点 overflow 设置为 hidden 裁剪 css 代码:

.mask-inner {
    position: absolute;
    left: 0;
    top: 0;
    width: 39px;
    height: 39px;
    border: 4px solid transparent;
    border-radius: 50%;
    border-left: 4px solid blue;
    border-top: 4px solid blue;
    transform: rotate(197deg);
}

蓝色部分就是我们的小尾巴的位置,我们用白色替换蓝色边框

.mask-inner {
    position: absolute;
    left: 0;
    top: 0;
    width: 39px;
    height: 39px;
    border: 4px solid transparent;
    border-radius: 50%;
    border-left: 4px solid #FFFFFF;
    border-top: 4px solid #FFFFFFl
    transform: rotate(197deg);
}

效果:

哇,看看,小尾巴已经不见了。 如果遮盖圆和左边亮色进度条设置一样的边框大小,会出现金色边

好吧,样式方面已经基本完成,其他点缀的样式就不在这里列出了,可以看看下面的源码。要让进度条动起来,需要通过 js 来操作,js 里的源码我已经写了比较清楚的注释,方便理解。 html 代码:

<div class="task-container">
    <div class="task-cicle">
        <div class="task-inner">
            <div class="right-cicle">
                <div class="cicle-progress cicle1-inner"></div>
            </div>
            <div class="left-cicle">
                <div class="cicle-progress cicle2-inner"></div>
            </div>
            <div class="right-cicle">
                <div class="cicle-progress cicle3-inner" id="rightCicle"></div>
            </div>
            <div class="left-cicle">
                <div class="cicle-progress cicle4-inner" id="leftCicle"></div>
            </div>
            <div class="left-cicle">
                <div class="mask-inner"></div>
            </div>
            <div class="inner">
                <img src="https://img12.360buyimg.com/img/jfs/t1/150018/30/1001/2042/5eec2f8eEfd3c853a/e7982308423ce71a.png" alt="" srcset="">
                <div class="water-count">10</div>
            </div>
        </div>
        <div class="task-bottom">
            <div class="task-btn" id="time"></div>
        </div>
    </div>
</div>


<script>
    const rightCicle = document.getElementById('rightCicle');
    const leftCicle = document.getElementById('leftCicle');
    const timeDom = document.getElementById('time');
    let isStop = false;
    let timer;
    const totalTime = 10; // 总时间
    const halfTime = totalTime / 2; // 总时间的一半
    const initRightDeg = -135; // 右半边进度条初始角度
    const initLeftDeg = 195; // 左半边进度条初始角度
    const halfCicle = 120; // 左右连边各要转的总角度
    const perDeg = 120 / halfTime; // 每秒转的角度
    let inittime = 10;
    let begTime; // 倒计时开始时间戳
    let stopTime; // 倒计时停止时间戳

    function run() {
        const time = inittime;
        let animation;
        if (time > halfTime) {
            // 左半边还没转完
            // 左半边:动画的初始角度=左半边进度条初始角度+已经转的角度,最终角度=初始角度+120 度,动画持续时间=左半边还剩需要转的时间
            // 右半边:动画的初始角度=右半边进度条初始角度,最终角度=初始角度+120 度,动画持续时间=一半的时间,动画延迟=左半边还剩需要转的时间
            animation = `
                @keyframes task-left {
                    0% {
                        transform: rotate(${initLeftDeg + (totalTime - time) * perDeg}deg);
                    }
                    100% {
                        transform: rotate(${initLeftDeg + halfCicle}deg);
                    }
                }
                .task-left {
                    animation-name: task-left;
                    animation-duration: ${time - halfTime}s;
                    animation-timing-function: linear;
                    animation-delay: 0s;
                    animation-fill-mode: forwards;
                    animation-direction: normal;
                    animation-iteration-count: 1;
                }
                @keyframes task-right {
                    0% {
                        transform: rotate(${initRightDeg}deg);
                    }
                    100% {
                        transform: rotate(${initRightDeg + halfCicle}deg);
                    }
                }
                .task-right {
                    animation-name: task-right;
                    animation-duration: ${halfTime}s;
                    animation-timing-function: linear;
                    animation-delay: ${time - halfTime}s;
                    animation-fill-mode: forwards;
                    animation-direction: normal;
                    animation-iteration-count: 1;
                }
            `;
        } else {
            // 左半边已经转完
            // 左半边动画:起始帧和重点帧都=左半边进度条初始角度+120 度
            // 右半边动画:动画的初始角度=右半边进度条初始角度+右半边已经角度,最终角度=初始角度+120 度,动画持续时间=剩余时间
            animation = `
                @keyframes task-left {
                    0% {
                        transform: rotate(${initLeftDeg + halfCicle}deg);
                    }
                    100% {
                        transform: rotate(${initLeftDeg + halfCicle}deg);
                    }
                }
                .task-left {
                    animation-name: task-left;
                    animation-duration: 0s;
                    animation-timing-function: linear;
                    animation-delay: 0s;
                    animation-fill-mode: forwards;
                    animation-direction: normal;
                    animation-iteration-count: 1;
                }
                @keyframes task-right {
                    0% {
                        transform: rotate(${initRightDeg + (halfTime - time) * perDeg}deg);
                    }
                    100% {
                        transform: rotate(${initRightDeg + halfCicle}deg);
                    }
                }
                .task-right {
                    animation-name: task-right;
                    animation-duration: ${time}s;
                    animation-timing-function: linear;
                    animation-delay: 0s;
                    animation-fill-mode: forwards;
                    animation-direction: normal;
                    animation-iteration-count: 1;
                }
            `;
        }
        // 增加动画暂停和开始类
        animation += `.stop {animation-play-state: paused;} .run {animation-play-state: running;}`
        const styleDom = document.createElement('style');
        styleDom.type = 'text/css';
        styleDom.innerHTML = animation;
        document.getElementsByTagName('head').item(0).appendChild(styleDom);
        leftCicle.classList.add('task-left');
        rightCicle.classList.add('task-right');
        begTime = Date.now();
        countDown();
    }

    function countDown() {
        if (begTime && stopTime) {
            // 从 1 秒到 1.6 秒后暂停,动画一直在走,而倒计时因为未到 2 秒,定时器就清除了,下次还是会从 1 开始计时,
            // 这就会导致倒计时和动画的不同步,之类稍微校正一下,如果结束时间和开始时间取余数大于 500,就把倒计时-1 秒
            const runtime = stopTime - begTime;
            console.log(runtime % 1000);
            if (runtime % 1000 > 500) {
                inittime -= 1;
            }
        }
        begTime = Date.now();
        timeDom.innerText = `${inittime}秒后获得 `;
        timer = setInterval(() => {
            inittime -= 1;
            timeDom.innerText = `${inittime}秒后获得 `;
            if (inittime <= 0) {
                clearInterval(timer);
            }
        }, 1000);
    }
    // 点击可暂停倒计时和动画
    timeDom.addEventListener('click', () => {
        if (isStop) {
            isStop = false;
            countDown();
            leftCicle.classList.remove('stop');
            leftCicle.classList.add('run');
            rightCicle.classList.remove('stop');
            rightCicle.classList.add('run');
        } else {
            stopTime = Date.now();
            isStop = true;
            clearInterval(timer);
            leftCicle.classList.remove('run');
            leftCicle.classList.add('stop');
            rightCicle.classList.remove('run');
            rightCicle.classList.add('stop');
        }
    }, false);

    run();
</script>

css 代码:

.task-container {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    margin: auto;
    width: 65px;
    height: 65px;
    display: flex;
    justify-content: center;
    align-items: center;


    .task-cicle {
        display: flex;
        justify-content: center;
        align-items: center;
        width: 53px;
        height: 53px;
        border-radius: 50%;
        background: #FFFFFF;
        box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
    }

    .task-inner {
        position: relative;
        width: 46px;
        height: 46px;
    }

    .right-cicle {
        width: 23px;
        height: 46px;
        position: absolute;
        top: 0;
        right: 0;
        overflow: hidden;
    }

    .cicle-progress {
        position: absolute;
        top: 0;
        width: 46px;
        height: 46px;
        border: 3px solid transparent;
        box-sizing: border-box;
        border-radius: 50%;
    }

    .cicle1-inner {
        left: -23px;
        border-right: 3px solid #e0e0e0;
        border-top: 3px solid #e0e0e0;
        transform: rotate(-15deg);
    }

    .left-cicle {
        width: 23px;
        height: 46px;
        position: absolute;
        top: 0;
        left: 0;
        overflow: hidden;
    }

    .cicle2-inner {
        left: 0;
        border-left: 3px solid #e0e0e0;
        border-top: 3px solid #e0e0e0;
        transform: rotate(15deg);
    }

    .cicle3-inner {
        left: -23px;
        border-right: 3px solid #feca02;
        border-top: 3px solid #feca02;
        transform: rotate(-135deg);
    }

    .cicle4-inner {
        left: 0;
        border-left: 3px solid #feca02;
        border-top: 3px solid #feca02;
        transform: rotate(195deg);
    }

    .mask-inner {
        position: absolute;
        left: 0;
        top: 0;
        width: 39px;
        height: 39px;
        border: 4px solid transparent;
        border-radius: 50%;
        border-left: 4px solid #FFFFFF;
        border-top: 4px solid #FFFFFF;
        transform: rotate(195deg);
    }

    .inner {
        position: absolute;
        left: 0;
        top: -2px;
        right: 0;
        bottom: 0;
        width: 22px;
        height: 26px;
        margin: auto;

        img {
            width: 100%;
            height: 100%;
        }
    }

    .water-count {
        position: absolute;
        top: 8px;
        left: 50%;
        transform: translateX(-50%);
        font-family: "JDZhengHei-01-Regular";
        font-size: 12px;
        color: #FFFFFF;
    }

    .task-bottom {
        display: flex;
        justify-content: center;
        align-items: center;
        position: absolute;
        width: 60px;
        height: 15px;
        left: 50%;
        transform: translateX(-50%);
        bottom: 2px;
    }

    .task-btn {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 15px;
        border-radius: 7px;
        background-image: linear-gradient(-45deg, #FEB402 0%, #FF8407 100%);
        font-size: 8px;
        color: #FFFFFF;
        line-height: 15px;
        padding: 0 4px;
    }
}

三、总结

浅色圆弧和亮色进度条的实现比较绕,一眼看过去不太好理解,我们可以把每一步拆分开。4 个圆弧的实现,父节点都进行了裁剪,裁剪之后很难看出子元素原本的样子,我们可以先把裁剪去掉,看看未裁剪时,各个圆的表现。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WecTeam 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、实现步骤
    • 添加容器
      • 画底盘
        • 重点来了,接下来实现圆弧
          • 画左边的弧
            • 画右半边进度条
              • 画左半边的进度条
                • 画遮盖圆
                • 三、总结
                相关产品与服务
                容器服务
                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档