position: sticky 在移動端的應用與實踐
前面《iOS 與 彈性滾動》里講到,iOS 的 UIWebkit 內核瀏覽器中啟用彈性滾動后,滾動事件不會立即觸發的問題。不過話說回來,綁定 scroll 本來就對整體 UI 性能影響很大,某些通常需要綁定 scroll 事件的東西其實有其他更為簡便的實現方式。
比如說有這樣一個很常見需求:一個長列表里分了很多小節,每個小節有一個頭部標題。要求當各小節尚未完全滾動到屏幕外時,小節的頭部標題始終固定在屏幕頂部。好多人一看到“滾動”這個詞就直接監聽 scroll 事件開始搞,其實對于這個需求有更好的、更方便的解決方式,這就是本文的主角: position: sticky
什么是 position: sticky
The box position is calculated according to the normal flow (this is called the position in normal flow). Then the box is offset relative to its flow root and containing block and in all cases, including table elements, does not affect the position of any following boxes. When a box B is stickily positioned, the position of the following box is calculated as though B were not offset. The effect of ‘position: sticky’ on table elements is the same as for ‘position: relative’.
我是看了幾遍沒看懂,但根據實踐, sticky (下文我把他翻譯為粘性定位)是這樣一種定位方式。如果有
#one {
position: sticky;
top: 10px;
}
-
當 #one 處于可視范圍內時, #one 表現的就像一個普通的 static 元素(但是它仍非 static 定位,所以仍然是內部元素的 offsetParent )。
-
當 #one 處于可視范圍之外(相對于 #one 外層的第一個非 overflow: visible 元素而言,如果沒有則為整個 window ,這里以 #parent 代替), #one 即被“粘”在 #parent 的頂部。
其中 10px 是 #one 的上邊框( border-box )至 #parent 的上邊框( content-box )的距離。如果未設置,則粘性定位效果對于該邊不起作用。
-
當 #one 與 #parent 中間還有其他靜態定位的父級元素, #one 將被限制在其父級元素之內。當與 條款2 沖突時,本條款覆蓋上一條。
-
對 table 元素無效,相當于 position: relative 。
說起來比較抽象,下面以幾個示例說明。
例子中,整個 window 為一個滾動區域,所有 dt 相對于 window 粘性定位。向下滾動時,所有 dt 會堆疊到窗口頂部。
例子中, #wrapper 嵌在 #parent 內,構成一片較大的滾動區域, #child 處于 #wrapper 的中心,相對于 #parent ( #child 外層第一個非 position: static 的元素)粘性定位。可以看到,無論怎樣滾動,中間的黑框 #child 始終處于 #parent 的內部。
為了便于更好的說明 條款3,下面的例子對上面兩例略加修改
本例將 例1 略微改動,把各個 dt 單獨放入一個 dl 中。向下滾動時可以看到類似下面的 dl 把上面的 dl 頂上去的效果。 其實并非如此,多個粘性定位元素并無關聯。產生這樣效果的原因僅僅是因為 dt 的父元素 dl 整個都被滾動到了窗口外, dl 隨之把粘性定位的 dt 給帶走了
本例將 例2 略微修改,給 #child 包了一層大小一致的 div#wrapper1 ,同樣是水平豎直居中,粘性定位卻“失效”了。其實原因與 例3 一樣,粘性定位的 #child 只是被 #wrapper1 牢牢地固定住了,定位效果并未失效。
position: sticky 能做什么
首先就是引題中的需求:固定列表頭。示例 1、3 已經實現了這個效果。不僅僅是列表頭,文檔頭、段標題,甚至兩邊的側邊欄都可以用——如果你想給側邊欄一個浮動效果的話。一個例外是表格的標題欄,可以看到 MDN 的最后一句話: position: sticky 對表格元素不起作用,當然你完全可以用別的方式模擬 table 布局。
position: sticky 相對于綁定 scroll 事件的優點
首先最大的優點:有了它我們不用再綁定惡心、緩慢,還有各種兼容性問題的 scroll 事件了。其次:簡單。設置一個 CSS 屬性的事情干嘛要 JS 操心,布局的東西本來就應該使用純 CSS 實現。最后: position: sticky 與 -webkit-overflow-scrolling: touch 相性極佳,滾動效果無比順滑,并非 scroll 事件可以模擬。
position: sticky 的瀏覽器兼容性
這是一個不可避免的問題。很不辛,Android 陣營全部陣亡。Chrome 當前的狀態是 In development ,canary 版本上已經可以體驗到其初步實現,相信不久之后就會看到 Chrome(Blink) 的正式支持。除 Safari 陣營外,Firefox 也已經支持了此屬性,建議調試粘性定位效果時在 Firefox 上調試,怎么說也比 Safari 的調試器好用。
另外還是由于 iOS 的限制,所有的 iOS 瀏覽器包括 Chrome 在內,和其他內置的比如微信內嵌瀏覽器全部支持此屬性。
順便一提 Edge 的狀態是 Under Consideration ,微軟的瀏覽器怎樣都好了。。。
position: sticky 的 fallback 實現
在 Chrome 的原生粘性定位實現來臨前,我們仍需要一個 fallback 實現,使用 absolute 模擬 sticky 效果。簡單起見,這里只考慮縱向滾動 window 的情況,以例 http://codepen.io/CarterLi/pen/qZmKzX 為基礎做修改。
原始頁面如下所示
- var n = 1
while n <= 20
dl
dt= 'TITLE ' + (n++)
each val in [1, 2, 3, 4, 5, 6, 7, 8, 9]
dd= val + ' ' + val
將標題行用一個 div 包一層,用于絕對定位元素之后給原位置占位。
- var n = 1
while n <= 20
dl
dt
div= 'TITLE ' + (n++)
each val in [1, 2, 3, 4, 5, 6, 7, 8, 9]
dd= val + ' ' + val
首先需要檢測瀏覽器是否支持 position: sticky
var elem = document.createElement('div');
elem.style.position = '-webkit-sticky';
elem.style.position = 'sticky';
if (elem.style.position.indexOf('sticky') < 0) {
// 當前瀏覽器不支持粘性定位,需要 fallback 實現
}
預先把占位 div 的高度設置好
Array.prototype.forEach.call(document.querySelectorAll('dt'), function (elem) {
elem.style.height = elem.clientHeight + 'px';
});
監聽 scroll 事件,遍歷所有標題行,找到需要 sticky 效果的行,添加類名 sticky 。
addEventListener('scroll', function() {
var stickyElements = document.querySelectorAll('dt');
for (let idx = 0; idx < stickyElements.length; ++idx) {
var elem = stickyElements[idx];
// 對于滾 window 而言,BoundingClientRect 就是元素的視口坐標值
var clientRect = elem.getBoundingClientRect();
// 如果標題行被滾到了窗口外
if (clientRect.top < 0) {
const parentBottom = elem.parentElement.getBoundingClientRect().bottom;
if (parentBottom < 0) {
// 如果父元素整個區域都滾動到了窗口外,則去除標題行的 sticky 類
elem.classList.remove('sticky');
} else {
// 添加 sticky 類,將標題行固定在頂部
elem.classList.add('sticky');
// 動態計算 style.top,表現推上去的效果
elem.style.top = Math.min(0, parentBottom - clientRect.height) + 'px';
}
} else {
// 如果標題行還在窗口內部或下面,則中斷循環,將其后的所有標題行的 sticky 類全部刪除
// 用于解決用戶滾動太快時的問題
for (let j = idx; j < stickyElements.length; ++j) {
stickyElements[j].classList.remove('sticky');
}
break;
}
}
});
CSS 代碼中添加 sticky 類,用于置頂標題欄
.sticky div {
position: fixed;
left: 0;
right: 0;
top: inherit;
}
完整示例: http://codepen.io/CarterLi/full/LNwJmq
來自: https://fe.ele.me/position-sticky-zai-yi-dong-duan-de-ying-yong-yu-shi-jian/