iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
JavaScript

PM說: RD大大,這個功能要怎麼寫啊?系列 第 18

PM 說: App 上常見的可拖曳底部區叫做什麼? 可以用網頁實現嗎?

  • 分享至 

  • xImage
  •  

bus+

前言

封面圖是 Bus+ App,紅色框框的設計在App上很常見
它的名稱叫做 Bottom sheets
今天目標是寫一個網頁版

更多設計請參考:
https://m2.material.io/components/sheets-bottom#standard-bottom-sheet

比較需要注意的地方

和網頁的分割視窗線最大的不同是手機能做出"甩動操作"
甩動=移動超過一定距離就會觸發高度變化

常見的底部高度比例

  1. 100% 撐滿
  2. 50% 半高
  3. 30% 最小高度
  4. 0% 隱藏

成果

手機上的botttom-sheet

demo-bottom-sheet

重點

0.計算比例是相反的; 因為計算高度是由上而下

//下面 30%
this.ratio = 0.7;
//下面 50%
this.ratio = 0.5;
//下面 100%
this.ratio = 0;
  1. 計算出現在滑到螢幕高度占比
    https://www.shubo.io/get-bounding-client-rect/
// 先計算距離
const y = touch.clientY - containerRect.top;
// 計算比例(%)
比例 = y / containerRect.height
  1. 移動時調整高度
    https://www.runoob.com/jsref/prop-element-offsetheight.html
        adjustRatio(inputRatio) {
          // 限制比例
          this.ratio = Math.max(0.001, Math.min(0.99, inputRatio));
          const containerHeight = container.offsetHeight;
          // 上面的高度
          const topHeight = containerHeight * inputRatio;
          // 下面的高度
          const bottomHeight =
            containerHeight - topHeight - divider.offsetHeight;
          // 調整CSS
          topPane.style.flex = `0 0 ${topHeight}px`;
          bottomPane.style.flex = `0 0 ${bottomHeight}px`;
        },

程式碼

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Bottom Sheet Demo</title>
    <style>
      body,
      html {
        height: 100%;
        margin: 0;
      }
      .container {
        display: flex;
        flex-direction: column;
        height: 100%;
      }
      .top,
      .bottom {
        overflow: auto;
      }
      .top {
        background-color: #f0f0f0;
        flex: 1;
      }
      .bottom {
        background-color: #e0e0e0;
        flex: 1;
      }
      .divider {
        height: 10px;
        background-color: #ccc;
        cursor: row-resize;
      }
      .controls {
        position: fixed;
        bottom: 10px;
        left: 50%;
        transform: translateX(-50%);
        display: flex;
        gap: 10px;
        min-height: 5px;
      }
      button {
        padding: 5px 10px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="top">
        <p>請用手機開啟網頁,嘗試拖曳中間的線。</p>
      </div>
      <div class="divider"></div>
      <div class="bottom">
        <button class="half-btn">🔼</button>
        <h3>我叫做 Bottom Sheet</h3>
        <h3>是手機App很常見的設計</h3>
        <p>請用手機嘗試拖曳中間的線。</p>
        <p>當我100%時只能透過按鈕控制</p>
        <p>當我50%時可以自由拖曳</p>
        <p>當我30%時只能往上</p>
      </div>
    </div>

    <script>
      const container = document.querySelector(".container");
      const divider = document.querySelector(".divider");
      const topPane = document.querySelector(".top");
      const bottomPane = document.querySelector(".bottom");
      const halfBtn = document.querySelector(".half-btn");
      let isDragging = false;

      const BottomSheet = {
        ratio: 0.5,
        getRatio() {
          return this.ratio;
        },
        setRatio(value) {
          this.ratio = value;
        },
        adjustRatio(inputRatio) {
          // 限制比例
          this.ratio = Math.max(0.001, Math.min(0.99, inputRatio));
          const containerHeight = container.offsetHeight;
          const topHeight = containerHeight * inputRatio;
          const bottomHeight =
            containerHeight - topHeight - divider.offsetHeight;

          topPane.style.flex = `0 0 ${topHeight}px`;
          bottomPane.style.flex = `0 0 ${bottomHeight}px`;
        },
        toStaticRatio(type) {
          switch (type) {
            //下面 100%
            case "full": {
              this.adjustRatio(0);
              this.ratio = 0;
              // 變更線條顏色
              divider.style.backgroundColor = "#e0e0e0";
              // 變更按鈕文字
              halfBtn.textContent = "🔽";
              break;
            }
            //下面 50%
            case "half": {
              this.adjustRatio(0.5);
              this.ratio = 0.5;
              // 變更線條顏色
              divider.style.backgroundColor = "#ccc";
              // 變更按鈕文字
              halfBtn.textContent = "🔼";
              break;
            }
            //下面 30%
            case "low": {
              this.adjustRatio(0.7);
              this.ratio = 0.7;
              // 變更線條顏色
              divider.style.backgroundColor = "#ccc";
              // 變更按鈕文字
              halfBtn.textContent = "🔼";
              break;
            }
            default: {
              break;
            }
          }
        },
      };

      // touch 事件綁定
      divider.addEventListener("touchstart", (e) => {
        e.stopPropagation();
        isDragging = true;
      });
      // 綁在 document 提升體驗(範圍廣)
      document.addEventListener("touchmove", handleTouchMove);
      document.addEventListener("touchend", handleTouchEnd);

      halfBtn.onclick = () => {
        const mode = BottomSheet.getRatio() === 0 ? "half" : "full";
        BottomSheet.toStaticRatio(mode);
      };

      function handleTouchMove(e) {
        e.stopPropagation();
        if (!isDragging) {
          return;
        }
        if (BottomSheet.getRatio() === 0) {
          return;
        }
        const containerRect = container.getBoundingClientRect();
        const touch = e.touches[0];
        const y = touch.clientY - containerRect.top;
        BottomSheet.setRatio(y / containerRect.height);
        const currentRatio = BottomSheet.getRatio();
        if (currentRatio >= 0.7) {
          return;
        }
        BottomSheet.adjustRatio(currentRatio);
      }

      function handleTouchEnd(e) {
        const currentRatio = BottomSheet.getRatio();
        let mode = "full";
        if (currentRatio >= 0.66) {
          mode = "low";
        } else if (currentRatio >= 0.4 && currentRatio < 0.66) {
          mode = "half";
        }
        BottomSheet.toStaticRatio(mode);
        isDragging = false;
      }
    </script>
  </body>
</html>

如果有更好的寫法歡迎留言分享~


上一篇
PM 說: 怎麼做出卡片的拖曳排序?
下一篇
PM 說: 身分證在前端加上浮水印如何實現?
系列文
PM說: RD大大,這個功能要怎麼寫啊?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言