FLIP 动画技术实现

238 阅读3分钟

FLIP 动画技术实现

FLIP 动画技术是一种实现流畅、高性能动画效果的方法,它将动画分为四个步骤:

  1. First(第一帧) : 记录动画元素的初始状态(位置、大小等)。
  2. Last(最后一帧) : 将元素立即切换到动画的最终状态,不应用任何动画效果。
  3. Invert(反转) : 通过 CSS 变换(transform)将元素从最终状态反转回第一帧的状态。
  4. Play(播放) : 移除反转效果,此时元素从第一帧状态平滑过渡到最终状态,实现动画效果。

核心代码

<script setup lang="ts">
import { onMounted, ref } from "vue";

const showPanel = ref<boolean>(false);
const panelDom = ref<HTMLDivElement>();
let closeTimer: ReturnType<typeof setTimeout> | null = null;

function changeShowCard() {
  if (!showPanel.value) {
    // 展开动画
    showPanel.value = true;
    requestAnimationFrame(() => {
      if (panelDom.value) {
        const clientRect = panelDom.value.getBoundingClientRect();
        panelDom.value.style.height = '0px';
        requestAnimationFrame(() => {
          if (panelDom.value) {
            panelDom.value.classList.add("trans-animate");
            panelDom.value.style.height = `${clientRect.height}px`;
          }
        });
      }
    });
  } else {
    // 收起动画
    if (panelDom.value) {
      const clientRect = panelDom.value.getBoundingClientRect();
      panelDom.value.classList.remove("trans-animate");

      if (clientRect.height < 100) {
        closeTimer && clearTimeout(closeTimer);
        panelDom.value.classList.remove("trans-animate");
        showPanel.value = false;
      } else {
        requestAnimationFrame(() => {
          if (panelDom.value) {
            panelDom.value.style.height = `${clientRect.height}px`;
            closeTimer && clearTimeout(closeTimer);
            closeTimer = setTimeout(() => {
              panelDom.value?.classList.remove("trans-animate");
              showPanel.value = false;
              closeTimer = null;
            }, 700);
            requestAnimationFrame(() => {
              if (panelDom.value) {
                panelDom.value.classList.add("trans-animate");
                panelDom.value.style.height = "0px";
              }
            });
          }
        });
      }
    }
  }
}

onMounted(() => {
  closeTimer && clearTimeout(closeTimer);
});
</script>

代码解释

展开动画

  1. 将 showPanel.value 设置为 true
  2. 使用 requestAnimationFrame 确保在下一次浏览器重绘前执行关键代码。
  3. 获取元素的初始高度 clientRect.height
  4. 将元素高度设置为 0px
  5. 在下一次重绘前,添加 CSS 过渡类 trans-animate,并将元素高度设置为目标高度 clientRect.height。这时元素会从初始高度 0px 平滑过渡到目标高度,实现展开动画效果。

收起动画

  1. 获取元素当前高度 clientRect.height

  2. 移除 CSS 过渡类 trans-animate,中断当前动画。

  3. 如果当前高度小于 100px,直接将高度设置为 0px,移除过渡类,设置 showPanel.value 为 false

  4. 否则,使用 requestAnimationFrame 确保在下一次重绘前执行以下操作:

    • 将元素高度设置为当前高度 clientRect.height
    • 清除上一次的定时器(如果存在)。
    • 设置一个 700ms 的定时器,在定时器到期后移除 CSS 过渡类,设置 showPanel.value 为 false
    • 在下一次重绘前,添加 CSS 过渡类 trans-animate,并将元素高度设置为 0px。这时元素会从当前高度平滑过渡到 0px,实现收起动画效果。

其他细节

  • 在组件挂载时,使用 onMounted 钩子清除可能存在的定时器,避免内存泄漏。
  • 使用 requestAnimationFrame 确保关键代码在浏览器重绘前执行,保证视觉效果的流畅性。
  • 通过设置元素高度和添加/移除 CSS 过渡类来控制动画效果。

注意事项

  1. 性能问题: 获取元素大小/位置信息以及强制浏览器重绘都会带来一定性能开销,需要适当优化。
  2. 动画中断: 如果在动画播放过程中再次触发动画,需要处理动画中断的情况。
  3. 动画方向: 根据实际需求,可能需要处理动画的方向(如垂直/水平展开)。
  4. 嵌套动画: 如果动画元素内包含了其他子元素,可能需要额外处理子元素的状态变化。
  5. 定时器管理: 使用定时器时要注意清除,避免内存泄漏。