封面圖是 Bus+ App,紅色框框的設計在App上很常見
它的名稱叫做 Bottom sheets
今天目標是寫一個網頁版
更多設計請參考:
https://m2.material.io/components/sheets-bottom#standard-bottom-sheet
和網頁的分割視窗線最大的不同是手機能做出"甩動操作"
甩動=移動超過一定距離就會觸發高度變化
常見的底部高度比例
0.計算比例是相反的; 因為計算高度是由上而下
//下面 30%
this.ratio = 0.7;
//下面 50%
this.ratio = 0.5;
//下面 100%
this.ratio = 0;
// 先計算距離
const y = touch.clientY - containerRect.top;
// 計算比例(%)
比例 = y / containerRect.height
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>
如果有更好的寫法歡迎留言分享~