😎使用css也能轻松搞定倒计时效果!

750 阅读14分钟

1. 前言

倒计时是许多网站上常见的一个功能,它能够增强网站和用户互动方面的体验。一般来说,开发者们会使用JavaScript来实现网页的动态行为,但是使用纯CSS也可以创建一个功能完善且视觉效果不错的倒计时器。

在本教程中,我们将探讨JavaScript和纯CSS这两种实现方式,并使用Chrome开发者工具来对比两种实现方案的性能表现,详细分析每种实现方式的优点和缺点。

2. 通过JavaScript实现倒计时

我们将开始实现一个每秒更新一次的简单的倒计时效果,倒计时将包括一个开始和暂停按钮,以控制其操作。

2.1 HTML 结构

首先我们创建如下的HTML结构:

<div class="container">
    <div class="controls">
      <button id="startBtn">Start</button>
      <button id="pauseBtn">Pause</button>
    </div>
    <div class="countdown-container">
      <div class="countdown"></div>
    </div>
</div>

这段HTML代码中包含了一个容器,用于放置控制按钮和倒计时显示区域。

2.2 CSS 样式

接下来,我们创建如下的CSS样式:

body {
  background-color: black;
  font-family: Arial, sans-serif;
  height: 100%;
}

.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.controls {
  width: 200px;
  margin-top: 10%;
  display: flex;
  justify-content: space-between;
  flex-direction: row;
  flex-wrap: wrap;
}

.countdown-container {
  position: relative;
  width: 200px;
  height: 200px;
  margin-top: 32px;
  border: 0.4em solid #9b51e0;
}

button {
  font-size: 1.5em;
  border: none;
  padding: 0.3em;
  background-color: #9b51e0;
  border-radius: 0.4em;
  color: white;
}

.countdown {
  position: relative;
  width: 100%;
  height: 100%;
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 5em;
  color: #9b51e0;
}

这段CSS代码定义了倒计时计时器的样式。body的背景设置为黑色,.container类用于居中其内容并提供元素之间的间距。.controls类用于设置开始和暂停按钮的样式,确保它们均匀分布。.countdown-container类定义了倒计时显示的尺寸和外观,包括边框和外边距。

2.3 JavaScript 代码

接下来,我们创建如下的JavaScript代码:

window.addEventListener("load", () => {
  const startBtn = document.getElementById('startBtn');
  const pauseBtn = document.getElementById('pauseBtn');
  const countdownView = document.getElementsByClassName('countdown')[0];

  let totalTime = 10;
  let timeLeft;
  let countDownIntervalID;
  let isPaused = false;

  pauseBtn.style.display = 'none';
}); 

这段JavaScript代码中,我们为页面加载添加了一个load事件监听器,当页面加载完成后,我们通过id和类名获取了 startBtn(开始按钮)、pauseBtn(暂停按钮)和 countdownView(倒计时显示区域)这些DOM元素。同时,我们还设置了一些初始变量:totalTime(总时间)、timeLeft(剩余时间)、countDownIntervalID(倒计时间隔ID)和 isPaused(是否暂停状态)。此外,我们还将暂停按钮设置为初始隐藏状态。

现在,让我们为开始和暂停按钮添加click事件监听器:

startBtn.addEventListener('click', startOrStopTimer);
pauseBtn.addEventListener('click', pauseOrResumeTimer);

这些代码将click事件监听器添加到开始和暂停按钮上。startOrStopTimerpauseOrResumeTimer函数将在后面定义,用于处理按钮的点击事件。

接下来,我们来定义startOrStopTimer函数:

function startOrStopTimer() {
  startBtn.innerHTML = startBtn.innerHTML === 'Start' ? 'Stop' : 'Start';
  if (countDownIntervalID === undefined && !isPaused) {
    timeLeft = totalTime;
    startTimer();
    pauseBtn.style.display = 'inline';
  } else {
    stopTimer();
    countdownView.innerHTML = '';
    pauseBtn.style.display = 'none';
    isPaused = false;
    pauseBtn.innerHTML = 'Pause';
  }
}

在这个函数中,我们点击开始按钮,开始按钮的文本,将在StartStop之间切换。如果倒计时未运行且未暂停,我们将timeLeft设置为totalTime并开始计时。否则,我们停止计时并重置页面。

