手风琴菜单
效果如下:
- 实现动画展开、收起效果
- 展开情况下只有一个菜单可以展开
思路
思考初始化需要做什么(无),交互需要做什么(点击展开关闭)
本质就是将菜单的高度从0设置为其对应的高度,运用上动画效果。
给对应菜单dom添加自定义属性status,用来标记当前菜单展开/关闭状态。
处理细节,当菜单处于正在打开/关闭时,不能重复的触发动画效果。
互斥效果,展开一个的同时关闭其他菜单。
动画的效果是通用的,本质就是从一个状态的数值到另一个状态数值的变化,所以可以封装成一个公用的函数。 函数不仅可以用于高度的变化,也可用于数字的变化。 封装成库是比较耗时和脑力的,这部分需要长期的积累和体会, 如果实在理解不了,那么就先掌握使用方法。
代码案例
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<ul class="menu-container">
<li class="menu">
<h2>菜单1</h2>
<ul class="submenu">
<li>菜单1</li>
<li>菜单2</li>
<li>菜单3</li>
<li>菜单4</li>
</ul>
</li>
<li class="menu">
<h2>菜单2</h2>
<ul class="submenu">
<li>菜单1</li>
<li>菜单2</li>
<li>菜单3</li>
<li>菜单4</li>
</ul>
</li>
<li class="menu">
<h2>菜单3</h2>
<ul class="submenu">
<li>菜单1</li>
<li>菜单2</li>
<li>菜单3</li>
<li>菜单4</li>
</ul>
</li>
<li class="menu">
<h2>菜单4</h2>
<ul class="submenu">
<li>菜单1</li>
<li>菜单2</li>
<li>菜单3</li>
<li>菜单4</li>
</ul>
</li>
</ul>
<script src="./animate.js"></script>
<script src="./index.js"></script>
</body>
</html>css
h2 {
margin: 0;
padding: 0;
font-size: 100%;
font-weight: normal;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
.menu-container {
width: 200px;
margin: 0 auto;
line-height: 30px;
}
.menu-container h2 {
padding: 0 25px;
cursor: pointer;
background: lightblue;
}
.submenu {
background: #e0f0f7;
padding: 0 30px;
height: 0;
overflow: hidden;
}
.menu {
margin: 20px 0;
}
.submenu li {
height: 30px;
}js
// 交互
const titles = document.querySelectorAll(".menu h2");
const Params = {
itemHeight: 30,//菜单项的高度
}
for (let i = 0; i < titles.length; i++) {
titles[i].addEventListener('click', function (e) {
// 互斥
const hasExpaned = document.querySelector(".menu .submenu[status=expanded]");
if (hasExpaned) {
onClose(hasExpaned);
}
// 切换展开状态
handleToogle(this.nextElementSibling);
});
}
// 展开菜单
function onExpand(menu) {
console.log("点击", menu.children.length);
const status = menu.getAttribute('status');
// 不是关闭状态就退出执行
if (status && status != 'closed') {
return;
}
menu.setAttribute('status', 'playing');//设置自定义属性-状态
createAnimation({
from: 0,
to: menu.children.length * Params.itemHeight,//高度
totalMs: 200,
duration: 10,
onMove(v) {
menu.style.height = v + 'px';
},
onEnd() {
menu.setAttribute('status', 'expanded');
}
})
}
// 关闭菜单
function onClose(menu) {
console.log("点击", menu.children.length);
const status = menu.getAttribute('status');
// 不是关闭状态就退出执行
if (status && status != 'expanded') {
return;
}
menu.setAttribute('status', 'playing');//设置自定义属性-状态
createAnimation({
from: menu.children.length * Params.itemHeight,//高度,
to: 0,
totalMs: 200,
duration: 10,
onMove(v) {
menu.style.height = v + 'px';
},
onEnd() {
menu.setAttribute('status', 'closed');
}
})
}
// 切换展开-关闭
function handleToogle(menu) {
const status = menu.getAttribute('status');
if (status == 'playing') {
return;
}
status == 'expanded' ? onClose(menu) : onExpand(menu);
}js
function createAnimation(options) {
let { from, to, totalMs = 1000, duration = 15, onMove, onEnd } = options;
const times = Math.floor(totalMs / duration);//动画要变化的次数
let dis = (to - from) / times;//每次变化的数值
let currTimes = 0;//现在变化的次数,用来判断是否动画结束,兼容从大到小的变化
const timerId = setInterval(() => {
currTimes++;
from += dis;
// 判断动画结束
if (currTimes >= times) {
from = to;
clearInterval(timerId);
onMove && onMove(from);
onEnd && onEnd();//执行回调
return;
}
onMove && onMove(from);//执行回调
}, duration)
}
createAnimation({
from: 0,//起始
to: 100,//结束
totalMs: 500,//总时长
duration: 15,//动画频率
onMove(value) {
console.log("动画执行了", value);
},
onEnd() {
console.log("动画结束了");
}
})