うるおいらんど

【JavaScript】指定した位置で慣性スクロールを止めよう【スマホ】

JavaScript

どうも、Reoです。

スマホ等のタッチデバイスでスクロール時に、指定した位置でスクロールをストップさせたい!というのを実装してみたので紹介します。

PureでVanillaな素のjsで実装しました。

 

今回デモ用に作ったのは以下のものです。

指定したボックスの下端が画面の中心に来たらスクロールが止まります。

スクロールを止める方法

スクロールを止める方法は結構色々試しました。

試した方法は3種類。

  1. addEventListenerでスクロール中にpreventDefaultをする
  2. スクロール中に画面をタッチしたことにして止める
  3. 指定座標についたらscrollToで同じ座標にスクロールさせる

1はtouchmoveを指定してやってみましたが、慣性スクロールは止まらず、慣性スクロールが止まった後のtouchmoveを無効にすることしかできませんでした。

2の擬似タッチはタッチしたことにならず諦め。原理的にはイケそうな気はしてます。

 

そして今回の方法(3番)は発想の転換、というかそんな感じで偶然発見できた方法です。なにより、一番コード量が少なく簡単にできます!

スクロールを止める部分のコードはこちら。

/**
 * スクロールを止める
 */
function stopScroll() {
    // 現在のスクロール量
    var scrollOffsetY = window.pageYOffset;
    window.scrollTo(0,scrollOffsetY);
}

 

実質たったの2行で実現できちゃいました。 あとは位置指定部分と、スクロール中に呼ばれる部分を実装すればおkです。

要素が指定した範囲内にあるかの判別

要素(item)が指定範囲内にあるかどうかを返す関数を書きます。

function isItemInScreen( item ) {
    var screenHeight = window.innerHeight; // 画面の高さ
    var itemBottomY = item.getBoundingClientRect().bottom; // item bottom座標
    var boundary = screenHeight / 2; // 上からスクロールしてくるとここで止まる 画面topからどれだけか

    if ( 0 <= itemBottomY && itemBottomY <= boundary ) {
        return true;
    }

    return false;
}

今回は画面トップ(0)から画面の真ん中までの範囲内に要素があれば、trueが返るものを用意しました。

getBoundingClientRect()で要素のページ内座標が取得できます。

ここはお好みで止めたい位置を指定ください〜。

 

スクロール中の処理を書く

最後にスクロール中の処理を書きます。window.onscrollを使います。  

/**
 * Scroll
 */
window.onscroll = scrollWindow;

/**
 * スクロールされている間呼ばれる
 */
function scrollWindow() {
    // 要素を取得
    var items = document.querySelectorAll('.stop');
    items.forEach( function(item){
        var backgroundColor = item.style.backgroundColor;
        if ( isItemInScreen(item) && !(backgroundColor == 'rgb(37, 37, 37)') ){
            item.style.backgroundColor = 'rgb(37, 37, 37)';
            stopScroll();
        }
    });
}

scrollWindow内にて要素を取得し、アイテムが範囲内にあるかどうかをチェックし、あればスクロールを止めます。

今回のデモでは、止まるのは最初の一度だけです。止まる時に色を変えて、その色で条件分岐しています。本来ならもうちょっと別のことで条件分岐したほうがいいと思います。

 

はい、これだけ!

全体コード

gistにコードあげときました。一応htmlとcssも付けておきました。

https://gist.github.com/uruly/e1bcf3348e1458c301c67b602e9023fa

実はjs単体の記事は初めてなので、CodePen的なのは何も用意してないです。あとで追記するかもしれないし、しないかもしれないです。

慣性スクロールを止めるのは使いどころは結構難しいような、でも意外とあるような、そんな気がしてます。 例えばヘッダーとかフッターでスクロールoffsetがマイナスになるのを防ぐとか。

onScrollが必ず境界値で呼ばれるわけではないのでスクロール速度が速いとずれることがあります。

 

最近気付いたけれど、Xcodeのシミュレータならローカルファイルでもチェックできるので便利ですね。

 

ちなみに慣性スクロールはタッチデバイスでのみなので、同様なものをPC側で実装するには、addEventListenerで一定時間preventDefaultで実現することができます。動かしてみるとちょっと微妙だけれど。

これはjavascript - How to disable scrolling temporarily? - Stack Overflowの回答で実装できます。

 

書き終わってプレビューして見たらGIFが酷いね。。。雰囲気が伝われば・・・。

ほいでは〜〜〜。

参考リンク

Comments

こんにちは。 大変参考になる記事、ありがとうございます。

ただ、今回のコードでは止まるのは最初の一度だけですが、 itemBottomYを超えたら「下にスクロールできない」「上にスクロールはできる」 を実現するにはどうしたら良いでしょうか?

ご教授いただけたら幸いです。 よろしくお願いいたします。

小辻裕太

初めまして、コメントありがとうございます。

本記事の方法ですと、「下にスクロールできない」かつ「上にスクロールはできる」を実現するのは難しいかと思います。 あくまで、慣性スクロール(指を離している状態でのスクロール動作)を一時的に止めるものになりますので...。

また、私自身JavaScriptはあまり詳しくはなくて申し訳ないのですが、私の調べた限りでは実現はかなり難しいのではないかな?と思います。

まず実装の条件として以下があります。

スクロール中に指定場所を超えているかどうかを判定する(itemBottomY を超えているかどうか) 超えている場合はスクロールを無効にする 1, 2の状態で上にスクロールをする場合にのみスクロールを有効化する

この1~3の条件を判定する場所は window.onscroll の中になります。

2の時点でスクロールを無効化する方法として document.addEventListener('touchmove', function(e) {e.preventDefault();}, {passive: false}); のように touchmove 自体を無効化にします。

そうすると、2の時点でスクロールは行われなくなってしまうので、3を判定する方法がなくなってしまいます。 「上にスクロールする」も「下にスクロールする」のどちらも受け付けない状態になっています。

このように一度スクロール無効をしてしまうと、解除するきっかけが作れないので、本件は実現が難しいのではないかなと思います。

この他にももしかすると方法はあるのかも知れませんが、私の知っている限りではこのような回答になってしまいます。 お力になれず、申し訳ございません。

Reo(管理人)