- 作者:老汪软件技巧
- 发表时间:2024-09-06 21:03
- 浏览量:
前言
日常开发中,弹窗是一个很常用的组件。通过 v-model 传入 boolean 值控制 v-if v-show 指令切换组件dom元素显示状态,但因为是直接控制元素显示/隐藏,所以在UI体现上会特别生硬,这时加入渐入渐出的动画就非常重要了。
如果你的组件显示状态是直接由 v-model 直接控制的,那么你就会因为 v-show 指令在 false 时会将元素 display none 的特性,导致渐出动画无法正常显示,本篇文章便是提供一个解决这种问题的思路。
CSS动画
一个元素只播放一次渐入CSS动画是很简单的,你只需要 animation-name 和 animation-duration 就能让他动起来,但当你需要他再播放一次渐出动画时,最简单的实现方法就是更改他的 animation-name,并重新写一套渐出的CSS动画关键帧,下面我将简略列举两个库不同的实现方法。
这里我研究过 element-plus 和 animista 的两种实现方式
element-plus 就是按照最常见的写法,分别写了两套动画Class,并赋上渐入/渐出的关键帧样式,在显示/隐藏切换 Class 实现渐入渐出。这种方式虽然能实现功能,但弊端就是每写一个组件时你都要为他重写一套 Class 和动画,非常的不智能。
animista 实现方法就非常巧妙,通过 animation-direction: reverse 这个属性,只需要写一次动画帧,用一个 animation-name 就能让渐入动画倒放,实现渐入渐出,以下内容将基于这个逻辑在 Vue 中实现。
基本功能
这是一个最基础的弹窗组件代码,包含了最简单的渐入的效果:
@keyframes rtl 编写好了渐入的动画,通过 .rtl Class 引入样式,在 .s-popup 切换为显示状态时,让其子元素 .s-popup--wrapper.rtl 播放渐入动画。
**注意:**这里有个技巧,弹窗组件的定位样式是写在最外层的 .s-popup 上,而播放动画则是由其子元素 .s-popup--wrapper 负责,这种写法好处是,编写关键帧时不需要考虑元素本身的 transform 定位问题,让写好的关键帧少一些多余样式,能够适用到不同的组件里。
渐出实现
在基础功能有了以后,就需要实现渐出动画,上面解析 animista 的逻辑时说了,我们只需要让渐入动画倒放就是渐出效果了,在实现代码前,我们需要问题 难点。
正常情况下,渐出在弹窗关闭时就需要触发,但组件在 v-model / props.modelValue 设为 false 后, dom元素 就已经被 display: none 隐藏而导致无法正常播放动画,所以我们需要些额外处理逻辑。
异步关闭弹窗
首先我们需要分出几点核心逻辑:
弹窗显示状态是与 v-model 异步的,需要变量独立控制;弹窗隐藏前,需要播放渐出动画,不能被 display: none 影响;
首先我们可以通过 animationstart 和 animationend 事件读取 animationPlaying 播放中状态,同时在 v-model 触发弹窗关闭后,我们不能真的去隐藏弹窗,而是要在这个时机里设置一个 closed 关闭中状态,去触发渐出动画,让动画去负责弹出的显示控制。具体则通过一个 setInterval 异步方法不断查询 animationPlaying 来更新弹窗的显示状态,代码实现如下:
现在,触发动画的代码已完成,下一步就是控制样式,让渐入动画倒放,实现渐出
触发渐出动画
这里我们结合 Vue3 的特性,使用组合式函数来编写一个可复用的倒放样式。
由于动画是使用同一个 animation-name,所以在触发的那一刻我们需要先把 animation-name 清空,然后 setTimeout 一个 animation-irection: reverse 倒放属性,就能够让元素动画倒放,实现渐出效果了
// useAnimationReverse.ts
import type { Ref } from 'vue';
import { ref, watch } from 'vue';
export function useAnimationReverse(on: Ref<boolean>) {
const style = ref<{ [key: string]: any }>({});
watch(on, (val) => {
if (val) {
// 先重设元素动画样式
style.value = {
animationDuration: 'auto',
animationTimingFunction: 'ease',
animationName: 'none',
};
// 随后设置倒放
setTimeout(() => {
style.value = {
animationDirection: 'reverse',
};
}, 0);
} else {
style.value = {};
}
});
return {
style,
};
}
最后引入到组件代码中即可完成。
最终代码