动画07 展开收起小动画

在群聊里看到一位同学要实现这样一个需求,点击按钮,弹窗回收到按钮的位置。实现这样一个小动画,群里的大神给出了一个解决方案,自己记录一下,并进行了一点改进,以备不时之需。

需求

实现下面这样的动画:

实现

总体上实现不复杂,要值得注意的是首先使用getBoundingClientRect获取元素的位置和尺寸属性,可以将它封装为一个工具函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
export const getRect = ele => {
const { width, height, top, left, bottom, right } = ele.getBoundingClientRect();
return {
width,
height,
top,
left,
bottom,
right,
middleLeft: left + width / 2,
middleTop: top + height / 2,
}
};

实现上面的动画效果,实际上由两组动画复合而成sacle + transform,这种动画一般在CSS中使用keyframes实现,但是由于两个元素之间的移动和缩放都是需要计算的,所以使用了Web Animiate API的Element.animate()方法,这个方法让我们能够在JavaScript中使用keyframes的威力。

兼容性:

1
Element.animate(keyframes, keyframeOptions)

其中,keyframes代表包含所需CSS关键帧的JavaScript表示的一个对象数组,每个对象都包含一个关键帧,它们一起构成了所需的动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var boxframes = [{
transform: 'translateX(0)',
background: 'red',
borderRadius: 0
}, {
transform: 'translateX(200px) scale(.5)',
background: 'orange',
borderRadius: 0,
offset: 0.6 /* set explicit point (60%) when frame starts */
}, {
transform: 'translateX(400px)',
background: 'green',
borderRadius: '50%'
}]

keyframeOptions代表包含动画的其他设置,如easing, duration, fill-mode等:

 
属性 等效 CSS 描述
id none 给这个动画的命名以便在后面的代码中引用。
delay animation-delay 动画开始之前的延迟(整数)毫秒。默认 0s.
direction animation-direction 定义动画是否应该正常播放,反之亦然,或两者之间是否交替播放。可能的值是:
  • normal: 动画正常播放。在每个动画周期之后,动画重置为开始状态并重新开始(默认)
  • reverse: 从结束状态开始反向播放动画。在每个动画周期之后,动画重置为结束状态并重新开始。
  • alternate: 动画在正常和反向之间交替。相反,动画从结束状态开始并向后播放。动画定时功能也相反。
  • alternate-reverse: 动画在反向和正常方向之间交替,从第一次迭代开始反向。
duration animation-delay 动画的持续时间(整数),以毫秒为单位,如1000.默认为0(无动画,跳转到最后一帧)。
easing animation-timing-function S设置用于动画 @keyframe 缓动功能。可用值 "ease", "ease-in", "ease-in-out","linear", "frames(integer)" 等. 默认 "linear".
endDelay n/a 动画结束后延迟的毫秒数。当基于另一个动画的结束时间对多个动画进行排序时,这非常有用。默认为0。D默认为0
fill animation-fill-mode 定义当动画不再播放时,动画应如何将样式应用于其目标。默认为 "none"。 可能的值是:
  • none: 不播放动画时,不应将任何样式应用于目标。Default value.
  • forwards: 当动画未播放时,目标元素将保留最后关键帧中定义的计算样式(即:当关键帧处于100%时)。
  • backwards: 当动画未播放时,目标元素将保留第一个关键帧中定义的计算样式(即:当关键帧处于0%时)。
  • both: 当动画未播放时,目标元素将保留在第一个和最后一个关键帧中定义的计算样式。
iterationStart n/a 设置动画应该开始的迭代中的点。值应该是一个正数,浮点数。在迭代次数为1的动画中, iterationStart 的值为0.5会在中途开始动画。 在2次迭代的动画中,iterationStart 值为1.5,通过第二次迭代等途径开始动画。 IDefaults to 0.0.
iterations

 

 

animation-iteration-count 设置停止前动画应该运行的次数。Infinity 意味着永远。 默认为1

实际上这个API的使用和很多属性都与在CSS中使用animatekeyframes声明动画都是很相似的,但是这个API让我们有能力根据需要操作结果,例如暂停,跳过前进或挂接到动画的事件处理程序。

1
var animation = element.animate(keyframes, options);

它将返回一个新建的Animation对象实例,它有着能够控制动画的属性和方法,比如通过cancel方法取消动画,通过pauseplay方法来暂停和恢复动画的播放等等,更多的属性可以参考MDN的文档

总的来说,这个API虽然很强大,但是还存在一定的兼容性的问题,希望浏览器的支持早点跟上,我们就能够想使用jQuery的animate方法一样痛快的使用原生的animate方法

代码

我是在Vue中实现的,完整的代码在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<template>
<div class="main">
<div class="modal" ref="modal" @click="toggleModal"></div>
<div class="button" ref="button" @click="toggleModal">{{text}}</div>
</div>
</template>

<script>
import {getRect} from '@/utils/index.js'

export default {
name: 'demo42',
data() {
return {
isExpand: true,
modalRectInit: {},
buttonRectInit: {},
}
},

computed: {
text() {
return this.isExpand ? '收起' : '展开'
}
},

methods: {
toggleModal() {
this.isExpand = !this.isExpand;
this.animate(this.isExpand);
},

animate(reverse) {
const modal = this.$refs.modal,
button = this.$refs.button;

modal.style.transform = 'none';

this.buttonRectInit = getRect(button);
this.modalRectInit = getRect(modal);

const scaleX = this.buttonRectInit.width / this.modalRectInit.width,
scaleY = this.buttonRectInit.height / this.modalRectInit.height;

let transformStart = [
'translateX(-50%)',
'translateY(-50%)',
'scaleX(1)',
'scaleY(1)',
].join(' ');


let transformEnd = [
`translateX(${(this.buttonRectInit.left - this.modalRectInit.left)}px)`,
`translateY(${(this.buttonRectInit.top - this.modalRectInit.top)}px)`,
`scaleX(${scaleX})`,
`scaleY(${scaleY})`,
].join(' ');

if (reverse) {
[transformStart, transformEnd] = [transformEnd, transformStart]
}


const keyFrames = [
{transform: transformStart},
{transform: transformEnd},
];

modal.animate(
keyFrames,
{
duration: 400,
easing: 'ease-in',
iterations: 1,
}
);

modal.style.transform = transformEnd;
},
},
}
</script>

<style scoped>
.modal {
width: 480px;
height: 360px;
line-height: 360px;
background: darkgoldenrod;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
transform-origin: top left;
cursor: pointer;
font-size: 14px;
z-index: 2;
}
.button {
width: 80px;
height: 30px;
line-height: 30px;
background: aqua;
cursor: pointer;
font-size: 14px;
}
</style>

参考