精细控制transition细节 - 实现一个活泼的弹框
作为一个前端,用户的良好体验是我们的基本信仰!一个有趣的交互会使用户对页面的好感提升,一个恰当的反馈能使用户轻松理解你想表达的意思。
本篇文章带大家一步一步打造一个活泼的轻量级弹框。你会学到:
transition
细节控制- 如何添加css3小动画
- 如何通过props实现可替换组件
HTML结构
<div class="dialog__wrap">
<div class="dialog__overlay"></div>
<div class="dialog">
<div class="dialog__header"></div>
<div class="dialog__content"></div>
<div class="dialog__footer"></div>
</div>
</div>
我们为.dialog__wrap
和.dialog__overlay
都加上
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
其实这两个元素是叠在一起的,为什么要单独用一个div
来做遮罩呢,是因为我们想要独立控制遮罩层的过渡,并且不影响弹框的过渡。
然后.dialog
居中布局就好了。
显示/消失过渡
首先来看最基础的,没有过渡。
我们使用最简单的v-if
来实现效果。
<div v-if="visible" class="dialog__wrap"> ... </div>
export default {
data() {
return {
visible: false
};
}
}
然后我们只需要控制visible
的值可以了。
现在我们来给它加一点过渡效果,你应该知道我们要用什么来实现,就是transition
组件。
<transition name="vivid" :duration="600">
<div v-if="visible" class="dialog__wrap">
<div class="dialog">
<div class="dialog__header"></div>
<div class="dialog__content"></div>
<div class="dialog__footer"></div>
</div>
</div>
</transition>
transition
根据过渡的状态提供了6个类名供我们使用:
-enter
, -enter-to
, -leave
, -leave-to
, -leave-active
, -enter-active
在我们的过渡中, -enter-to
和-leave
就是最终的状态,所以我们用不到这俩。
接下来我们详细介绍剩下的四个类。
细节把控
如果你对transition
的类名生命周期不太熟的话,可以先回顾官方文档
接下来先看一下我们想要实现的效果。
GIF的帧率不是很高,可能看不清细节,大体上来说就是:
- 出现/消失效果为缩放
- 弹出框容器,标题,内容,底部按钮错开缩放
- 缩放带有弹跳效果 (bounce)
- 显示和消失的过渡是镜像的而不是相同
首先我们来实现简单的缩放效果
.vivid-enter,
.vivid-leave-to {
.vivid-dialog {
opacity: 0;
transform: scale(0.5);
}
.vivid-dialog__header,
.vivid-dialog__content,
.vivid-dialog__footer > *:first-child,
.vivid-dialog__footer > *:nth-child(2) {
opacity: 0;
transform: scale(0.6);
}
}
这样所有元素都会同时的进行变大/变小。但这不是我们想要的,我们想要的是像水泡一样的一个一个冒出来的效果。
这里关键是每个元素的出场时间不同,也就是延时不同。
这里依助于-enter-active
和-leave-active
,我们可以定义过渡中的时间延时。
.vivid-enter-active {
.vivid-dialog__header {
transition-delay: 0.05s;
}
.vivid-dialog__content {
transition-delay: 0.1s;
}
.vivid-dialog__footer > *:first-child {
transition-delay: 0.2s;
}
.vivid-dialog__footer > *:nth-child(2) {
transition-delay: 0.25s;
}
}
这段scss
能够让弹框标题,内容和底部按钮依次冒出来。但是它看起来还不够调皮,所以我们为它增加弹跳的效果。
那么问题来了,弹跳效果怎么添加呢?其实也很简单,就是贝塞尔曲线。
当我们给transition
设置了transition-timing-function
时,将时间作为x,那么元素的属性值就会被映射为y值。我们常见的ease
,ease-in
,ease-out
等等的y轴比例都没有超过1,也就是说过渡过程中不会出现比如width
大于最终值这种效果。但是实际它是可以比1大的,比如:
像这样的曲线,过渡就会有个回弹的效果,也就是属性值*1.x
,最后又变回属性值*1
那用到我们这就是:
vivid-enter-active {
.vivid-dialog {
transition: all 0.4s cubic-bezier(0.44, 0.03, 0.33, 1.76);
}
.vivid-dialog__header,
.vivid-dialog__content,
.vivid-dialog__footer > *:first-child,
.vivid-dialog__footer > *:nth-child(2) {
transition: all 0.4s cubic-bezier(0.44, 0.03, 0.33, 1.6);
}
}
到这里,开始的过渡效果已经OK了,但是!不能将开始的效果照搬放到-leave-active
中去。因为我们想实现的效果是,离开的时候最后出现的元素先消失,并且弹跳的效果出现在过渡一开始而不是最后,也就是说transition-timing-function
完全不一样。
哦还有一点,记得由于我们的过渡时长是由所有元素决定的,而transition
自动识别的是根元素的过渡结束事件,所以我们需要显示指定:duration="600"
。
大家可以先自己想想怎么改动,源码放在最下方。
到这里,我们已经有一个活泼的弹框了,下面是优化以及增加组件扩展性的内容,可以选择跳过。
拒绝关闭动画
点击弹框之外的区域关闭弹框已经成为了一个约定俗成的交互方式,有时候我们不希望用户点太快直接关闭弹框,我们需要他了解弹框的内容。
所以我们这里做了一个拒绝关闭的动画(当然是可以配置的),这个效果在上面也演示过了,我们讲一下原理和细节。
首先原理很简单,在用户点击时增加动画的class
,然后在动画结束后删除class
。
首先我们有一个动画类
@keyframes headShake {
0% {
transform: translateX(0);
}
6.5% {
transform: translateX(-6px) rotateY(-9deg);
}
18.5% {
transform: translateX(5px) rotateY(7deg);
}
31.5% {
transform: translateX(-3px) rotateY(-5deg);
}
43.5% {
transform: translateX(2px) rotateY(3deg);
}
50% {
transform: translateX(0);
}
}
.headShake {
animation-timing-function: ease-in-out;
animation-name: headShake;
animation-duration: 0.6s;
animation-fill-mode: both;
}
只要一个元素添加了headShake
类名,就会有一个摇头的动画,更多动画可以参考animate.css
接下来就是如何操作类名了:
onClickOverlay() {
this.closeOnClickOverlay && this.close();
if (!this.closeOnClickOverlay && this.visible) {
const el = this.$refs.dialog;
el.classList.add("headShake");
el.addEventListener(
"animationend",
() => {
el && el.classList.remove("headShake");
},
{ once: true }
);
}
},
这个函数在点击遮罩时执行,先判断是否允许关闭,再判断是否已关闭(但是在动画中DOM还没消失),最后添加headShake
。
并且监听了animationend
事件,动画结束后就会自动删除该类名,以便下次还能触发。
这里值得注意的一点是,transition
也是监听的animationend
/transitionend
事件,所以在摇头的过程中关闭弹框就会有些难看。我们得在关闭之前取消掉摇头动画:
clearAnimation() {
this.$refs.dialog &&
this.$refs.dialog.getAnimations().forEach((animation) => {
animation.cancel();
});
},
再试一下,随便怎么你啥时候关,没问题!
可定制按钮
大多数弹框组件还有☝️问题,他们的内置按钮组件太丑了!还不能换。所以我们来解决一下这个问题,我们为普通用户提供一个默认按钮(很普通),为想统一UI风格的小伙伴提供一种替换方法。
这里是借鉴类似接口的思想,通过vue
的props
属性和component
组件,我们可以制定一个抽象组件:
<component
v-if="footer.ok"
primary
:loading="okLoading"
:disabled="okLoading ? 'disabled' : undefined"
v-bind:is="btnComponent"
@click="handleOK"
>确定</component>
.vivid-dialog-btn {
height: 40px;
width: 100%;
display: inline-block;
}
也就是说,只要组件正确处理了primary
,loading
,disabled
,click
,default slot
,并且有vivid-dialog-btn
类。
那么,就可以正确嵌入到弹框中,并且可以有预期的样式和行为。除此之外,组件可以拥有自己的任何行为和样式。
我称其为抽象组件哈哈[二哈]。
提供一个自己的按钮。
总结
transition
可以实现细节控制- 通过
animationend
实现多次触发动画 - 抽象法组件使组件变得更易扩展
实际上呢,本篇文章是作者编写插件的一个小结,源码和文档可与DakerHub查看。可以直接使用npm安装使用。也欢迎大家的想法和PR!