接下来,我们来定义startTimer函数:

function startTimer() {
  countDownIntervalID = setInterval(() => {
    countdownView.innerHTML = timeLeft;

    if (timeLeft === 0) {
      stopTimer();
      startBtn.innerHTML = 'Start';
      pauseBtn.style.display = 'none';
      countdownView.innerHTML = '';
    } else {
      timeLeft = timeLeft - 1;
    }
  }, 1000);
}

这个函数设置了一段时间间隔,每隔1秒更新一次倒计时。如果timeLeft达到0,我们将停止计时,重置开始按钮的文本,并隐藏暂停按钮。

接下来,我们来定义stopTimer函数:

function stopTimer() {
  if (countDownIntervalID !== undefined) {
    clearInterval(countDownIntervalID);
    countDownIntervalID = undefined;
  }
}

这个函数用于停止计时,并清除计时器ID——countDownIntervalID

最后,我们来定义pauseOrResumeTimer函数:

function pauseOrResumeTimer() {
  isPaused = !isPaused;
  pauseBtn.innerHTML = isPaused ? 'Resume' : 'Pause';

  if (countDownIntervalID !== undefined) {
    stopTimer();
  } else {
    startTimer();
  }
}

在这个函数中,我们实现切换暂停状态和按钮文本。暂停按钮文本会在PauseResume之间切换。如果计时器正在运行,我们停止倒计时;否则,我们会再次开始倒计时。

现在在浏览器中打开我们的网页,你会看到如下效果:

我们可以再页面上操作一下来测试倒计时的效果。

2.4 添加环形进度条

为了增强倒计时计时器的用户体验,我们将添加一个环形进度条,以给用户一个剩余时间的视觉感受。

首先,我们需要修改我们的HTML代码,以包含环形进度条元素。在index.html中,我们在.countdown-containerdiv中添加一个带有.circular-progressspan元素。这个span元素将用于创建环形进度条:

<div class="countdown-container">
  <span class="circular-progress"></span>
  <div class="countdown"></div>
</div>

接下来,我们需要定义环形进度条的CSS样式。我们添加如下代码:

.countdown-container {
  ...
  /* border : 0.4em solid #9b51e0; */
}
.circular-progress {
  width: 200px;
  height: 200px;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  transition: 0.5s;
  background-color: #13171f;
}

.circular-progress::before {
  width: 185px;
  height: 185px;
  content: "";
  position: absolute;
  border-radius: 50%;
  background-color: black;
}

这段样式首先移除了.countdown-container的边框,然后设置环形进度条的尺寸和形状,以及它的位置和背景颜色。我们还添加了一个::before伪元素来创建进度条的内圆。

接下来,我们需要添加JavaScript代码来实现环形进度条的动画效果。在变量初始化代码块中添加如下代码:

const circularProgressEl = document.getElementsByClassName("circular-progress")[0];
let circularProgress;
let circularProgressIntervalID;

这段代码通过类名获取了环形进度条的DOM元素,并创建两个新变量,circularProgresscircularProgressIntervalID,用于标识动画进度条。

接下来,我们在pauseOrResumeTimer()函数后面添加如下的startCircularProgressAnimation函数:

function startCircularProgressAnimation() {
  let start = totalTime - timeLeft;
  let degreesPerSecond = 360 / totalTime;
  let degreesPerInterval = degreesPerSecond / 20;
  circularProgress = degreesPerSecond * start; 

  circularProgressIntervalID = setInterval(() => {
    if (Math.round(circularProgress) === 360) {
      clearInterval(circularProgressIntervalID);
    } else {
      circularProgress = circularProgress + degreesPerInterval;
      circularProgressEl.style.background = `conic-gradient(#9b51e0 ${circularProgress}deg, #13171f 0deg)`;
    }
  }, 50);
}

startCircularProgressAnimation函数用于计算环形进度条的起始点和旋转角度,并设置一个间隔来给环形进度条添加动画效果。

接下来,我们在startCircularProgressAnimation函数后面继续添加如下代码:

function resumeCircularProgressAnimation() {
  startCircularProgressAnimation();
}

function pauseCircularProgressAnimation() {
  clearInterval(circularProgressIntervalID);
}

