在给我的博客主题做移动端适配的时候,菜单会随着屏幕的缩小而隐藏,右上角的按钮会随着菜单的打开和关闭展示不同的图标。
但是突兀的切换让我心生不爽,于是开启了图标切换动画的探寻之路。经历一番搜寻,总结出下面几种图标变换的实现方式。
切换动画
简单的切换动画并不涉及什么复杂的图标变化,原理就是事先准备好两个图标,把他们重叠在同一个位置,每次只显示其中一个,切换的时候通过调整 opacity
和 “size”
来达到一种动态切换的效果。下面看慢放是怎么回事。
弄清楚原理之后直接开始码,下面是简单的 HTML,两个 icon 和一个 container
足矣。
<div class="container">
<img src="./assets/menu.svg" class="icon icon-menu">
<img src="./assets/close.svg" class="icon icon-close">
</div>
给 container
添加点击事件,点击切换添加 close
类。
window.onload = function () {
document.querySelector('.container').addEventListener('click', function (event) {
this.classList.toggle('close');
});
}
接下来就是 CSS,container
下默认显示菜单图标,添加 close
类后隐藏菜单图标并显示关闭图标。
.container {
width: 96px;
height: 96px;
border-radius: 4px;
position: relative;
cursor: pointer;
}
.container > .icon {
width: 72px;
height: 72px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: all .3s ease-in-out;
}
.container > .icon-menu,
.container.close > .icon-close {
opacity: 1; transform: translate(-50%, -50%) scale(1);
}
.container > .icon-close,
.container.close > .icon-menu {
opacity: 0; transform: translate(-50%, -50%) scale(0);
}
这里有几点值得一提
- 因为两个图标要重叠,所以使用绝对定位和和
transform
的方式居中 - 图标的
transform
属性中translate
和scale
的顺序不能改变,如果scale
在前就会出现下面的问题
- 具体切换效果可以自己定义,例如把
scale
换成rotate(-180deg)
和rotate(180deg)
就是下面的效果
一点小改进
例子中图标切换只有两个状态:显示菜单和显示关闭,单独用 JavaScript 添加点击事件动态添加 CSS 类感觉有点没必要。可以考虑通过 checkbox
只用 HTML 和 CSS 实现相同的效果。
<div class="container">
<input class="checkbox" id="toggle" type="checkbox">
<label class="label" for="toggle">
<img src="./assets/menu.svg" class="icon icon-menu">
<img src="./assets/close.svg" class="icon icon-close">
</label>
</div>
实现的原理是将两个图标放置在 label
中,再把 label
和 checkbox
绑定,然后通过 CSS 隐藏 checkbox
,这样就可以通过 checkbox:checked
来判断切换状态而不用单独注册点击事件,相关的开关开合逻辑可以注册放在 checkbox
的 change
事件中,代码是不是有点逻辑和显示分离的感觉了😄。
.container {
width: 96px;
height: 96px;
}
.checkbox {
display: none;
}
.label {
display: block;
width: 100%;
height: 100%;
position: relative;
cursor: pointer;
}
.icon {
width: 72px;
height: 72px;
position: absolute;
left: 50%;
top: 50%;
}
.label > .icon-close,
#toggle:checked ~ label > .icon-menu {
opacity: 0;
transform: translate(-50%, -50%) scale(0);
transition: all .3s;
}
.label > .icon-menu,
#toggle:checked ~ label > .icon-close {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
transition: all .3s;
}
模拟动画
这个模拟动画的例子可能会感觉和上面的切换动画有点像,其实并不太一样。切换动画是通过 opacity
的交替加上一些效果(如旋转,缩放等)来实现图标切换。而模拟动画,直接来看慢放。
可以看到,模拟动画(morphing transition)是从图标的细节实现的的。要做一个模拟动画,一般来讲将变换前的图标和变换后的图标分成对应的部分,设计好图标的各个部分是如何经过一系列的变化最后构成新图标后就可以开始写了。不像切换动画直接拿图标来用,这里是要手动写图标的。所以对于一些过于复杂的图标,这种方法不是很适用。接下来看代码
<div class="container">
<input class="nav-checkbox" id="toggle" type="checkbox">
<label class="nav-label" for="toggle">
<i class="icon icon-menu"></i>
</label>
</div>
接着用 checkbox
写,在 label
中添加 i
标签,我们打算用 ::before
和 ::after
伪元素来构成默认菜单的三条杠。
.container {
width: 96px;
height: 96px;
border-radius: 4px;
overflow: hidden;
}
.checkbox {
display: none;
}
.label {
width: 96px;
height: 96px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.icon-menu,
.icon-menu::before,
.icon-menu::after {
width: 72px;
height: 12px;
background-color: black;
display: block;
transition: all 3s;
}
.icon-menu {
position: relative;
}
.icon-menu::before,
.icon-menu::after {
position: absolute;
content: '';
}
.icon-menu::before {
top: -24px;
}
.icon-menu::after {
top: 24px;
}
到这里为止,一个 menu
图标就用 HTML 和 CSS 的方式写出来了,接下来加动画。
#toggle:checked ~ label .icon-menu {
transform: rotate(225deg);
}
#toggle:checked ~ label .icon-menu::before {
top: 0;
transform: rotate(90deg);
}
#toggle:checked ~ label .icon-menu::after {
top: 0;
}
代码只有几行,因为我们要写的变换比较简单,第一条杠顺时针 45 度,第二三条杠逆时针 45 度旋转构成一个 X 图标,然后调整旋转的圈数让动画看起来复杂一些,最后选择的是 225 和 90,看起来还比较舒服。
模拟动画的实现方式也有很多,这里我选择用伪元素求简单反而导致能实现的效果受到了限制(想实现中间那条杠慢慢变短,但是因为这里伪元素的绝对定位没办法缩放)。用三个 span
标签的可能会更好,下面是一个别人写的 Material Morphing Icons,然后再推荐一个比较有名的库 hamburgers。
SVG 动画
说到图标动画应该是绕不开 SVG 的,原理是和模拟动画类似,但是 SVG 更加灵活容易操作,而且也可以应付复杂的图标和动画。一样慢动作回放看效果。
中间缩短消失,然后上下旋转组成 X。这里我用的 snapsvg
写的。首先在 HTML 文件中插入 snap.js
。
<script src="./snap.svg-min.js"></script>
开始写图标
var container = Snap(96, 96);
var rectTop = container.rect(12, 20, 72, 10);
var rectMed = container.rect(12, 42, 72, 10);
var rectBot = container.rect(12, 64, 72, 10);
然后是动画。
var closeState = false;
container.click(function () {
if (closeState) { // 默认 menu
rectTop.animate({
transform: null
}, 300, mina.easeinout);
rectMed.animate({
transform: null
}, 300, mina.easeinout);
rectBot.animate({
transform: null
}, 300, mina.easeinout);
} else {
rectTop.animate({ // 第一条杠位移到中间同时旋转45度(相对自身中心点旋转)
transform: 't0, 22 r45 ' + rectTop.getBBox().cx +
' ' + rectTop.getBBox(0).cy
}, 300, mina.easeinout);
rectMed.animate({ // 中间的杠长度缩小消失
transform: 's0, 1'
}, 300, mina.easeinout);
rectBot.animate({ // 第三条杠位移到中间同时旋转-45度(相对自身中心点旋转)
transform: 't0, -22 r-45 ' + rectBot.getBBox().cx +
' ' + rectBot.getBBox(0).cy
}, 300, mina.easeinout);
}
closeState = !closeState; // 更新 closeState
});
snapsvg
只是粗略研究了一会文档就开始写了,这里的动画实现应该有更简便的方法,有时间再另外探究。
感谢阅读