Skip to content

手风琴菜单

效果如下:

  • 实现动画展开、收起效果
  • 展开情况下只有一个菜单可以展开

思路

  • 思考初始化需要做什么(无),交互需要做什么(点击展开关闭)

  • 本质就是将菜单的高度从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("动画结束了");
    }
})

MIT License