大前提
ウェブサイトを作っていて、スクロールを起点に演出を行い場合が多々あります(ページがスライドしたりなど)。
しかし、通常のマウスと違い、MacBookとかのトラックパッドだと慣性スクロールが働いて、ユーザーがトラックパッドから指を離した後も一定時間スクロールし続けます(onWheelイベントやonScrollイベントが発火し続けます)。
これを制御したいというのが今回のテーマです。
失敗ケース
とりあえず以下に試してみたけどダメだったやつを。
ググる
先人の方々がとっくに解決策を見つけてくれてるだろうと期待しましたが、ベストな策は見つかりませんでした。
同じ悩みの方はたくさん見つかったのですが……。
http://stackoverflow.com/questions/12486141/check-if-user-is-scrolling-with-trackpad
慣性スクロールを切る
CSSとかJSで慣性スクロールを無効化できるか探しましたが、そのようなプロパティはありませんでした。
イベントをonceにすれば、慣性スクロールを殺せるのでは……
onWheelイベントをonceにして、初回発火だけ拾い、setTimeoutとかで一定時間後に再登録すれば
初回の時の慣性スクロールが強制キャンセルされるのではと思いましたが、見事に生きていました。
慣性スクロールはユーザースクロール(ユーザーがトラックパッドを触っている時のスクロール)と違い、規則的な感覚で発火しているのでは……
event.timeStamp でイベントの発火時間が取得できるので、慣性スクロール時は感覚が一定なのでは(それによりユーザースクロールによるものか否かを識別できるのでは)と思いましたが、見事にバラバラでした。
その他にも2〜3個思いついては試しましたが、すべて不発となりました。
成功ケース
というわけで、結果うまくいった策は以下になります。
var MOUSE_WHEEL_EVENT = 'onwheel' in document ? 'onwheel' : 'onmousewheel' in document ? 'onmousewheel' : 'DOMMouseScroll';
var callback = function () {};
var isFired = false;
var sleep = 250;// ここの値はお好み。小さくしすぎると一度のユーザースクロール中に複数回発火するし、大きすぎるとそもそもcallbackが発火しない。
var delta, uintDelta, timeStamp;
// キャッシュ
callback.delta = 0;
callback.timeStamp = Date.now();
document[MOUSE_WHEEL_EVENT] = function (e) {
e.preventDefault();
delta = e.deltaY ? -(e.deltaY) : e.wheelDelta ? e.wheelDelta : -(e.detail);
// 移動量が0の場合は無視。
// ユーザースクロールと慣性スクロールの切り替わりの瞬間に、0が発生することがあるので、それの予防も兼ねて。
if (!delta) {
return;
}
uintDelta = Math.abs(delta);
if (uintDelta - callback.delta > 0) {
// 増加
timeStamp = e.timeStamp;
if (!isFired && timeStamp - callback.timeStamp > sleep) {
callback();
isFired = true;
}
callback.timeStamp = timeStamp;
} else {
// 減退
isFired = false;
}
callback.delta = uintDelta;
};
これによってユーザースクロール1回ごとに1回のイベント発火が可能になりました。
ポイントは、ユーザースクロールと慣性スクロールの見分け方を timeStamp でなく、
スクロールの増加量によって判断しています。
ユーザースクロール中は(例外はあれど)スクロール量が段々増えていき、慣性スクロールは段々減っていきます。
なので、スクロール量が増えた初回だけcallbackを発火し、その後減退したらcallbackの発火許可(isFired)を再び与えます。
また、それだけだとユーザースクロール中にスクロール量が減退したときにcallbackが複数回発火してしまいので
timeStamp でセーフティネットを設けています。
とりあえずこれが自分が見つけられた一番使い心地(スクロール心地)の良い方法でした。
もっといい案がある場合は教えて頂けると幸いです。