visualViewport の導入

ビューポートが複数あるとしたらどうでしょう。

BRRRRAAAAAAAMMMMMMMMMM

現在使用しているビューポートは、実際にはビューポート内のビューポートです。

BRRRRAAAAAAAMMMMMMMMMM

また、DOM から取得したデータが、そのうちの 1 つのビューポートを指している場合もあります。

BRRRRAAAAM… えっ、何?

以下をご覧ください。

レイアウト ビューポートとビジュアル ビューポート

上の動画では、ウェブページのスクロールとピンチズームが行われています。右側のミニマップには、ページ内のビューポートの位置が表示されています。

通常のスクロールでは、緑色の領域は、position: fixed アイテムが貼り付けられるレイアウト ビューポートを表します。

ピンチ操作でズームすると、動作がおかしくなります。赤いボックスはビジュアル ビューポートを表します。これは、実際に表示されるページの部分です。このビューポートは移動できますが、position: fixed 要素はレイアウト ビューポートに接続されたまま、元の位置に留まります。レイアウト ビューポートの境界でパンすると、レイアウト ビューポートも一緒にドラッグされます。

互換性の向上

残念ながら、Web API は参照するビューポートに関して一貫性がなく、ブラウザ間で一貫性もありません。

たとえば、element.getBoundingClientRect().yレイアウト ビューポート内のオフセットを返します。ページ内の位置を取得したい場合は、次のように記述します。

element.getBoundingClientRect().y + window.scrollY

ただし、多くのブラウザでは window.scrollYビジュアル ビューポートが使用されるため、ユーザーがピンチ操作でズームすると、上記のコードが中断されます。

Chrome 61 では、window.scrollY が変更され、代わりにレイアウト ビューポートを参照するようになりました。つまり、上記のコードはピンチズームしても機能します。実際、ブラウザはすべての位置プロパティを徐々に変更し、レイアウト ビューポートを参照するようにしています。

1 つの新しいプロパティを除き、

ビジュアル ビューポートをスクリプトに公開する

新しい API は、視覚的なビューポートを window.visualViewport として公開します。これはドラフト仕様で、クロスブラウザ承認を受けており、Chrome 61 でリリースされます。

console.log(window.visualViewport.width);

window.visualViewport は次のような情報を提供します。

visualViewport 件の宿泊施設
offsetLeft ビジュアル ビューポートの左端とレイアウト ビューポートの間の距離(CSS ピクセル単位)。
offsetTop ビジュアル ビューポートの上端とレイアウト ビューポートの間の距離(CSS ピクセル単位)。
pageLeft ビジュアル ビューポートの左端とドキュメントの左境界との距離(CSS ピクセル単位)。
pageTop 視覚的なビューポートの上端とドキュメントの上端境界との間の距離(CSS ピクセル単位)。
width ビジュアル ビューポートの幅(CSS ピクセル単位)。
height 可視ビューポートの高さ(CSS ピクセル単位)。
scale ピンチ操作によるズームで適用されるスケール。ズームによりコンテンツのサイズが 2 倍になった場合、2 が返されます。これは devicePixelRatio の影響を受けません。

イベントもいくつかあります。

window.visualViewport.addEventListener('resize', listener);
visualViewport 件のイベント
resize widthheightscale が変更されたときにトリガーされます。
scroll offsetLeft または offsetTop が変更されたときに呼び出されます。

デモ

この記事の冒頭の動画は visualViewport を使用して作成されています。Chrome 61 以降でご覧くださいvisualViewport を使用して、ミニマップをビジュアル ビューポートの右上に固定し、逆スケールを適用して、ピンチ ズームしても常に同じサイズで表示されるようにしています。

解決済み

イベントは、視覚的なビューポートが変更された場合にのみ発生します。

当たり前のことのように思えますが、visualViewport を初めて使用したときには、この点に注意が必要でした。

レイアウト ビューポートのサイズが変更されても、ビジュアル ビューポートのサイズが変更されない場合、resize イベントは発生しません。ただし、視覚的なビューポートの幅と高さも変更せずに、レイアウト ビューポートのサイズを変更することは一般的ではありません。

問題はスクロールです。スクロールが行われても、視覚的なビューポートがレイアウト ビューポートに対して静止したままである場合、visualViewportscroll イベントは発生しません。これは非常に一般的なことです。通常のドキュメントのスクロール中、ビジュアル ビューポートはレイアウト ビューポートの左上にロックされたままであるため、visualViewportscrollトリガーされません

pageToppageLeft など、ビジュアル ビューポートのすべての変更を検出するには、ウィンドウのスクロール イベントもリッスンする必要があります。

visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
window.addEventListener('scroll', update);

複数のリスナーとの作業の重複を避ける

ウィンドウで scrollresize をリッスンする場合と同様に、結果としてなんらかの「更新」関数を呼び出すことになります。ただし、これらのイベントの多くは同時に発生することが一般的です。ユーザーがウィンドウのサイズを変更すると、resize がトリガーされますが、scroll もトリガーされることがあります。パフォーマンスを向上させるため、変更を複数回処理しないようにします。

// Add listeners
visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
addEventListener('scroll', update);

let pendingUpdate = false;

function update() {
    // If we're already going to handle an update, return
    if (pendingUpdate) return;

    pendingUpdate = true;

    // Use requestAnimationFrame so the update happens before next render
    requestAnimationFrame(() => {
    pendingUpdate = false;

    // Handle update here
    });
}

単一の update イベントなど、より良い方法がある可能性があるため、この問題に関する仕様の問題を報告しました

イベント ハンドラが機能しない

Chrome のバグのため、以下は機能しません

すべきでないこと

バグあり - イベント ハンドラを使用

visualViewport.onscroll = () => console.log('scroll!');

代替方法は次のとおりです。

すべきこと

動作する - イベント リスナーを使用

visualViewport.addEventListener('scroll', () => console.log('scroll'));

オフセット値は四捨五入されます

これは別の Chrome バグだと思います(そうであってほしいです)。

offsetLeftoffsetTop は丸め処理されるため、ユーザーがズームインするとかなり不正確になります。この問題はデモで確認できます。ユーザーがズームインしてゆっくりとパンすると、ミニマップがズームされていないピクセル間でスナップします。

イベントの発生頻度が低い

他の resize イベントや scroll イベントと同様に、特にモバイルでは、フレームごとに発生するわけではありません。これはデモで確認できます。ピンチ操作でズームすると、ミニマップがビューポートに固定されなくなります。

ユーザー補助

デモでは、visualViewport を使用してユーザーのピンチ操作によるズームを無効にしました。このデモでは理にかなっていますが、ユーザーがズームインすることを無視する操作を行う場合は、慎重に検討する必要があります。

visualViewport はユーザー補助機能の改善に使用できます。たとえば、ユーザーがズームインしている場合は、装飾的な position: fixed アイテムを非表示にして、ユーザーの邪魔にならないようにすることができます。ただし、ユーザーが詳しく確認しようとしているものを隠さないように注意してください。

ユーザーがズームインしたときにアナリティクス サービスに投稿することを検討してください。これにより、デフォルトのズームレベルでユーザーが操作しにくいページを特定できます。

visualViewport.addEventListener('resize', () => {
    if (visualViewport.scale > 1) {
    // Post data to analytics service
    }
});

これで完了です。visualViewport は、互換性の問題を解決する便利な API です。