精细控制transition细节 - 实现一个活泼的弹框

精细控制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>
HTML结果

我们为.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的类名生命周期不太熟的话,可以先回顾官方文档

接下来先看一下我们想要实现的效果。

vivid-dialog-example.gif

GIF的帧率不是很高,可能看不清细节,大体上来说就是:

  1. 出现/消失效果为缩放
  2. 弹出框容器,标题,内容,底部按钮错开缩放
  3. 缩放带有弹跳效果 (bounce)
  4. 显示和消失的过渡是镜像的而不是相同

首先我们来实现简单的缩放效果

.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值。我们常见的easeease-inease-out等等的y轴比例都没有超过1,也就是说过渡过程中不会出现比如width大于最终值这种效果。但是实际它是可以比1大的,比如:

截屏20210125 下午5.50.29.png

像这样的曲线,过渡就会有个回弹的效果,也就是属性值*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风格的小伙伴提供一种替换方法。

这里是借鉴类似接口的思想,通过vueprops属性和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类。
那么,就可以正确嵌入到弹框中,并且可以有预期的样式和行为。除此之外,组件可以拥有自己的任何行为和样式。

我称其为抽象组件哈哈[二哈]。

截屏20210125 下午6.28.57.png

提供一个自己的按钮。

总结

  • transition可以实现细节控制
  • 通过animationend实现多次触发动画
  • 抽象法组件使组件变得更易扩展

实际上呢,本篇文章是作者编写插件的一个小结,源码和文档可与DakerHub查看。可以直接使用npm安装使用。也欢迎大家的想法和PR!

上一篇 Bash echo 高阶用法
下一篇 Goroutine编程入门