Android 端末の回転処理

端末を回転させたとき、アプリを見やすくするために要素の配置やサイズを変えたり、要素を非表示にしたいことがあるかと思います。そこで、Android端末の回転処理について調べました。

回転を抑止する

端末を回転させて横置きにすると回転に対応しているアプリでは横幅に合わせて文字や画像が拡大されたりしますが、そもそもアプリを回転させるメリットがない場合は、回転させないようにしたいところです。
Monacaの場合はconfig.xmlに以下の記述をすることでアプリの向きを固定することができます。

// 縦置き
<preference name="Orientation" value="portrait">

// 横置き
<preference name="Orientation" value="landscape">

CSSメディアクエリ

CSSのorientationメディアクエリで端末の回転を検出することができます。

メディアクエリ — MDN
メディアクエリの利用 — MDN

例)縦置きのときツールバーを表示し、横置きのときツールバーを非表示にする

HTML

<ons-toolbar id="tinytoolbar">
  ...
</ons-toolbar>

CSS

@media screen and (orientation: portrait) {  // 縦置き
  #tinytoolbar {
    display: flex;
  }
}
@media screen and (orientation: landscape) {  // 横置き
  #tinytoolbar {
    display: none;
  }
}

Note

ソフトキーボードが起動すると、その高さの分だけコンテンツが縮小します。このため縦置きの状態でもlandscapeのメディアクエリが適用されることがあります。個人的にこれはネックになりやすいのではと思います。

window.orientation

windowオブジェクトのorientationプロパティは、デバイスの本来の向きに対するビューポートの向きを表します。取得できる値は [-90, 0, 90, 180] とのことです。

Window.orientation — MDN

原文(英語)— WHATWG
日本語翻訳版 — WHATWG

Can i useに掲載されていないのがちょっと謎です。window.screenは掲載されているのですが、現在AndroidやiOSでは対応していないようです。

orientationchangeイベントとresizeイベント

端末を回転させるとorientationchangeイベントとresizeイベントが発火するようです。

Script

window.addEventListener("orientationchange", function(e) {
  console.log("orientationchange");
});

window.addEventListener("resize", function(e) {
  console.log("resize");
});

実機で確認してみる!

テスト用のapkを構築してAndroid端末で動かしてみることにしました。

調べたいこと

・orientationchangeイベントとresizeイベントの発火タイミング
・イベント発生時のwindow.orientationの値
・イベント発生時のビューポートの幅と高さ

HTML

<body>
  <ul id="results" contenteditable="true"></ul>
</body>

CSS

#results {
  margin: 0;
  padding: 4px;
}
li.orient { background-color: orange; }
li.resize {}
li.start { background-color: yellow; }

Script

var count = 0;

ons.ready(function() {
  console.log("Onsen UI is ready!");
  print(-1, check());
});

window.addEventListener('orientationchange', function(e) {
  print(0, check());
});

window.addEventListener('resize', function(e) {
  print(1, check());
});

function check() {
  var result = (count++) + ">";
  result += " i(" + window.innerWidth + "," + window.innerHeight + ")";
  result += " o(" + window.outerWidth + "," + window.outerHeight + ")";
  result += " s(" + screen.width + "," + screen.height + ")";
  result += " *(" + window.orientation + ")";
  return result;
}

function print(typ, msg) {
  var results = document.getElementById("results");
  var li = document.createElement("li");
  switch (typ) {
    case 0: li.className = "orient"; break;
    case 1: li.className = "resize"; break;
    case -1: li.className = "start"; break;
    default: break;
  }
  li.appendChild(document.createTextNode(msg));
  results.appendChild(li);
}

テストプログラムでは以下の3か所で値を取得し表示します。
黄色:起動時
:resizeイベント
:orientationchangeイベント

端末はAndroid 4.4.2を使用しています。テストに先立ち、chromeインスペクタでinnerWidth/innerHeightの値を調べました。

縦置き 360 × 567
横置き 598 × 335

ソフトキーボードを表示した状態では以下のようになりました。縦置きでも(幅 > 高さ)になっています。

縦置き 360 × 341
横置き 598 × 151

縦置きでアプリを起動した場合

(1) 端末を縦置きにしてアプリを起動
0 > orientation == 0

(2) 横置きに回転させる
1 > resizeイベント発火 orientation == 0
2 > resizeイベント発火 orientation == 0
3 > orientationchangeイベント発火 orientation == 90

(3) 縦置きに戻す
4 > resizeイベント発火 orientation == 90
5 > resizeイベント発火 orientation == 90
6 > orientationchangeイベント発火 orientation == 0

まとめ

・resizeイベントは回転時に複数回発火していた
・window.orientationはorientationchangeイベントリスナで使えそう(3, 6)
 resizeイベントリスナでは変化しなかった(1, 2, 4, 5)
・innerWidth/innerHeightはどちらのイベントリスナでも使えそう
・screen.width/screen.heightはどちらのイベントリスナでも変化しなかった
・outerWidth/outerHeightはresizeイベントリスナでは変化しなかった

横置きでアプリを起動した場合

(1) 端末を置きにしてアプリを起動
0 > orientation == 0

(2) 縦置きに回転させる
1 > resizeイベント発火 orientation == 0
2 > resizeイベント発火 orientation == 0
3 > orientationchangeイベント発火 orientation == 0

(3) 横置きに戻す
4 > resizeイベント発火 orientation == 0
5 > resizeイベント発火 orientation == 0
6 > orientationchangeイベント発火 orientation == 90

