javascriptでchromeのアドレスバーを再現する

 Android版のgoogle chromeでは、アドレスバーが上部に配置されていますが、ページを下にスクロールするとこれが隠れるように設計されています。

 今回は、この動きをjavascriptで再現してみたいと思います。

デモページ

slidable nav

実装

 デザイン変更を考慮し、可能な限り変更箇所を少なくするよう心がけます。

 考慮すべきこと

  • スクロール時のパフォーマンス
  • CSS改修時の変更コスト

 考慮しないこと

  • リロード時の対応
  • IE対応
  • 途中で止めた場合の動作

 それでは、次項で解説していきます。

解説

 まずはHTMLを記述します。簡単なもので構いません。

 今回は以下のようにしました。

<body>
  <nav id='slidable-nav'>
    <p>nav</p>
  </nav>
  <article>
    <h1>your contents</h1>
    <p>The quick brown fox jumps over the lazy dog.</p>
    <p>The quick brown fox jumps over the lazy dog.</p>
    <p>The quick brown fox jumps over the lazy dog.</p>
  </article>
</body>

ナビゲーションバー部分とアーティクル(記事)部分の単純なものです。

 次に、CSSを記述します。

<style type='text/css'>
  body{
    height:2000px;
  }

  #slidable-nav{
    background-color:rgba(0,0,0,0.5);
    width:100%;
    position:fixed;
    top:0;
    left:0;
  }
</style>

 こちらもわかりやすいように最低限のスタイル付けを行います。

 次に、本体であるjavascript部分をコーディングしていきます。

<script>
  (function(){    
    document.addEventListener("DOMContentLoaded",function(){
      var FPS = 60;
      var previousScrollPosition = window.pageYOffset;
      var nav = document.getElementById("slidable-nav");
      var navHeight = nav.clientHeight;
      var minTop = - navHeight;
      var id = null;
      document.body.style.paddingTop = (parseInt(document.body.style.paddingTop || 0) + navHeight) + "px";

      window.addEventListener("scroll",function(){
        clearTimeout(id);
        id = setTimeout(function(){
          var y = window.pageYOffset;
          var amount = previousScrollPosition - y;
          var navY = (parseInt(nav.style.top) || 0);
          var absolute = navY + amount;

          absolute = Math.min(absolute,0);
          absolute = Math.max(absolute,minTop);

          nav.style.top = absolute + "px";
          previousScrollPosition = y;
        },1000/FPS);
      });
    });
  })();
</script>

 非常に単純ですが、順に見ていきましょう。

 グローバル汚染を防ぐため、即時関数で囲みます。
 更に、DOMContentLoadedにイベントリスナを追加します。

  (function(){
    document.addEventListener("DOMContentLoaded",function(){

 ここで、よくloadイベントに登録しているコードを見かけますが、DOMContentLoadedがDOMツリーの生成後すぐに呼ばれるのに対し、loadイベントでは、画像など全ての要素がロードされて初めて呼ばれます。

 そのため、ページ閲覧者に優れたユーザビリティを提供できない場合が多く、通常loadイベントを使うべきではありません。特段の理由が無い限りは、DOMContentLoadedイベントを登録しましょう。

 次に、使用する変数を定義します。

      var FPS = 60;
      var previousScrollPosition = window.pageYOffset;
      var nav = document.getElementById("slidable-nav");
      var navHeight = nav.clientHeight;
      var minTop = - navHeight;
      var id = null;
変数名 説明
FPS setTimeoutの間隔
previousScrollPosition 前回のスクロール位置を記憶しておくための変数
nav ナビゲーションバーのDOMオブジェクト
navHeight ナビゲーションバーの高さ
minTop ナビゲーションバーのMin位置
id setTimeoutのID

 次に、スクロール位置が一番上のときにコンテンツが隠れないよう、bodyのpadding-topを設定します。

      document.body.style.paddingTop = (parseInt(document.body.style.paddingTop || 0) + navHeight) + "px";

 通常、paddingTopプロパティはIntでなく、”○○px”といったStringで格納されています。

 javascriptのparseIntは、整数変換のためのメソッドですが、pxなどの余分な文字列を排除して数値化してくれるという、素晴らしい仕様となっています。

 しかし、空文字を評価した場合はNaNを返しますので、NaNが返ってきた場合は0を返し、更にnavHeight分を加えてpadding-topを設定します。

 次に、イベントリスナを追加します。

      window.addEventListener("scroll",function(){

 おなじみですね。スクロールにフックして関数を呼びます。

 ここで直接スタイルの変更処理を書いてもいいのですが、スクロールイベント毎に関数を呼ぶとパフォーマンス的な問題があったりするので、一工夫加えます。

        clearTimeout(id);   
        id = setTimeout(function(){

 setTimeoutを使い、遅延させることで、一定時間以内に連続してイベントが発火した場合は、最後のイベントのみ呼ばれるようにします。

          var y = window.pageYOffset;
          var amount = previousScrollPosition - y
          var navY = (parseInt(nav.style.top) || 0);
          var absolute = navY + amount;

 pageYOffsetで現在のスクロール位置を取得し、記憶していたスクロール位置から現在のスクロール位置を引いた差分をとり、amount(移動量)を求めます。

 次に、ナビゲーションの現在のtop位置を取得し、amountを足して絶対位置を求めます。この絶対位置が、設定するtop値になります。

 そのままtopに設定できれば良いのですが、そうするとスクロールした分だけ無限にtop値が変化してしまいます。そのため、変化する上限を設定する必要があります。

 if文を使っても三項演算子を使っても構いません。自分がやりやすい方法で実装します。

          absolute = Math.min(absolute,0);
          absolute = Math.max(absolute,minTop);

 今回は、Math.min、Math.maxでお茶を濁します。

 絶対位置が求められたら、それを設定するだけですね。

          nav.style.top = absolute + "px";
          previousScrollPosition = y;

 前回スクロール位置の記憶用変数に、今回のスクロール位置を忘れず設定します。

        },1000/FPS);

 setTimeoutの第二引数は、ミリ秒で評価されますので、1000/FPSの値を設定します。

 これで、簡易ではありますが、スクロールにあわせて画面に吸い付くようにナビゲーションが表示されるようになりました。

 工夫は必要ですが、更にパフォーマンスをよくすることもできますし、途中で止めた場合は完全に表示したり、逆に完全に非表示にできたりもします。

ユーザビリティを考える

 ところで、今回実装したようなナビゲーションは、見栄えがよくてかっこいいのですが、スマートフォンサイトなどでは、貴重な画面の領域を食いつぶしてしまう一因となります。
 文章主体のサイトでは、少し戻って文章を読み直したい場合もあるでしょう。

 たとえば、おしえてgooという有名なサイトでは、上に少しでもスクロールすると、アニメーション等も殆どなく、パっとグローバルナビが出現します。こういった実装は、必要なコンテンツの閲覧を阻害してしまったり、突然現れることによってストレスを感じてしまうユーザーも多いことでしょう。

 コンテンツは、ユーザーに向けて発信されるものであって、開発者の独りよがりになってはいけません。

 今回紹介した実装方法も、閲覧する環境によっては、スクロールがカクついたりなどの弊害があるかもしれません。例えば、ある一定以上の移動量になったらスクロールを行うなどで応急的な処置は可能だと思いますが、これらの障壁を全て取り除くことは不可能です。

 うまく妥協点を見つけつつ、ユーザビリティの優れたページを作っていきたいですね。