function stopCircularProgressAnimation() {
  clearInterval(circularProgressIntervalID);
  circularProgressEl.style.background = `conic-gradient(#9b51e0 0deg, #13171f 0deg)`;
}

这段代码定义了resumeCircularProgressAnimationpauseCircularProgressAnimationstopCircularProgressAnimation三个函数,这些函数用于恢复、暂停和停止环形进度条动画。

最后,我们需要修改startOrStopTimerpauseOrResumeTimer这两个函数,使环形进度动画能够随着计时器一起启动和停止:

function startOrStopTimer() {
  // ...
  if (countDownIntervalID === undefined && !isPaused) {
    // ...
    startCircularProgressAnimation();
  } else {
    // ...
    stopCircularProgressAnimation();
  }
}

function pauseOrResumeTimer() {
  // ...
  if (countDownIntervalID !== undefined) {
    stopTimer();
    pauseCircularProgressAnimation();
  } else {
    startTimer();
    resumeCircularProgressAnimation();
  }
}

进过我们的修改,现在倒计时计时器已经包含了一个环形进度条动画效果。去浏览器里打开页面,点击开始按钮,你会看到如下效果:

在demo演示中,我们会看到从暂停状态到恢复倒计时的时候,进度条不是从当前进度开始,而是会动一下,那是因为倒计时的定时间隔和环形进度条动画的时间间隔不一致导致的。

3. 通过纯CSS实现倒计时

接下来,我们将使用纯CSS来实现一个每秒更新的倒计时效果。这个计时器设计简洁而实用,并且有开始按钮和暂停按钮来控制其运行。

3.1 HTML 结构

首先,我们创建如下的HTML结构:

<div class="container">
  <div class="controls">
    <input type="checkbox" id="startBtn" class="checkbox-wrapper">
    <label for="startBtn" id="startLabel">
      <span>Stop</span>
      <span>Start</span>
    </label>
    <input type="checkbox" id="pauseBtn" class="checkbox-wrapper">
    <label for="pauseBtn" id="pauseLabel">
      <span>Resume</span>
      <span>Pause</span>
    </label>
    <div class="countdown-container">
      <ul class="countdown">
        <li>10</li>
        <li>9</li>
        <li>8</li>
        <li>7</li>
        <li>6</li>
        <li>5</li>
        <li>4</li>
        <li>3</li>
        <li>2</li>
        <li>1</li>
      </ul>
    </div>
  </div>
</div>

这段代码设置了我们倒计时计时器的HTML结构。它有一个包含开始和暂停计时器控件的容器div,以及倒计时显示本身。控制div包含了两个带有label的复选框,它们也将起到开始按钮和暂停按钮的作用。我们可以通过复选框的黑魔法,来让按钮通过CSS切换其各自的状态。

倒计时容器countdown-container中包含了一个无序列表(ul),列表项(li)用于展示从10到1的倒计时数字。随着倒计时的进行,这些数字将依次显示。

3.2 CSS 样式

接下来,我们创建如下的CSS样式:

body {
  background-color: black;
  font-family: Arial, sans-serif;
  height: 100%;
}

.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.controls {
  width: 20%;
  margin-top: 10%;
  display: flex;
  justify-content: space-between;
  flex-direction: row;
  flex-wrap: wrap;
}

.countdown-container {
  position: relative;
  width: 20vw;
  height: 20vw;
  margin-top: 12%;
  border : 0.4em solid #9b51e0;
}

#startLabel span {
  display: none;
}

label {
  cursor: pointer;
  font-size: 1.5em;
  padding: 0.3em;
  background-color: #9b51e0;
  border-radius: 0.4em;
  color: white;
}

#startBtn:checked~#startLabel span:nth-child(1) {
  display: inline;
}

#startBtn:not(:checked)~#startLabel span:nth-child(2) {
  display: inline;
}

#startBtn:not(:checked)~#pauseLabel,
#pauseBtn {
  display: none;
}

#pauseLabel span {
  display: none;
}

#pauseBtn:checked~#pauseLabel span:nth-child(1) {
  display: inline;
}

#pauseBtn:not(:checked)~#pauseLabel span:nth-child(2) {
  display: inline;
}

.checkbox-wrapper {
  display: none;
}

