1. シナリオ
「スティッキーヘッダー(吸頂)」は比較的古いインタラクション手法であり、PCページでは長年使用されてきました。以下の図のようになります。
[caption id="attachment_1210" align="alignnone" width="539"]
sticky[/caption]
スティッキー要素の初期位置は通常ページ上部付近ですが、最上部とは一定の距離があり、この領域にはバナー画像など最も目立つ要素が置かれます。ページが下へスクロールし、スティッキー要素の初期位置を超えたとき、その要素を上部に固定します。
スティッキー化が求められる要素は、一般的にサブナビゲーションバー、検索ボックス、記事のタイトルバー(h1)、テーブルヘッダー(thead)、タブバーなどです。共通する特徴は、コンテンツや機能の面で比較的重要であるものの、最も重要な要素ではないということです(最も重要な要素は通常、常にページ上部に固定されます。例:navbar-fixed-top)。
2. PC向け解決策
ページが一定の位置までスクロールした際に、何らかの処理を行います。
「トップへ戻る」ボタンも同様で、ページが下へ150px以上スクロールした時にボタンを表示し、それ以外は非表示にします。
したがって、実装の基本的な考え方はscrollイベントを監視することです。
var stickyEl = document.querySelector('.sticky');
var stickyT = stickyEl.offsetTop;
window.onscroll = function(e) {
var scrollT = document.body.scrollTop;
// console.log(scrollT, stickyT);
if (scrollT > stickyT) {
stickyEl.classList.add('fixed-top');
}
else {
stickyEl.classList.remove('fixed-top');
}
};
「トップへ戻る」の実装方法と全く同じであり、一見するとうまく動作しているように見えます。しかし、臨界点であるstickyTまでスクロールした瞬間、ページがガタつき、上に少し縮むことにすぐに気づくでしょう。これは、stickyElがその瞬間にfixedによって通常のフローから外れ、下の要素が上にせり上がってきて元の場所を占拠するため、ページがガタつくのです。
滑らかでガタつきのない動きにするためには、stickyElの元の場所を確保しておくプレースホルダーが必要です。
var stickyEl = document.querySelector('.sticky');
// 場所を確保するプレースホルダー
var stickyHolder = document.createElement('div');
var rect = stickyEl.getBoundingClientRect();
// console.log(rect);
stickyEl.parentNode.replaceChild(stickyHolder, stickyEl);
stickyHolder.appendChild(stickyEl);
stickyHolder.style.height = rect.height + 'px';
var stickyT = stickyEl.offsetTop;
window.onscroll = function(e) {
var scrollT = document.body.scrollTop;
// console.log(scrollT, stickyT);
if (scrollT > stickyT) {
stickyEl.classList.add('fixed-top');
}
else {
stickyEl.classList.remove('fixed-top');
}
};
スティッキー要素を同じ高さのプレースホルダーで包むことで、臨界点でstickyElがfixed状態になっても、stickyHolderが空間を維持し、下の要素がせり上がってこなくなり、ページのガタつきが解消されます。
ただし、この方法にも問題があります。スティッキー要素の上にある各要素の読み込みが遅い場合、取得したstickyTが実際よりも小さくなったり、最悪0になったりします(上に大きなバナー画像がある場合など)。そのため、デフォルトの画像プレースホルダー(base64)と併用するか、手抜きをしてとりあえずmin-heightで高さを確保しておき、上の画像がonloadされたタイミングでstickyTを修正する必要があります。
3. モバイル向け解決策
原理的には、そのまま持ち込むことが可能です。Android 4.0以上では確かに機能しますが、iOSではほぼ全滅です。
Androidのscroll
Android 4.0のscrollイベントはそれほどリアルタイムではありません(スロットリングがかかっているような感覚)が、Android 4.1以降のscrollイベントはPCとほとんど変わりません。
The Android browser in Ice Cream Sandwich fires the event but doesn’t feel very responsive and only sporadically re-paints the DOM to move the blue box. Luckily, Jelly Bean’s Android browser handles this example perfectly; everything is updated and rendered smoothly as the user scrolls.
(参考資料1より引用)
ページがスクロールしている間、scrollイベントは激しくトリガーされるため、手動でスロットリング(間引き)を行う必要があり、まさにこれこそ私たちが求めている挙動です。もしscroll自体にスロットリングがかかっていると、臨界点の判定を見逃しやすく、スティッキー要素が「ジャンプ」してしまい、滑らかな体験が得られません。
iOSのscroll
iOS 8未満のSafari(UIWebViewを含む)では、scrollイベントに大きな制限がかけられています。
指で画面をスワイプ -> スクロール -> 指を離す -> 慣性スクロール -> スクロール停止
このプロセス全体を通して、スクロールが完全に停止した時に初めてscrollイベントが1回だけトリガーされます。つまり、iOS 8未満ではscrollがscrollendに変わってしまっているのです。スクロールを監視して位置を判定する方法は完全に機能せず、滑らかなスティッキー効果は、臨界点を超えてスクロールし停止した時に初めて要素が目標位置へジャンプするという、非常に劣悪な体験となり、耐え難いものになります。
scrollが使えないなら、タイマーでscrollTopを読み取ったり、touchmove、iscrollを使うといった、少し変わったアプローチが考えられます。
ある先人が詳細なテストを行っています(参考資料1を参照)。
タイマーは指が画面から離れていない時は実行されません。touchmoveは十分な頻度でトリガーされ、scrollTopも取得できますが、touchend後の慣性スクロール中は利用できるイベントがなく、その間のscrollTopを取得できません。この慣性スクロールの距離(減速運動)を予測することは非常に困難であり、iOSのバージョンによって計算方法が同じかどうかも定かではありません。
iscrollのような擬似スクロー��であれば、当然リアルタイムにスクロール位置を取得できます。iscrollにはそのための専用バージョンが存在します。
iscroll-probe.js, probing the current scroll position is a demanding task, that's why I decided to build a dedicated version for it. If you need to know the scrolling position at any given time, this is the iScroll for you. (I'm making some more tests, this might end up in the regular iscroll.js script, so keep an eye on it).
iOS 8以上のSafariやWKWebViewでは、指が画面にあるかどうか、慣性スクロール中であるかどうかにかかわらず、scrollイベントが激しくトリガーされるようになりました。しかし、iOS 8以上のUIWebViewでは、scrollの制限はまだ残っています。
iOS 8未満のデバイス、および任意のiOSバージョンのUIWebViewをサポートする必要がある場合、このアプローチは行き止まりです。scrollのことは忘れましょう。
sticky
scrollによる方法はうまくいきませんが、iOSは別の手段を提供しています。それがposition: stickyです。iOS 6.1からサポートされており、最近になってChrome 56でもサポートされました。
このCSSルールは、スティッキーヘッダー専用です。一般的な使い方は以下の通りです。
.sticky {
// 初期位置を超えてスクロールした時に自動でスティッキー化
position: -webkit-sticky;
position: sticky;
// スティッキー時の位置指定
top: 0;
left: 0;
// 下にあるすべての要素よりもz-indexを高くする
z-index: 9999;
}
初期位置を超える前は、position: relativeと似た動作をします(空間を占有し、子孫要素の配置基準を提供します)が、topやleftは無効です。
初期位置を超えると、position: fixedと似た動作になり、topやleftが有効になって画面の可視領域に固定されます。しかし、ページはガタつかず、元々占有していた空間は維持されます(場所確保のプレースホルダーが内蔵されているような感覚です)。
スティッキー効果は非常に滑らかで、Androidのscroll方式よりもスムーズな体験が得られます。しかし制限も明確で、スティッキー状態になっているかどうかをリアルタイムで知ることができません。これに関連する様々な効果が制限されます。例えば、スティッキー化するタブリストなどです。
[caption id="attachment_1211" align="alignnone" width="255"]
sticky-tab[/caption]
非スティッキー状態の時は、リスト部分をスワイプしてページ全体をスクロールさせ、スティッキー状態へと移行します。複数のタブリストはシームレスに切り替わり、スクロール状態は互いに影響しません。
スティッキー状態の時は、現在のタブリストをスワイプし、一番上まで到達するとページ全体がスクロールし、非スティッキー状態へと戻ります。
つまり、非スティッキー状態の時はタブリストをスクロール不可(overflow-y: hidden)にし、スティッキー状態の時はタブリストをスクロール可能(overflow-y: auto)にする必要があります。
しかし、iOSのstickyは私たちが制御できるものではなく、スティッキー状態をリアルタイムに知ることができません。スティッキー状態を知ろうとすると、ページスクロール中にスクロールバーの位置をどうやってリアルタイムに取得するかという最初の問題に逆戻りします。CSSのstickyではこの問題を解決できません。
筆者はまだ適切な解決策を見つけられていません。現在のところ、タブごとのスクロール状態の独立性を犠牲にし、複数のタブでbodyのスクロールバーを共有し、タブ切り替え時に前の位置へスクロールして戻すという方法をとっています。これによりスティッキー状態の判定を避けることができますが、タブリストのシームレスな切り替えという完璧な体験は犠牲になります。
もし新しいアプローチ、良いアイデア、または確立された解決策をご存知でしたら、ぜひお知らせください。大変ありがたく存じます。
4. オンラインデモ
-
PC、Android 4.0+、
WKWebView向けソリューション:http://www.ayqy.net/temp/sticky/sticky-pc.html -
iOS 6.1+向けソリューション:http://www.ayqy.net/temp/sticky/sticky-ios.html
5. まとめ
-
一般的な要素のスティッキー化:Androidは
scroll方式を採用し、許容できる範囲内で手動スロットリングを行いパフォーマンスを向上させます。iOSはCSSのstickyを使用します。もしiOS 8未満や任意のバージョンのUIWebViewをサポートする必要がないのであれば、iOSでもscroll方式を採用することができます。 -
スティッキー化するタブリスト:良い解決策が見つかっていないため、一時的にシームレスな切り替えを犠牲にする方式を採用します。
ページ全体にiScrollを適用するのはリスクの高いアプローチです。ページが複雑な場合、安易に試すべきではありません。たとえ今は複雑でなくても、将来複雑にならないという保証はありません。
参考資料
-
onscroll Event Issues on Mobile Browsers:詳細な
scrollイベントのテスト記事であり、多くの人の時間を節約してくれました。 -
Why the Scroll Event Change in iOS 8 is a Big Deal:iOS 8で
scrollイベントの制限が解除された後の変化を実例で紹介しています。これも上記の先人による記事です。 -
javascript scroll event for iPhone/iPad?:iOSの
scrollイベントの制限を図解で説明しています。 -
CSS “position: sticky” – Introduction and Polyfills:PolyfillはすべてPC向けであり、モバイルではあまり役に立ちません。
コメントはまだありません