古く無効な情報です。
メジャーブラウザで visualViewport が実装され、scrollY
/scrollX
は layout viewport の座標を指すようになりました。
ウェブブラウザでピンチインすると拡大ができるが、この状態で window.scrollX/Y
, getBoundingClientRect
/getClientRect
にアクセスをすると意図しない座標が取れたので調査したのでメモ。
tl;dr 相対座標を扱う時は visual viewport と layout viewport の2つの viewport のどちらをオフセットにしたものか意識することが必要。
具体的に簡単なアプリケーションコードを示して問題をハッキリさせる。基本的にココにあるコードは全部 google chrome を前提にしている。
問題になるコード
ページ上の適当な要素をダブルクリックするとその要素を覆い被せるように移動してくるお邪魔エレメントです。コンソールにコピペすると確認できる:
(() => {
const d = document.body.appendChild(document.createElement("div"));
Object.assign(d.style, {
position: "absolute",
backgroundColor: "black",
opacity: "0.3",
transition: "500ms",
top: "0",
left: "0",
});
window.addEventListener("dblclick", ({ target }) => {
const r = target.getBoundingClientRect();
Object.assign(d.style, {
top: `${r.top + scrollY}px`,
left: `${r.left + scrollX}px`,
width: `${r.width}px`,
height: `${r.height}px`,
});
});
})();
このコードは、拡大をしていない状態では特に問題なく機能する。しかし、ピンチインをして少しでもページを拡大をすると、このお邪魔エレメントは必ず右下にズレて移動をするようになってしまう。大きさは適切なのに。
原因
scrollY
/scrollX
と getBoundingClientRect
/getClientRect
はそれぞれ違うものを基準にした座標を扱っている:
-
scrollY
/scrollX
は visual viewport の左上端の座標を示している。 -
getBoundingClientRect
/getClientRect
は layout viewport の左上端からの相対座標の要素の四角形を示している。
これは visual viewport はその名の通り拡大したときに見えている範囲で、layout viewport は拡大していないときに見える範囲ということ。
この2つの viewport は、拡大していない状態なら同じ四角形の範囲を示しているため、上記のお邪魔エレメントも正常にお邪魔する。しかし拡大をすると、visual viewport は layout viewport にすっぽり収まる小さい四角形になり、意図したとおりにお邪魔してくれなくなる。
本当は図を示して見たほうがいいけどポンチ絵書くの苦手なので他の便利ページを参照:
- 例えばこことか http://www.quirksmode.org/mobile/viewports2.html
- ただしここに出てくる
Scrolling offset
の話は多分仕様と違うっぽいので注意
- ただしここに出てくる
どうするの
実際に JavaScript で扱うときにこれらにどうアクセスするのかという話。ズバリそのものの名前のアクセッサは無いのでちょっとコードが必要。
いかのそれぞれの座標はページ左上端を起点とした絶対座標を指しています。
visual viewport
visual viewport の左上端の座標:
function getVisualViewportOffsets() {
return {
y: window.scrollY,
x: window.scrollX,
};
}
visual viewport のサイズ:
function getVisualViewportSize() {
return {
width: window.innerWidth,
height: window.innerHeight,
};
}
layout viewport
layout viewport の左上端の座標:
function getLayoutViewportOffsets() {
const rootRect = document.documentElement.getBoundingClientRect();
return {
y: - rootRect.top,
x: - rootRect.left,
};
}
layout viewport のサイズ:
function getLayoutViewportSize() {
return {
height: document.documentElement.clientHeight,
width: document.documentElement.clientWidth,
};
}
問題になったコードを修正しよう
getBoundingClientRect
は layout viewport からの相対距離なので、layout viewport の座標を足し算をしよう。scrollX/Y
だと足しすぎて上で確認したように右下にズレてしまう。
つまりこうなる:
(() => {
const d = document.body.appendChild(document.createElement("div"));
Object.assign(d.style, {
position: "absolute",
backgroundColor: "black",
opacity: "0.3",
transition: "500ms",
top: "0",
left: "0",
});
window.addEventListener("dblclick", ({ target }) => {
const layoutVpOffsets = getLayoutViewportOffsets();
const r = target.getBoundingClientRect();
Object.assign(d.style, {
top: `${r.top + layoutVpOffsets.y}px`, // ← scrollY 足すのやめた
left: `${r.left + layoutVpOffsets.x}px`,// ← scrollX 足すのやめた
width: `${r.width}px`,
height: `${r.height}px`,
});
});
function getLayoutViewportOffsets() {
const rootRect = document.documentElement.getBoundingClientRect();
return {
y: - rootRect.top,
x: - rootRect.left,
};
}
})();
これもコピペして実行できる。ピンチインして拡大してもズレなくお邪魔エレメントが覆い被るように移動してくる。
解決。
補足
それぞれの viewport の説明は非常に雑なので、キチンと扱うには言及不足。
スクロールバーの有無でもこの2つの viewport はズレるみたいだけど、今回は簡単のため考慮しないで書いてる。後述の polyfill を参考にすると良いかも。
visual viewport に関してはズバリそのもののアクセッサを用意しようって話はあるみたい:
この polyfill は Google Chrome 以外での viewport の扱いについてクロスブラウザを意識して書かれている。ので chrome 以外の対応が必要なら参考になる。
また、サンプルで用意したダブルクリックするとその要素に被る要素の top/left は、本当に正確にいろんなサイトで動かすには body 要素の position
が static
かそれ以外かを判断し、static
以外なら body の座標も考慮する必要がある。