在这个CSS文件中,我们首先为body和容器设置了一些基础样式。body采用黑色背景,容器通过flexbox实现居中对齐,并设置了上边距使其从视口顶部向下偏移。

controls div被设置为响应式,以确保按钮均匀分布。countdown-container div被设置为带有边框,稍后将被圆形进度指示器替换。

我们使用复选框的黑魔法来切换开始和暂停按钮的标签文本的可见性。根据复选框是否被选中,标签内的不同span元素会被显示。这意味着标签可以根据复选框的状态显示不同的文本(Start或Stop,Pause或Resume)。

接下来,我们在 styles.css 文件的底部添加如下代码:

.countdown {
  position: relative;
  width: 100%;
  height: 100%;
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 5em;
  color: #9b51e0;
}

.countdown li {
  position: absolute;
  opacity: 0;
  transition: opacity 1s linear;
}

#startBtn:checked~.countdown-container .countdown li:nth-child(1) {
  animation-delay: 0s;
}

#startBtn:checked~.countdown-container .countdown li:nth-child(2) {
  animation-delay: 1s;
}

#startBtn:checked~.countdown-container .countdown li:nth-child(3) {
  animation-delay: 2s;
}

#startBtn:checked~.countdown-container .countdown li:nth-child(4) {
  animation-delay: 3s;
}

#startBtn:checked~.countdown-container .countdown li:nth-child(5) {
  animation-delay: 4s;
}

#startBtn:checked~.countdown-container .countdown li:nth-child(6) {
  animation-delay: 5s;
}

#startBtn:checked~.countdown-container .countdown li:nth-child(7) {
  animation-delay: 6s;
}

#startBtn:checked~.countdown-container .countdown li:nth-child(8) {
  animation-delay: 7s;
}

#startBtn:checked~.countdown-container .countdown li:nth-child(9) {
  animation-delay: 8s;
}

#startBtn:checked~.countdown-container .countdown li:nth-child(10) {
  animation-delay: 9s;
}

@keyframes countdownAnimation {
  0%,
  10% {
    opacity: 1;
  }
  11%,
  100% {
    opacity: 0;
  }
}

#startBtn:checked~.countdown-container .countdown li {
  animation: countdownAnimation 10s steps(10) forwards;
}

#pauseBtn:checked~.countdown-container .countdown li {
  animation-play-state: paused;
}

通过这段代码,我们为倒计时列表设置了样式。倒计时类(countdown)在容器(countdown-container)中采用绝对定位,其中的列表项初始状态下通过设置 opacity: 0 来保持隐藏。

然后,我们使用关键帧(keyframes)和动画属性来创建倒计时效果。通过 animation-delay 属性设置延迟时间,使列表项依次显示。countdownAnimation 关键帧控制着每个列表项的可见性,让它们短暂显示后再次隐藏。

当暂停按钮被选中时,我们通过 animation-play-state 属性来暂停动画效果。

最后,我们可以在浏览器中打开网页,测试一下倒计时效果。

3.3 实现环形进度条

为了让倒计时器在视觉上更具吸引力,我们可以添加一个显示剩余时间的环形进度条。为此,我们需要对 HTML 和 CSS 代码进行如下修改:

首先,用以下代码替换 index.html 文件中的 countdown-container div元素:

<div class="countdown-container">
  <span class="circular-progress">
  </span>
  <ul class="countdown">
    <li>10</li>
    <li>9</li>
    <li>8</li>
    <li>7</li>
    <li>6</li>
    <li>5</li>
    <li>4</li>
    <li>3</li>
    <li>2</li>
    <li>1</li>
  </ul>
</div>

在这段代码中,我们在 countdown-container div 元素内部添加了一个 class 为 circular-progress 的 span 元素。

接下来,我们需要修改 styles.css 文件中的样式。将以下代码添加到文件末尾:

.countdown-container {
  ...
  /* border : 0.4em solid #9b51e0; */
}

.circular-progress {
  width: 20vw;
  height: 20vw;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  transition: 0.5s;
  background: conic-gradient(#9b51e0 var(--angle), #13171f 0deg);
}

.circular-progress::before {
  width: 18.5vw;
  height: 18.5vw;
  content: "";
  position: absolute;
  border-radius: 50%;
  background-color: black;
}

@keyframes circularProgressAnimation {
  to {
    --angle: 360deg;
  }
}

@property --angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}

#startBtn:checked~.countdown-container .circular-progress {
  opacity: 1;
  animation: circularProgressAnimation 10s linear;
}

#pauseBtn:checked~.countdown-container .circular-progress {
  animation-play-state: paused;
}

在这段代码中,我们首先移除了 countdown-container div元素的边框,然后为 circular-progress 类添加了样式。环形进度指示器是一个 span 元素,它在 countdown-container 中被绝对定位。我们使用conic gradient来创建环形进度效果。

我们还定义了一个关键帧动画 circularProgressAnimation,它在倒计时期间将进度指示器从0度动画到360度。我们使用--angle CSS 属性用于控制渐变的角度。

最后,我们使用复选框黑魔法来开始和暂停环形进度动画以及倒计时数字。当开始按钮被选中时,circular-progress span元素上的动画会开始进行,当暂停按钮被选中时,动画则会暂停。

通过以上这些修改,我们的倒计时器现在包含了一个环形进度条,它会与计时器一起动画。现在我们可以通过浏览器打开页面,测试一下效果。

4. 每种方法的优缺点

在实现了纯CSS和JavaScript倒计时器之后,接下来让我们讨论两种方式的优缺点,以确定哪种方法最适合您的需求。

4.1 CSS实现倒计时方式

优点:

  • 较小的脚本开销:正如我们在性能分析中所看到的,纯CSS计时器需要很少的脚本,从而降低了脚本执行的CPU使用率。这对于提高整体页面性能是有益的,特别是在处理能力有限的设备上。

  • 代码简洁:通过使用CSS来实现动画,代码保持更简洁和更易于维护。这种方法降低了实现的复杂性,并且可以使开发人员更容易理解和管理代码。

缺点:

  • 更高的渲染和绘制时间:纯CSS计时器往往有更高的渲染和绘制时间。由于CSS动画的性质,它对浏览器的渲染引擎要求更高,这可能会影响具有多个动画或复杂布局的页面的性能。

  • 有限的交互性:CSS动画本质上不如JavaScript灵活。实现更具交互性的功能,如动态更新或条件逻辑,可能具有挑战性,并且可能需要额外的JavaScript,这会部分抵消了代码简洁的优势。

4.2 JavaScript实现倒计时方式

优点:

  • 灵活性高:JavaScript可以对倒计时逻辑和DOM操作进行精细控制。这允许我们实现更复杂的交互、条件行为和动态更新,使其适用于更复杂的应用程序。

  • 高效的渲染和绘制:与CSS实现方式相比,JavaScript计时器拥有较低的渲染和绘制时间。JavaScript可以优化对DOM的更新,从而在涉及频繁更新的场景中实现更平滑的动画和更好的性能。

缺点:

  • 更高的脚本开销:JavaScript倒计时计时器的主要缺点是增加了脚本处理时间。JavaScript逻辑引入了额外的CPU开销,这可能会影响性能,特别是在处理能力较低的设备上或脚本使用量大的页面上。

  • 代码更复杂:实现JavaScript倒计时计时器需要编写和管理更多的代码,这可能会增加项目的复杂性,这种增加的复杂性可能会使代码库更难维护和调试。

纯CSS计时器代码简洁且易于理解,对于实现一个简单倒计时来说是一个不错的选择。但是它可能难以处理更复杂的动画和交互功能。另一方面,JavaScript计时器提供了更大的灵活度,能够允许更动态的交互,但这是以更高的脚本开销和增加的代码复杂度为代价的。最终,两种方法之间的选择取决于我们项目的具体需求和场景。

5. 总结

本文中,我们具体探讨了实现倒计时器的两种方法:使用JavaScript和仅使用CSS。我们从一个基本的JavaScript倒计时器开始,添加功能和样式,使其用户友好且视觉上吸引人。然后,我们实现了一个仅使用CSS的倒计时器,展示了CSS创建简单而有效的动画的能力。

现在不管是使用CSS实现倒计时器,还是使用JavaScript实现倒计时器,我们都可以根据我们的需求和项目的实际需求和场景来选择合适的方式。

本文如有错误,敬请指正!欢迎大家点赞~