ウェブページのスクロール量(読了率)を計算する

スクロール量

はじめに

スクロール量を取得します。ウェブページをどこまでスクロールしたのかの値です。ブログ等では、読了率として記事の良し悪しの指標としてよく利用されています。

高さやスクロール量の取得と計算といった単純な処理ですが、クロスブラウザの互換周りを考慮すると実装までだいぶ面倒だったため、記事に落とし込みました。

スクロール位置の取得する

まずは、スクロール位置の取得です。スクロール位置は、window.scrollYで取得できます。ただし、クロスブラウザー互換性を考慮するとwindow.pageYOffsetで取得したほうが無難です。次にコード例を示します。IE8-用のpolyfillも合わせて記載します。

// IE除く
var scroll = window.scrollY;
// IE9+
var scroll = window.pageYOffset;
// polyfill(IE8-)
var scroll = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;

補足(window.pageYOffset / window.scrollY)

pageYOffset プロパティは、scrollY プロパティのエイリアスです。

window.pageYOffset === window.scrollY; // 常に true

補足(Element.scrollTop)

window.scrollYと似た値として、Element.scrollTopがあります。scrollYはページ全体のスクロール位置ですが、scrollTopは要素固有のスクロール位置です。ただし、例外的に<html>scrollTopは、scrollYの値が返されます。

scrollTopは、要素の上端から(ページの)最も上に表示されているコンテンツまでの距離です。また、要素が垂直スクロールバーを生成していない場合、scrollTopの値は0になります。

今回取得するのはページ全体のスクロール量であるため、Element.scrollTopを使用せず、window.scrollYを使用します。(polyfill としては、使用しています)

補足(小数値と整数値)

window.pageYOffsetまたはwindow.scrollYの戻り値は小数値です。整数値が必要な場合、Math.round()で四捨五入する必要があります。今回の場合、整数値が必要なため、四捨五入が必要になります。

次に出てくる「ドキュメント全体の高さ(Element.scrollHeight)」「画面表示領域の高さ(Element.clientHeight)」は、小数値を整数値に補正した値です。

ドキュメント全体の高さの取得する

ウィンドウに収まる範囲ではなく、ウィンドウの領域外を含めたドキュメント全体の高さを取得します。

// IE8+
var documentHeight = document.documentElement.scrollHeight;

画面表示領域の高さの取得する

ウィンドウに収まる範囲(画面内に表示されている領域)の高さを取得します。

// IE6+
var viweHeight = document.documentElement.clientHeight;

補足

Element.clientHeightは、通常「CSSの高さ+ CSSのパディング-水平スクロールバーの高さ」を格納しています。ですが、ルート要素の場合は、ビューポートの高さを格納しています。

完全にスクロールしたか判定する

次の式が成り立つ時、スクロールが完了しています。

ドキュメントの高さ - スクロール位置 === 画面領域の高さ

ちなみに、ページ全体のどれだけスクロールしたかは、次のようになります。

// ページ全体のスクロール量(0-1, 0:スクロールなし, 1:完全にスクロール)
スクロール位置 / (ドキュメント全体の高さ - 画面領域の高さ)

補足

The following equivalence returns true if an element is at the end of its scroll, false if it isn't.

element.scrollHeight - element.scrollTop === element.clientHeight

コード(スクロール量の取得)

// スクロール量の取得(0-1, 0:スクロールなし, 1:完全にスクロール)(IE9+)
function getScrollAmount() {
  var scroll = Math.round(window.pageYOffset);
  var documentHeight = document.documentElement.scrollHeight;
  var viweHeight = document.documentElement.clientHeight;
  var scrollHeight = documentHeight - viweHeight;
  return scroll / scrollHeight;
}

コード(ページ表示後の最大スクロール量の取得)

// ページ表示後の最大スクロール量の取得(IE9+)
(function() {
  var maxPageY = 0;
  function updateScroll() {
    var y = window.pageYOffset;
    if (maxPageY < y) {
      maxPageY = y;
      //console.log(maxPageY);
      //console.log(getScrollAmount(maxPageY));
    }
  }

  var timer = null;
  window.addEventListener('scroll', function() {
    // 高頻度呼び出し対策
    window.clearTimeout(timer);
    timer = window.setTimeout(updateScroll, 300);
  }, {capture:true, passive:true});

  function getScrollAmount(maxY) {
    var documentHeight = document.documentElement.scrollHeight;
    var viweHeight = document.documentElement.clientHeight;
    var scrollHeight = documentHeight - viweHeight;
    var y = Math.round(maxY);
    return y >= scrollHeight ? 1 : y/scrollHeight;
  }

  // 最大スクロール量の送信(GoogleAnalytics用)
  window.addEventListener('pagehide', function() {
    updateScroll();
    var scroll = Math.floor(getScrollAmount(maxPageY)*100);
    var label = (Math.ceil(scroll / 10) * 10)+'%';
    //ga('send', 'event', 'scroll', 'post', label, scroll);
    //  0%:      0
    // 10%:  1- 10
    // ...
    //100%: 91-100
  });
  // Note: ページの高さが拡大する可能性を考慮しています。
  //       ページの高さが縮小する可能性は考慮していません。
})();