(4) もう一度縦置きにしてみる
7 > resizeイベント発火 orientation == 90
8 > resizeイベント発火 orientation == 90
9 > orientationchangeイベント発火 orientation == 0

イベントの発火や各種サイズの値については縦置きから回転させた場合と同じでした。ただ、予期していなかった点が一つ。
横置きでアプリを起動したときのwindow.orientationの値は90ではなく0ということでした。

これは、回転が発生して初めてwindow.orientationの値が設定される、という感じでしょうか?
横置きにしてアプリのアイコンをタップしてからアプリ画面が表示されるまでをよく観察してみると、まずアプリ画面が表示される前にAndroid端末のフレーム表示(上部にあるバッテリーや時間の表示、下部にあるホームボタン、戻るボタンなど)が回転しています。その後アプリ画面が表示されますが、この間に回転イベントは発火していません。

はっきりとはいえませんが、とりあえず回転イベントが発生する前にwindow.orientationを参照しないほうがよさそうです。

キーボードを表示した状態で回転

縦置きでアプリを起動後、キーボードを表示した状態で回転させてみました。

(1) 端末を縦置きにしてアプリを起動
0 > orientation == 0

(2) キーボードを表示させる(一行目をタップするとcontenteditable属性によりキーボードが起動する)
1 > resizeイベント発火
2 > resizeイベント発火

(3) 横置きに回転させる
3 > resizeイベント発火
4 > resizeイベント発火
5 > orientationchangeイベント発火 orientation == 90

(3) 縦置きに戻す
6 > resizeイベント発火
7 > resizeイベント発火
8 > orientationchangeイベント発火 orientation == 0
9 > resizeイベント発火
10 > resizeイベント発火
11 > resizeイベント発火
12 > resizeイベント発火

まとめ

・resizeイベントはキーボードの描画のためかorientationchangeイベントの発火後も数回発火した
・orientationchangeイベントはresizeイベントとは異なり一回転につき一回だけ発火した
・innerWidth/innerHeightは描画が完全に完了してからでないと最終的な値は取得できなさそう

Note

数回テストしましたが、resizeイベントの発火回数やresizeイベントとorientationchangeイベントの発火順序がこの結果と異なることがたまにありました。おそらく順番に発火していくのではなくそれぞれが独立した発火タイミングを持っていて、再描画などの条件によりずれるのだと思います。

縦置き・横置きを検出してみる!

気をつける点

・回転が発生する前のwindow.orientationは参照しない
・回転発生後はorientationchangeイベントリスナでwindow.orientationを参照できる
・window.orientationの0という値は「自然な方位を表現する(0 represents the natural orientation.)」と定義されており、縦置きという意味ではない
・キーボードが表示されている状態では、幅と高さの関係から縦置き・横置きを判定できない

これらを踏まえて以下のように実装しました。

var orn = {
  portrait: true,     // true: portrait, false: landscape
  orientgp: -1,        // 0: 0 or 180, 1: 90 or -90
  oflg: false,
  initOrn: function() {  // キーボードが表示されていない状態で使用する
    this.portrait = window.innerWidth < window.innerHeight;
  },
  initGroup: function() {  // orientationchangeイベントで呼び出す
    if (this.oflg === false) {  // 初回の回転時
      this.oflg = true;
      this.orientgp = !this.getOrnGroup();  // アプリ起動時のorientation値を設定
    }
  },
  getOrnGroup: function() {
    return (window.orientation === 0 || window.orientation === 180) ? 0 : 1;
  },
// 回転が発生していない場合、orientation値は不定(-1)とみなし、この状態の縦横比に従う
  isPortrait: function() {
    var xor = (this.orientgp === -1) ? false : this.orientgp ^ this.getOrnGroup();
    if ((!xor && this.portrait) || (xor && !this.portrait)) return true;
    else return false;
  }
};

document.addEventListener('init', function(event) {  // DOM初期化完了イベント
  orn.initOrn();
});

window.addEventListener('orientationchange', function(e) {
  orn.initGroup();
});

アプリの起動時にinitOrn()により縦置きか横置きかを判定しportraitに保存しておきます。
初回の回転イベントでorientation値を取得し、その値の否定値をとることで、回転イベント発生前のorientation値とし保存します。このorientation値と先ほどのportrait値をペアとして、以降、縦横判定を行いたいときにisPortrait()により現在のorientation値と比較することで縦横を判定します。

使い方は以下のようになります。

// 回転発生時
window.addEventListener('orientationchange', function(e) {
  orn.initGroup();
  if (orn.isPortrait()) {
    console.log("portrait");  // 縦置き
  } else {
    console.log("landscape");  // 横置き
  }
});
// キーボード表示時(cordovaプラグイン)
window.addEventListener('native.keyboardshow', function(e) {
  if (orn.isPortrait()) {
    console.log("portrait");  // 縦置き
  } else {
    console.log("landscape");  // 横置き
  }
});

キーボードさえ表示されなければ、orientationchangeイベントリスナでinnerWidthとinnerHeightを比較するだけで縦横判定できそうなんですけどね。
そういえば最初、キーボードが表示しているかどうかが分かればorientationchangeイベントリスナで縦横判定できるのではと思い、上記キーボード表示イベントリスナでフラグを立てるようにしたら、うまくいきませんでした。このリスナ、キーボードが表示されていなくても端末を回転させたときにイベントを受けていました。showもないなーと調べてみると、このkeyboardshowの後すぐにkeyboardhideも発生していました。それでキーボードは表示されずに何事もないように見えていたというわけです。いやーちょっと怖いですね。
最後蛇足になりましたが、以上です。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA