Help us understand the problem. What is going on with this article?

もう逃げない。HTMLのviewportをちゃんと理解する

More than 1 year has passed since last update.

viewportをちゃんと理解する

今までviewportをよく理解せず、ごまかしごまかし

<meta name="viewport" content="width=device-width,initial-scale=1">

と呪文のように書いてきたが、いい加減ちゃんと整理して理解しよう。
いろいろ調査した結果、以下の考え方で理解できると思う。

  1. まず、実際の液晶の解像度は一旦忘れろ。
  2. <meta name="viewport" content="width=480">と指定したとする。
  3. するとそこに幅480pxの仮想的なウインドウが作られる。幅480pxの液晶モニターがあることをイメージして欲しい。これをviewportと呼ぶ。
  4. ブラウザはviewportにレンダリングする。viewportの中では、あたかも本当に480pxのモニターを使っているかのような環境になっている。なので、JSのdocument.documentElement.clientWidthなんかも480を返すし、media queryの条件判定でも480pxの画面幅が使われる。
  5. 次にviewportを実際の液晶画面に表示する。この時、viewportの幅が実際の液晶画面よりも大きければviewportがはみ出して、液晶画面にはviewportの一部しか表示されないだろう。逆に、viewportの幅が液晶画面よりも小さければviewportの内容は液晶画面に収まるが、液晶画面には余白ができてしまうだろう。これを調整するため、ブラウザはviewportのレンダリング結果を液晶の画面サイズに合うようにズームアップまたはダウンして表示する。
  6. <meta name="viewport" content="width=device-width">と指定した場合はviewportの幅は端末やブラウザアプリ毎によって異なる。重要なのはここでも実際の液晶の解像度ではなく360とか580などのスマホらしい小さい値が使われるということだ。

結局のところ

<meta name="viewport" content="width=360,initial-scale=1">

とか

<meta name="viewport" content="width=device-width,initial-scale=1">

を使えばいい。(結局、上の呪文と同じ形に落ち着いた)
どちらを使うべきかは状況によるが以下の一長一短があることを覚えておくといい。
前者は数字を明確に指定しているので端末ごとの表示を統一できるが、逆に言うと大きな画面サイズの端末でも小さな画面サイズの端末と同じ表示になるため、スペースの有効活用ができないことが有る。
後者は端末ごとのサイズに応じてフレキシブルな表示ができる一方、細かい表示の統一はやりづらい。

変な挙動になるのでやるべきではないこと

  • width=device-width, initial-scale=0.5
    このようにviewport幅が画面幅と同じで、縮小表示する必要がないにも関わらず縮小表示を強要するような設定にした場合、viewportの幅が予期せず大きくなってしまうことが有る。何故なら縮小するためにはviewportの幅が液晶画面よりも大きく無ければならないからだ。この例だとviewportの幅はdevice-widthの2倍になってしまう。

  • width=320, initial-scale=0.5
    これも上と同じ。320という、大抵の端末のdevice-widthより小さい値を指定した場合、やはり大抵の機種では縮小の必要がない。なのに縮小を強要するとviewportの幅を無理やり大きくして対応しようとしてしまうのだ。

  • width=240, initial-scale=1.0
    これは上の2つの逆パターン。viewportの幅は液晶画面より小さいのでそのまま表示したのでは液晶画面に余白ができてしまうため、デフォルトでviewportをズームアップするようになっている。つまり、暗黙のうちにinitial-scaleが1より大きな値となっているのである。そこに無理やり1.0というズーム率を指定するとどうなるかというと、液晶画面に余白が出来るのではなく、ブラウザはそれを防ぐために勝手にviewportの幅を大きくしてしまうのだ。

  • width=1280,initial-scale=0.5
    これは一見問題無さそうに見えるが、機種やブラウザによってはviewportが予期せぬ値に書き換えられてしまう。viewportは大きくし、それを縮小して初期状態で液晶画面に収まるようにしたいという狙いで使いたくなるが非常に挙動が不安定。試した限り、リロードする度に$(window).width()の結果が0になったり異常に大きな値になったりと訳の分からないことが起こった。initial-scaleに1より小さな値は使わないのが無難。

viewportはJSで切り替える技

以下の様なJSを使うとviewportを動的に変更できる。
手元のAndroidとiPhoneで試した限りではchromeでもsafariでもちゃんと動作する。
しかも、レンダリングが終了した後にこれを実行してもちゃんと新しいwidthで再レンダリングしてくれる。
(ただし、chromeではこれとmedia queriesを組み合わせた場合に、レンダリング後の再レンダリング時に期待通りのスタイルが適用されないというバグっぽい挙動をすることが有った)

document.getElementById('viewport').setAttribute('content', 'width=360,initial-scale=1');

スマホとPCでのデザインの切替はこうする

幅が1080pxのスマホが増えて、PCブラウザの画面幅と変わらないくらいの画面幅を持つようになった。
こうなるとメディアクエリーで画面幅によってレイアウトを変更するという芸当だけではダメなのでは?

心配いらない。スマホのブラウザはPCのブラウザと違って実際の液晶の解像度ではなく、
viewportに設定した画面幅によって支配されている。

例えばjQueryで$(window).width()なんてやるとPCでは普通にブラウザウインドウの幅を返すが、スマホではviewportの設定によって返る値が変わる。

例えば、

<meta name="viewport" content="width=480">

としてあれば、$(window).width()は480が返す。
そして、この値はmedia queryでも使用される。
従って、上のようにviewportのwidthを480pxに固定しているなら、480pxをブレークポイントにしたメディアクエリーで表示切り替えすればいい。
では、viewportのwidthにdevice-widthを使用している場合はどうかというと、この場合も前述の様に(大抵の)スマホではviewportのwidthが(実際の液晶の解像度とは無関係に)360とか580というスマホらしいサイズになるので問題ない。

スマホ判定にwindow.screen.widthを使う

少々余談だが、これまで説明したように、スマホでは実際の液晶の解像度とは無関係に色々と事が運ぶ。
device-widthの値もそうだし、window.screen.widthの値もそうだ。
これを利用するとwindow.screen.widthだけでスマホかどうか判定できる(かもしれない)。
window.screen.widthはPCではモニターの解像度(横方向のピクセル数)を返す。
一方、スマホでは実際の端末の液晶サイズとは無関係にスマホらしい320とか360とかいう数値を返す。
この値は(おそらく)ほとんどの端末でviewportのdevice-widthと同じ値になる。
しかも、viewportのwidth設定に関わらず常に同じ値を取得できる。
これが使えたら、PC並みの解像度を持つ端末が増えても、WindowsPhoneやFirefoxPhoneが増えても、UserAgentで判定するより簡単に判定できて便利だろう。

JSだけでスマホ、PCの表示モードを切り替える

これは少々実験的なテクニックなので、全ての端末で期待通り動くとは限らないが、今までの説明の集大成として紹介する。

<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script>
var pc_width = 1080;
var device = (screen.width < 600 ? 'sp' : 'pc');
var view_mode = (device == 'pc' || document.cookie.indexOf('view_mode=pc') != -1 ? 'pc' : 'sp');
if (device == 'sp' && view_mode == 'pc') {
    document.getElementsByName('viewport')[0].setAttribute('content', 'width=' + pc_width + ',initial-scale=1');
}

$(function () {
    if (device == 'sp') {
        if (view_mode == 'pc') {
            $('.sp-mode').on('click', function () {
                var date = new Date();
                date.setTime(0);
                document.cookie = 'view_mode=;expires='+date.toGMTString();
                location.reload(false);
            }).show();
        } else {
            $('.pc-mode').on('click', function () {
                document.cookie = 'view_mode=pc';
                location.reload(false);
            }).show();
        }
    }
});
</script>
</head>

(中略)

<button class="sp-mode" style="display:none;">スマートフォン用で表示</button>
<button class="pc-mode" style="display:none;">PC用で表示</button>

参考

ryounagaoka
福岡に引きこもっている永遠の中2
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした