Skip to content
大纲

GSAP 实现路由跳转动画

Vue Router 的过渡动效只能处理 RouterView 下的动画效果, 如果存在嵌套路由, 当子路由改变时想做出替换整个页面的动画可能就不太合适。

GSAP

这种情况可以手动的更改 URL, 渲染组件, 模拟路由跳转的过程。然后利用 Transition 的 JavaScript 钩子搭配 GSAP 库实现路由跳转动画。

动画的处理我们依赖于 GSAP 实现。对于 GSAP 而言, 主要依赖于两个方法:

  • gsap.set() : 这个方法通常使用在动画开始之前, 表示设置动画开始的元素属性
  • gsap.to() : 这个方法表示元素的最终展示状态

GSAP 会基于 set 和 to 的状态, 来自动执行中间的补间动画。我们只需:

  • 创建一个对应的组件, 使用 <Transition> 进行包裹
  • 计算出 set 时, 组件元素对应的样式属性
  • 计算出 to 时, 组件元素对应的样式属性

然后就可以由 GSAP 自动实现对应的补间动画了。

示例

比如实现一个图片点击时, 从图片中心位置放开展示详情页的动画, 关闭时收缩至中心点的动画。

实现动画

在父组件, 用 <Transition> 包裹详情组件, 采用 JavaScript 钩子的方式实现动画。

html
<!-- 详情内容展示 -->
<Transition :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave">
  <PinsComponent v-if="isVisiblePins" :data="currentPins.data"></PinsComponent>
</Transition>
vue
<script setup>
import NavBar from '../../../libs/NavBar.vue'
import SvgIcon from '../../../libs/SvgIcon.vue'
import { isMobileTerminal } from '../../../utils/flexible'
import { useRouter } from 'vue-router'
import ComBtn from '../../../libs/ComBtn.vue'

const router = useRouter()

defineProps({
  data: {
    type: Object,
    required: true,
  },
})

// 关闭
const onPop = () => {
  router.back()
}
</script>

<template>
  <div
    class="fixed left-0 top-0 w-screen h-screen text-xl z-20 backdrop-blur-4xl bg-white dark:bg-zinc-800 pb-2 overflow-y-auto xl:p-2 xl:bg-transparent"
  >
    <!-- 移动端下展示 navbar -->
    <NavBar v-if="isMobileTerminal" sticky>
      {{ data.author }}
      <template #right>
        <SvgIcon name="share" class="w-3 h-3" fillClass="fill-zinc-900 dark:fill-zinc-200"></SvgIcon>
      </template>
    </NavBar>

    <!-- pc 端下展示关闭图标 -->
    <SvgIcon
      v-else
      name="close"
      class="w-3 h-3 ml-1 p-0.5 cursor-pointer duration-200 rounded-sm hover:bg-zinc-100 absolute right-2 top-2"
      fillClass="fill-zinc-400"
      @click="onPop"
    ></SvgIcon>

    <!-- 内容区域 -->

    <div class="xl:max-w-[80%] xl:h-full xl:mx-auto xl:rounded-lg xl:flex xl:justify-center">
      <img
        class="w-screen mb-2 xl:w-auto xl:max-w-[80%] xl:h-full xl:rounded-tl-lg xl:rounded-bl-lg"
        :src="data.photo"
      />

      <div class="xl:min-w-[20%] xl:h-full xl:bg-white xl:dark:bg-zinc-900 xl:rounded-tr-lg xl:rounded-br-lg xl:p-3">
        <div v-if="!isMobileTerminal" class="flex justify-between mb-2">
          <SvgIcon
            name="share"
            class="w-4 h-4 p-1 cursor-pointer hover:bg-zinc-200 dark:hover:bg-zinc-800 duration-300 rounded"
            fillClass="fill-zinc-900 dark:fill-zinc-200"
          ></SvgIcon>

          <ComBtn class="" type="info" icon="heart" iconClass="fill-zinc-900 dark:fill-zinc-200"></ComBtn>
        </div>
        <!-- 标题 -->
        <p class="text-base text-zinc-900 dark:text-zinc-200 ml-1 font-bold xl:text-xl xl:mb-5">
          {{ data.title }}
        </p>
        <!-- 作者 -->
        <div class="flex items-center mt-1 px-1">
          <img v-lazy class="h-3 w-3 rounded-full" :src="data.avatar" alt="" />
          <span class="text-base text-zinc-900 dark:text-zinc-200 ml-1">{{ data.author }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped lang="scss"></style>

然后采用 GSAP 实现动画。

js
import gsap from 'gsap'
const beforeEnter = (el) => {
  gsap.set(el, {
    scaleX: 0,
    scaleY: 0,
    transformOrigin: '0 0',
    translateX: currentPins.value.location?.translateX,
    translateY: currentPins.value.location?.translateY,
    opacity: 0,
  })
}
const enter = (el, done) => {
  gsap.to(el, {
    scaleX: 1,
    scaleY: 1,
    duration: 0.3,
    translateX: 0,
    translateY: 0,
    opacity: 1,
    onComplete: done,
  })
}
const leave = (el, done) => {
  gsap.to(el, {
    scaleX: 0,
    scaleY: 0,
    duration: 0.3,
    translateX: currentPins.value.location?.translateX,
    translateY: currentPins.value.location?.translateY,
    opacity: 1,
    onComplete: done,
  })
}

切换路由

对于路由的切换利用 history.pushState() 方法手动更换路由就好了。在点击图片时, 切换路由, 并监听 popstate 浏览器后退按钮事件。

js
/**
 * 监听浏览器后退按钮事件
 */
useEventListener(window, 'popstate', () => {
  isVisiblePins.value = false
})

/**
 * 点击图片项, 进入详情
 */
const onToPins = (item) => {
  detailStore.changeLastDetail(item.data)
  // 修改浏览器的 URL
  const virtualID = item.data.id.replaceAll('https://', '').replaceAll('/', '-').replaceAll('.', '_')
  let baseUrl = import.meta.env.BASE_URL === '/' ? '' : import.meta.env.BASE_URL
  history.pushState(null, null, `${baseUrl}/pins/${virtualID}`)
  isVisiblePins.value = true
  currentPins.value = item

  console.log(currentPins.value.location?.translateX, currentPins.value.location?.translateY)
}

Mochi's personal blog.