Smoking Note V1.2.5 アップデート

喫煙管理アプリ「Smoking Note」のアップデートを行いました。

V1.2.5 アップデート内容

  • 誤入力データの取消機能追加
  • 表示文字やエフェクトなどのブラッシュアップ

設定画面の「いっぷく時間の取消」ボタンから直前の記録を削除できるようになりました。連続タップや誤入力をしてしまっても元に戻すことができます。

また、メイン画面で使用している文字や時間更新エフェクト、グラフ画面の表の色などを少し変更しました。

処理時間の計測

アプリの起動がみょーに遅くて、どこで時間がかかっているのか調べたくなることってありますよね。私はしょっちゅうあります。そこで、JavaScriptで処理時間を計測する方法について調べました。メジャーな方法は次の2つのようです。

  • consoleオブジェクトのtime()/timeEnd()メソッドを使う
  • DateオブジェクトのgetTime()メソッドを使う

それぞれについて簡単に説明します。

consoleオブジェクトのtime()/timeEnd()メソッドを使う

console.time()メソッドによりタイマーを開始し、console.timeEnd()メソッドでタイマーを停止してその間の経過時間をミリ単位で取得します。引数はタイマーを選別する任意の名前になります。

console.time() — MDN
console.timeEnd() — MDN

使用例

console.time("timer_0");
alert("click!");
console.timeEnd("timer_0");

console.timeEnd()メソッドを実行すると経過時間がコンソールに出力されます。

ここで、戻り値がどうなっているのか気になって実験してみました。

実験

var a = console.time("timer_0");
alert("click!");
var b = console.timeEnd("timer_0");

console.log("a: " + a + ", b: " + b);

コンソール出力

timer_0: 7798.10693359375ms
a: undefined, b: undefined

戻り値はundefinedでした。取得した経過時間をUIに表示したりプログラムで使うことはできないみたいです。console()メソッドですから当然といえば当然ですかね。

DateオブジェクトのgetTime()メソッドを使う

Date()コンストラクタからインスタンス化した日付オブジェクト(new Date())のgetTime()メソッドは、1970年1月1日00:00:00からインスタンス化までの時間をミリ秒で返します。処理時間は、計測開始時と終了時にgetTime()メソッドでミリ秒を取得し、その差分をとることで得ることができます。
ちなみに、インスタンス化せずに直接Dateオブジェクトのnow()メソッドを使用してもgetTime()メソッドと同じ結果になります。ただしIE9未満で未サポートになっています。

getTime() — MDN

使用例

var stime = new Date().getTime();
for(var i=0, a=0; i<100000; i++) a += i;
var etime = new Date().getTime();

console.log("time: " + (etime - stime));

時間計測用のオブジェクトを作ってみた

処理時間を計測したいポイントが複数ある場合、各ポイントにgetTime()の実行と取得した値を格納するコードを埋め込み、最後にループ文などで差分を計算して表示する、というパターンになるかと思います。そこで時間計測用オブジェクトを作成してみました。

Script

var timeLog = (function() {
  var timeLog = {};
  var memory = [];
  timeLog.rec = function(comment) {
    memory.push({
      comment: comment || "",
      time: new Date().getTime()
    });
  };
  timeLog.show = function(flg) {
    var n = memory.length;
    if (n<=1) {
      console.log("Too few records");
      return;
    }
    var stime = memory[0].time, etime, delta, log, logs="";
    for (var i=1; i<n; i++) {
      etime = memory[i].time;
      delta = etime - stime;
      stime = etime;
      log = "[" + i + "] " + memory[i].comment + ": " + delta + "ms";
      logs += log + "\n";
      console.log(log);
    }
    if (flg) window.alert(logs);
  };
  timeLog.clear = function() {
    memory.length = 0;
  };
  return timeLog;
})();

使用例

timeLog.rec("start");
for(var i=0, a=0; i<100000; i++) a += i;
timeLog.rec("一回目の処理");
for(var i=0, a=0; i<1000000; i++) a += i;
timeLog.rec("二回目の処理");
for(var i=0, a=0; i<10000000; i++) a += i;
timeLog.rec("最後の処理");

timeLog.show();

コンソール出力

[1] 一回目の処理: 9ms
[2] 二回目の処理: 33ms
[3] 最後の処理: 193ms

一連の処理をまとめただけなのでその必要性が微妙なのですが、使用感はすっきりさわやかです。また、show()の引数にtrueなどを渡して呼び出すとアラートダイアログを表示します。release版のapkなどで確認する場合に使えると思います。

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も発生していました。それでキーボードは表示されずに何事もないように見えていたというわけです。いやーちょっと怖いですね。
最後蛇足になりましたが、以上です。