使用 VueUse 和 View Transitions API 实现暗黑模式主题动画切换效果 ​

前言 ​

在当今的 Web 应用中,暗黑模式已经成为一种常见且受欢迎的功能。它不仅能够减少用户在夜间使用应用时的眼睛疲劳,还能为应用增添一份现代感。然而,简单的主题切换可能会显得生硬,影响用户体验。本文将介绍如何使用 VueUse 库和 View Transitions API 实现一个平滑的暗黑模式切换动画效果,让主题切换变得更加丝滑和优雅。

效果展示 ​

技术栈介绍 ​

View Transitions API ​

View Transitions API 是浏览器原生提供的一种 API,它能够让我们在 DOM 元素发生变化时创建平滑的过渡动画。这个 API 特别适合用于页面切换、主题切换等场景,能够大大提升用户体验。

兼容性:目前 View Transitions API 已经在 Chrome 和 Edge 等主流浏览器中得到支持,但在 Safari 和 Firefox 中可能尚未完全支持。Safari 仅在去年9月发布的 18 版本及以上支持。

VueUse ​

VueUse 是一个基于 Vue 的组合式 API 工具库,提供了大量实用的 Vue 组合式函数,能够帮助我们简化开发过程。在本文中,我们将使用 VueUse 提供的 useDark 和 useToggle 函数来实现主题切换功能。

NOTE

点击下发按钮,或在页面任意位置右键切换主题,即可查看切换效果,

loading

实现步骤 ​

1. 安装依赖 ​

首先,我们需要安装 VueUse 库:

bash
# 使用 npm
npm install @vueuse/core

# 或使用 pnpm
pnpm install @vueuse/core

# 或使用 yarn
yarn add @vueuse/core

2. 创建主题切换组件 ​

接下来,我们创建一个主题切换组件,例如 ThemeToggle.vue:

vue
<template>
  <div class="theme-container" ref="container">
    <button class="toggle-theme" @click="toggleTheme">
      <div class="icon">
        {{ isDark ? '🌞' : '🌙' }}
      </div>
    </button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useDark, useToggle } from '@vueuse/core'

// 使用 VueUse 的 useDark 函数来管理暗黑模式状态
const isDark = useDark({
  selector: 'html',
  attribute: 'data-bs-theme',
  valueDark: 'dark',
  valueLight: 'light'
})

// 使用 useToggle 函数来切换暗黑模式
const toggleDark = useToggle(isDark)

// 获取容器引用
const container = ref(null)

// 主题切换函数
const toggleTheme = (event) => {
  // 获取点击位置坐标
  const x = event.clientX
  const y = event.clientY
  
  // 计算结束半径(从点击位置到屏幕最远点的距离)
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  )

  // 兼容性处理:如果浏览器不支持 View Transitions API,则直接切换主题
  if (!document.startViewTransition) {
    toggleDark()
    return
  }

  // 使用 View Transitions API 创建过渡效果
  const transition = document.startViewTransition(async () => {
    toggleDark()
  })

  // 过渡准备就绪后,执行动画
  transition.ready.then(() => {
    // 定义圆形裁剪路径
    const clipPath = [
      `circle(0px at ${x}px ${y}px)`,
      `circle(${endRadius}px at ${x}px ${y}px)`,
    ]
    
    // 根据当前主题状态决定动画方向
    document.documentElement.animate(
      {
        clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
      },
      {
        duration: 400,
        easing: "ease-in",
        fill: 'forwards',
        pseudoElement: isDark.value
          ? "::view-transition-old(root)"
          : "::view-transition-new(root)",
      }
    )
  })
}
</script>

<style>
.theme-container {
  position: relative;
  overflow: hidden;
}

.toggle-theme {
  cursor: pointer;
  border: none;
  background: transparent;
  padding: 8px;
  border-radius: 50%;
  transition: background-color 0.3s;
}

.toggle-theme:hover {
  background-color: rgba(0, 0, 0, 0.1);
}

.icon {
  font-size: 20px;
}
</style>

3. 添加 CSS 样式 ​

为了确保 View Transitions API 正常工作,我们需要添加一些 CSS 样式。这些样式可以放在全局样式文件中,例如 main.css 或 App.vue 的 <style> 标签中:

css
/* 禁用默认的 View Transitions 动画 */
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

/* 设置亮色模式下的层级顺序 */
::view-transition-old(root) {
  z-index: 1;
}

::view-transition-new(root) {
  z-index: 2147483646;
}

/* 设置暗色模式下的层级顺序(与亮色模式相反) */
[data-bs-theme="dark"]::view-transition-old(root) {
  z-index: 2147483646;
}

[data-bs-theme="dark"]::view-transition-new(root) {
  z-index: 1;
}

4. 在主应用中使用主题切换组件 ​

最后,我们需要在主应用中使用这个主题切换组件:

vue
<template>
  <div class="app">
    <header>
      <h1>我的应用</h1>
      <ThemeToggle />
    </header>
    <main>
      <!-- 应用内容 -->
    </main>
  </div>
</template>

<script setup>
import ThemeToggle from './components/ThemeToggle.vue'
</script>

<style>
/* 定义主题变量 */
:root {
  --bg-color: #ffffff;
  --text-color: #1a1a1a;
  --card-bg: #f5f5f5;
  --border-color: #e0e0e0;
}

:root[data-bs-theme="dark"] {
  --bg-color: #1a1a1a;
  --text-color: #ffffff;
  --card-bg: #2d2d2d;
  --border-color: #404040;
}

/* 应用主题变量 */
body {
  background-color: var(--bg-color);
  color: var(--text-color);
  transition: background-color 0.3s, color 0.3s;
}

.app {
  min-height: 100vh;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
  border-bottom: 1px solid var(--border-color);
}

main {
  padding: 2rem;
}

/* 卡片样式 */
.card {
  background-color: var(--card-bg);
  border-radius: 8px;
  padding: 1.5rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  transition: background-color 0.3s, box-shadow 0.3s;
}
</style>

原理解析 ​

View Transitions API 工作原理 ​

  1. 捕获快照:当调用 document.startViewTransition() 时,浏览器会捕获当前 DOM 状态的快照。

  2. 执行 DOM 更新:在回调函数中,我们执行 DOM 更新(例如切换主题)。

  3. 捕获新状态:浏览器捕获更新后的 DOM 状态快照。

  4. 创建过渡:浏览器使用这两个快照创建过渡动画。

  5. 自定义动画:我们可以通过 CSS 伪元素 ::view-transition-old 和 ::view-transition-new 来自定义过渡动画。

圆形扩散效果实现 ​

在我们的实现中,我们使用了 clipPath 属性来创建圆形扩散效果:

  1. 计算点击位置:获取用户点击的坐标。

  2. 计算结束半径:计算从点击位置到屏幕最远点的距离,确保圆形能够覆盖整个屏幕。

  3. 创建圆形裁剪路径:定义从点击位置开始的圆形裁剪路径,从 0 半径扩展到结束半径。

  4. 根据主题状态调整动画方向:根据当前主题状态决定动画的方向(从亮色到暗色或从暗色到亮色)。

兼容性处理 ​

由于 View Transitions API 尚未在所有浏览器中得到支持,我们需要添加兼容性处理:

javascript
// 兼容性处理
if (!document.startViewTransition) {
  toggleDark();
  return;
}

这样,在不支持 View Transitions API 的浏览器中,主题切换仍然可以正常工作,只是没有动画效果。