這是掘金網頁版的頭部,當滾動條向下滑動時,主 header 會隱藏,次級 header 會吸在頁面頂部。
樣式#
首先,我們先通過兩個 div 來模擬這兩個 header
<template>
<div class="main-header-wrapper">
<div class="main-header"></div>
</div>
<div class="sub-header-wrapper">
<div class="sub-header"></div>
</div>
<div v-for="i of 100" :key="i">測試滾動</div>
</template>
<style scoped>
.main-header-wrapper {
height: 50px;
}
.main-header {
height: 50px;
width: 100%;
background: #ff0000;
position: fixed;
top: 0;
}
.sub-header-wrapper {
height: 40px;
}
.sub-header {
height: 40px;
width: 100%;
background: #00ff00;
position: fixed;
top: 50px;
}
</style>
這裡使用絕對定位來將兩個 header 固定在頁面頂部,這兩個 div 分別是main-header
和sub-header
。他們兩個分別用容器main-header-wrapper
和sub-header-wrapper
包裹,是為了防止由於 header 脫離文檔流導致遮住正文元素。
簡化#
不妨把滾動方向的檢測放到一邊,先實現這樣的效果:為兩個 header 加上類名hidden
的時候,主 header 隱藏,次級 header 吸頂。這裡可以直接用 transform 來實現。
為了方便演示效果,增加了一個按鈕,用來添加和刪除hidden
類名。
<script setup>
import { ref } from 'vue';
const isHidden = ref(false);
</script>
<template>
<div class="main-header-wrapper">
<div class="main-header" :class="{'hidden':isHidden}"></div>
</div>
<div class="sub-header-wrapper">
<div class="sub-header" :class="{'hidden':isHidden}"></div>
</div>
<div v-for="i of 100" :key="i">測試滾動</div>
<button style="position:fixed;bottom: 10px; right: 10px;" @click="isHidden = !isHidden">
切換
</button>
</template>
<style>
html, body {
margin: 0;
padding: 0;
}
</style>
<style scoped>
.main-header-wrapper {
height: 50px;
}
.main-header {
height: 50px;
width: 100%;
background: #ff0000;
position: fixed;
top: 0;
}
.sub-header-wrapper {
height: 40px;
}
.sub-header {
height: 40px;
width: 100%;
background: #00ff00;
position: fixed;
top: 50px;
}
.main-header, .sub-header {
transition: transform .15s ease-in-out;
transform: translateY(0px);
}
.main-header.hidden, .sub-header.hidden {
transform: translateY(-50px);
}
</style>
使用選擇器.main-header.hidden
和.sub-header.hidden
來選擇具有 hidden 類名的 header 元素,將它們的 transform 屬性設置為translateY(-50px)
(即向上偏移 50px)。同時為元素設置 transition,提供緩動效果。
這樣,header 的顯示和隱藏就實現了,接下來就是檢測滾動方向。
實現#
使用 vueUse 的useScroll
和useDebounceFn
簡化了部分代碼,
<script setup>
import { ref, watch } from 'vue';
import { useScroll, useDebounceFn } from '@vueuse/core'
const isHidden = ref(false);
const { directions, isScrolling, arrivedState } = useScroll(document)
const checkHeaderStatus = useDebounceFn(
(top, bottom, topArrived) => {
if (topArrived) {
isHidden.value = false
return
}
if (top) {
isHidden.value = false
} else if (bottom) {
isHidden.value = true
}
},
100
)
watch(directions, () => {
if (isScrolling.value) {
checkHeaderStatus(directions.top, directions.bottom, arrivedState.top)
}
})
</script>
useScroll 能夠輸出響應式的滾動方向、滾動狀態(到頂部 / 到底部)和是否在滾動
其中,函數返回的 directions 是一個 reactive 對象,我們對其進行監聽,當該變量發生變化時,說明滾動方向發生了改變。
為了防止高頻觸發(例如反復上下滾動)導致 header 亂動,使用防抖函數限制了一下,其中checkHeaderStatus
函數內的
if (top) {
isHidden.value = false
} else if (bottom) {
isHidden.value = true
}
是關鍵,當向上滾動時,顯示 header;向下滾動時,隱藏 header。
你可能會發現在這段代碼的上面,還有一段不明所以的代碼:
if (topArrived) {
isHidden.value = false
return
}
當滾動條到頂部時,直接顯示 header,這是為了避免 iOS 設備中 safari 瀏覽器橡皮筋效果導致的誤判
所以為什麼 safari 橡皮筋效果也會觸發 scroll 事件啊啊啊啊啊!!!甚至 y 值能到負值
這裡還有一個問題,只檢測了滾動到頂部,而沒有檢測滾動到底部。這是因為我沒想到什麼很好的方法去檢測。
前者,可以通過判斷 y 值是否為 0(或小於 0)來實現;而後者,正常來說,滾動到底部時,文檔高度 - 視窗高度應該和滾動條的 y 值是一致的。但是在 safari 裡可能不一致,在 safari 中,當地址欄收縮時,上文的公式成立;但是當地址欄是展開狀態時,兩者會一個相差地址欄的高度。而地址欄是否展開我沒找到判斷的方法。
如果有知道如何判斷是否滾動到底部的,希望能夠和我分享分享